To capture a page with PyQt4 and QWebKit, load the URL into a QWebPage, wait for loadFinished(bool), set the viewport you want, render the main QWebFrame into a QImage with QPainter, and save the image. The example below produces a full-content PNG without displaying a browser window, then explains the visible QWebView alternative, viewport choices, asynchronous pages, common failures, and migration to Qt WebEngine.
What the PyQt4 capture pipeline does
Qt WebKit separates the browser document from the widget that displays it. QWebView is the convenient visible widget; its QWebPage owns the document, and the page’s main QWebFrame contains the top-level page. For a screenshot service or batch job, you can use QWebPage directly and render that frame yourself.
- Create a page and obtain
mainFrame(). - Connect
loadFinished(bool)before starting navigation. - When the signal arrives, stop if its Boolean argument is false.
- Choose a viewport. Use the frame’s
contentsSize()for a full-content image, or a fixed size for a viewport screenshot. - Create a matching
QImage, paint the frame into it, end the painter, and callimage.save().
The loadFinished signal means loading succeeded or failed; it does not guarantee that JavaScript-driven visual changes or late rendering have finished. Pages that build their visible content after the initial load need an additional, site-specific readiness strategy.
Complete headless-style PyQt4 example
This adaptation follows Qt WebKit’s documented rendering flow. It is intentionally explicit so you can change the viewport, output format, and readiness logic.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →#1 Best Overall
import sys
from PyQt4.QtCore import QUrl, QObject, SIGNAL
from PyQt4.QtGui import QApplication, QImage, QPainter
from PyQt4.QtWebKit import QWebPage
class Capture(QObject):
def __init__(self, url, output_path):
QObject.__init__(self)
self.output_path = output_path
self.page = QWebPage()
self.frame = self.page.mainFrame()
self.page.loadFinished.connect(self.save_capture)
self.frame.load(QUrl(url))
def save_capture(self, ok):
if not ok:
sys.stderr.write("Page load failed\n")
QApplication.instance().quit()
return
# Full-frame capture: make the viewport as large as the document.
self.page.setViewportSize(self.frame.contentsSize())
image = QImage(self.page.viewportSize(), QImage.Format_ARGB32)
image.fill(0xffffffff)
painter = QPainter(image)
self.frame.render(painter)
painter.end()
if not image.save(self.output_path):
sys.stderr.write("Could not save image\n")
QApplication.instance().quit()
if __name__ == "__main__":
app = QApplication(sys.argv)
capture = Capture("https://example.com/", "capture.png")
sys.exit(app.exec_())
Keep the Capture object alive until the event loop finishes; otherwise it may be garbage-collected before the asynchronous load completes. The image is allocated after the content size is known, so the output matches the selected viewport. Saving a separate thumbnail copy is safer than resizing the original if you need both a full-resolution capture and a preview.
Full page versus viewport screenshots
Full-content capture
setViewportSize(frame.contentsSize()) follows Qt’s full-content example. It asks WebKit to lay out the page at the document’s current width and height, then renders the main frame into an image of that size. Very long pages can require substantial memory because the entire bitmap is held at once.
Fixed viewport capture
For a browser-window style screenshot, set a deliberate size instead:
from PyQt4.QtCore import QSize
page.setViewportSize(QSize(1366, 768))
image = QImage(page.viewportSize(), QImage.Format_ARGB32)
The viewport affects layout, including responsive breakpoints and scrollbar visibility. A 375-pixel mobile layout and a 1440-pixel desktop layout are different documents from the renderer’s perspective, so record the width and height with your output when reproducibility matters. A fixed viewport also avoids creating an enormous image for an exceptionally tall document.
Frames and embedded content
QWebFrame represents an individual frame. Rendering the main frame is the normal operation; Qt’s rendering example states that the contents and subframes are rendered into the painter. Site-specific behavior can still vary for delayed assets, plugins, cross-origin resources, or other dynamic content, so treat the result as a WebKit rendering rather than a guarantee of pixel-perfect modern-browser output.
Rank #2
Using QWebView when a visible widget is useful
If your application already has a GUI, QWebView avoids creating and wiring the page manually. Connect its page’s load signal, show or embed the widget, and render its main frame when ready:
from PyQt4.QtCore import QUrl
from PyQt4.QtGui import QApplication, QImage, QPainter
from PyQt4.QtWebKit import QWebView
app = QApplication([])
view = QWebView()
view.resize(1024, 768)
def capture(ok):
if not ok:
app.quit()
return
frame = view.page().mainFrame()
view.page().setViewportSize(view.size())
image = QImage(view.size(), QImage.Format_ARGB32)
painter = QPainter(image)
frame.render(painter)
painter.end()
image.save("widget-capture.png")
app.quit()
view.page().loadFinished.connect(capture)
view.load(QUrl("https://example.com/"))
view.show()
app.exec_()
Choose QWebView when users need to see or interact with the page. Choose a widget-less QWebPage when capture is a background operation and you want direct control over rendering. The available documentation does not establish a performance winner between the two approaches.
Waiting for pages that change after load
A successful load signal can arrive before a single-page application inserts its final content. Qt’s QWebPage documentation explicitly describes loadFinished() as independent of script execution or page rendering. If the target has a known readiness condition, add one rather than assuming a timer solves every case.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Readiness options
- Wait for a known DOM element by polling JavaScript from a timer, then capture when it exists.
- Use a short, page-specific delay when the site has a predictable animation or data request.
- Capture only after your application observes the page’s own completion signal or state.
These approaches require application-specific code because PyQt4/QWebKit provides no universal “all network and scripts are visually settled” signal. A delay that works for one site can be too short for another and unnecessarily slow for a third.
Output format, memory, and reliability considerations
- Bitmap size: memory rises with width, height, color depth, and any additional copies. A full-page image can be much larger than the compressed PNG on disk.
- Color and transparency:
Format_ARGB32is a practical general-purpose format. Filling white gives pages with transparent or unpainted areas a predictable background. - Save errors: check the Boolean result of
image.save(); a valid render does not imply that the destination path is writable. - Event loop: navigation and painting are asynchronous. Run the Qt event loop and do not exit immediately after calling
load(). - Repeatability: fix the viewport, URL, output format, and readiness rule. Responsive layout, animations, ads, and remote assets can otherwise change the pixels between runs.
Troubleshooting PyQt4 and QWebKit captures
The output file is missing or empty
Confirm that the event loop is running, that the capture object remains referenced, and that loadFinished is connected before navigation starts. Check the return value from image.save() and use an absolute, writable path.
The callback reports false
The Boolean argument indicates that loading failed. Verify the URL, DNS and TLS support available in the old WebKit build, and whether the page requires authentication or redirects that the embedded engine cannot complete. Do not save a capture when ok is false.
The screenshot contains only the top of the page
You rendered a viewport-sized image. For a full-content result, set the viewport from frame.contentsSize() before allocating the image. If the document grows after that point, wait for the page’s readiness condition and query contentsSize() again.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Responsive layout is wrong
Set an explicit viewport width and height before rendering. Viewport dimensions influence CSS layout and scrollbar behavior, so matching a target device requires matching its intended dimensions rather than simply enlarging the output bitmap afterward.
JavaScript content is absent
loadFinished is not a script-completion signal. Add a DOM or application readiness check, and capture only after the content has been inserted. A fixed delay is a fallback, not a universal guarantee.
Images or subframes do not appear
Rendering the main frame includes documented subframe content, but remote resources, plugins, security restrictions, and delayed loading remain site-dependent. Check whether the resource is available to the embedded engine and whether you are capturing before it arrives.
Qt WebKit is a legacy stack
PyQt4 and Qt WebKit target an older browser engine. Qt’s porting guidance distinguishes the WebKit modules (QT += webkitwidgets, QWebPage, and QWebFrame) from Qt WebEngine (QT += webenginewidgets and QWebEnginePage). WebEngine merges frame handling into the page, so methods such as QWebFrame.load() become page methods; this is not a safe mechanical class-name replacement.
Recommended Free Tools
Use the Qt WebKit API when maintaining a PyQt4 application that depends on it. For new work, evaluate a supported WebEngine binding and follow its migration documentation, then re-check page behavior because the rendering engine and security model differ. The archived Qt 4.7 documentation describes support for HTML, XHTML, SVG, CSS, and JavaScript, but that historical capability list is not a compatibility promise for current websites.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers.
For a direct image request, see the ScreenshotNeo documentation:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
The same call in Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
And in Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', data);
ScreenshotNeo also supports full-page captures with lazy images loaded, CSS-selector element captures, dark mode, device presets and custom viewports, retina scale, PDF paper and page options, HTML/CSS rendering, custom JavaScript and CSS, clicks, selector waits, delays, network-idle waits, request and resource blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, configurable caching, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
The Free plan includes 1,000 shots each month with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan, and yearly billing gives two months free. Sign up for the free plan to try it without a card.
Best Value
FAQ
Does loadFinished wait for every image?
No. It reports load success independently of script execution and page rendering. Use a readiness condition appropriate to the site when late content matters.
Can I capture only one element?
Qt’s basic flow renders a frame. To isolate an element, you need additional page-side positioning or a capture service with selector support, such as ScreenshotNeo’s CSS-selector option.
Why does changing the viewport change the page?
Viewport dimensions participate in layout and scrollbar decisions, so responsive CSS can select different styles and content.
Is PyQt4 syntax identical on every release?
Binding details can vary across PyQt4 versions. Treat the example as a PyQt4-flavored implementation of the documented Qt C++ flow and verify it against the exact binding installed in your environment.
Frequently Asked Questions
Does loadFinished wait for every image?
No. It reports load success independently of script execution and page rendering. Use a readiness condition appropriate to the site when late content matters.
Can I capture only one element?
Qt’s basic flow renders a frame. To isolate an element, you need additional page-side positioning or a capture service with selector support, such as ScreenshotNeo’s CSS-selector option.
Why does changing the viewport change the page?
Viewport dimensions participate in layout and scrollbar decisions, so responsive CSS can select different styles and content.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Is PyQt4 syntax identical on every release?
Binding details can vary across PyQt4 versions. Treat the example as a PyQt4-flavored implementation of the documented Qt C++ flow and verify it against the exact binding installed in your environment.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

