ScreenshotNeo

BlogScreenshots on your device

How to Capture a Specific Screen Area With PyQt

Capture a precise desktop rectangle in PyQt with QScreen.grabWindow(), including coordinates, high-DPI scaling, multi-monitor layouts, and troubleshooting.

By the ScreenshotNeo team1 October 20268 min read

How to Capture a Specific Screen Area With PyQt

Use QScreen.grabWindow(0, x, y, width, height) to capture one visible rectangle. It returns a QPixmap that you can save as PNG, JPEG, or another supported image format. The rectangle uses device-independent coordinates, and the coordinate origin depends on the operating system. On high-DPI displays, the saved pixel dimensions can be larger than the requested logical dimensions.

The smallest complete PyQt6 example is:

from PyQt6.QtGui import QGuiApplication

app = QGuiApplication([])
screen = QGuiApplication.primaryScreen()
if screen is None:
    raise RuntimeError("No screen is available")

x, y, width, height = 100, 100, 400, 300
pixmap = screen.grabWindow(0, x, y, width, height)
if pixmap.isNull():
    raise RuntimeError("Screen grab returned a null pixmap")

if not pixmap.save("region.png", "PNG"):
    raise RuntimeError("Could not save the captured region")

Qt documents grabWindow(0) without coordinates as the portable way to grab an entire screen. For a sub-area, validate the rectangle and account for monitor geometry before calling the method.

1. Install PyQt and choose a screen

Install PyQt6 in a virtual environment:

python -m venv .venv
# macOS/Linux
. .venv/bin/activate
# Windows PowerShell: .venv\\Scripts\\Activate.ps1
python -m pip install PyQt6

QScreen is part of Qt GUI, so a QGuiApplication is sufficient for a command-line capture script. A widgets application can use QApplication instead.

To inspect every connected display:

from PyQt6.QtGui import QGuiApplication

app = QGuiApplication([])
for index, screen in enumerate(app.screens()):
    print(index, screen.name(), screen.geometry(), screen.devicePixelRatio())

Use primaryScreen() for the main display, or select a screen by index, name, or the screen containing a point. The screen’s geometry() is expressed in the coordinate system expected by Qt for that display.

2. Capture and save a validated rectangle

A production script should reject empty sizes and keep the requested area inside the selected display. Intersecting with screen.geometry() prevents unsafe out-of-bounds grabs when a user drags a selection past an edge.

A drag selection becomes a QPixmap through QScreen.grabWindow().
A drag selection becomes a QPixmap through QScreen.grabWindow().
from pathlib import Path
from PyQt6.QtCore import QRect
from PyQt6.QtGui import QGuiApplication


def capture_region(output: str, x: int, y: int, width: int, height: int) -> None:
    app = QGuiApplication.instance() or QGuiApplication([])
    screen = app.primaryScreen()
    if screen is None:
        raise RuntimeError("No screen is available")
    if width <= 0 or height <= 0:
        raise ValueError("width and height must be positive")

    requested = QRect(x, y, width, height)
    visible = requested.intersected(screen.geometry())
    if visible.isEmpty():
        raise ValueError(
            f"Rectangle {requested} does not intersect {screen.geometry()}"
        )
    if visible != requested:
        raise ValueError(
            f"Rectangle extends outside the screen: requested={requested}, "
            f"screen={screen.geometry()}"
        )

    pixmap = screen.grabWindow(0, visible.x(), visible.y(),
                               visible.width(), visible.height())
    if pixmap.isNull():
        raise RuntimeError("Screen grab returned a null pixmap")

    Path(output).parent.mkdir(parents=True, exist_ok=True)
    if not pixmap.save(output):
        raise OSError(f"Could not save {output}")

    print({
        "file": output,
        "logical_size": [visible.width(), visible.height()],
        "pixmap_size": [pixmap.width(), pixmap.height()],
        "device_pixel_ratio": pixmap.devicePixelRatio(),
    })


if __name__ == "__main__":
    capture_region("captures/region.png", 100, 100, 400, 300)

Call pixmap.devicePixelRatio() and pixmap.size() when you need the physical output dimensions. A retina or other scaled display can return more physical pixels than the requested 400 × 300 logical rectangle.

3. Understand coordinates on multiple monitors

Platform Zero-window grab coordinates Practical rule
Windows Screen-local coordinates Use coordinates relative to the selected screen.
X11/Linux Screen-local coordinates Use the selected screen’s expected local coordinate space.
macOS Virtual-desktop-relative coordinates Convert a point from your desktop selection into the selected screen’s coordinate origin.

Qt’s QScreen documentation describes these origin differences. If your selection tool reports a virtual-desktop point on macOS, subtract screen.geometry().topLeft() before passing the coordinates for that screen. Always inspect the geometry returned by Qt instead of hard-coding monitor positions.

Device-independent coordinates can produce larger physical images on high-DPI screens.
Device-independent coordinates can produce larger physical images on high-DPI screens.
from PyQt6.QtCore import QPoint


def virtual_to_screen_local(screen, point: QPoint) -> QPoint:
    # Needed when your selection point is expressed relative to the
    # virtual desktop and the target platform requires screen-local values.
    return point - screen.geometry().topLeft()

Do not assume that the top-left display is at (0, 0). A monitor can be placed to the left or above another display, producing negative virtual coordinates.

4. Build a drag-to-select PyQt tool

For an interactive selector, show a transparent full-screen widget, record the mouse press and release positions, normalize the rectangle, then call grabWindow. The example below captures the primary display and writes the selected area.

import sys
from PyQt6.QtCore import QPoint, QRect, Qt
from PyQt6.QtGui import QGuiApplication, QPainter, QPen
from PyQt6.QtWidgets import QApplication, QWidget


class RegionSelector(QWidget):
    def __init__(self):
        super().__init__()
        self.screen = QGuiApplication.primaryScreen()
        if self.screen is None:
            raise RuntimeError("No screen is available")
        self.setGeometry(self.screen.geometry())
        self.setWindowFlags(
            Qt.WindowType.FramelessWindowHint |
            Qt.WindowType.WindowStaysOnTopHint |
            Qt.WindowType.Tool
        )
        self.setWindowOpacity(0.25)
        self.start = QPoint()
        self.end = QPoint()
        self.dragging = False
        self.setCursor(Qt.CursorShape.CrossCursor)

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self.start = event.position().toPoint()
            self.end = self.start
            self.dragging = True
            self.update()

    def mouseMoveEvent(self, event):
        if self.dragging:
            self.end = event.position().toPoint()
            self.update()

    def mouseReleaseEvent(self, event):
        if event.button() != Qt.MouseButton.LeftButton or not self.dragging:
            return
        self.end = event.position().toPoint()
        self.dragging = False
        local_rect = QRect(self.start, self.end).normalized()
        if local_rect.isEmpty():
            self.close()
            return

        # Add the overlay's screen origin to obtain the screen geometry point.
        screen_rect = local_rect.translated(self.geometry().topLeft())
        self.hide()
        pixmap = self.screen.grabWindow(
            0, screen_rect.x(), screen_rect.y(),
            screen_rect.width(), screen_rect.height()
        )
        if pixmap.isNull() or not pixmap.save("selected-region.png", "PNG"):
            raise RuntimeError("Could not save selected-region.png")
        self.close()

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.fillRect(self.rect(), Qt.GlobalColor.black)
        if self.dragging:
            rect = QRect(self.start, self.end).normalized()
            painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Clear)
            painter.fillRect(rect, Qt.GlobalColor.transparent)
            painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
            painter.setPen(QPen(Qt.GlobalColor.white, 2))
            painter.drawRect(rect)


app = QApplication(sys.argv)
selector = RegionSelector()
selector.show()
sys.exit(app.exec())

This simple selector covers one display. A multi-monitor selector should create one overlay per screen or use a virtual-desktop overlay, then convert the final rectangle into the coordinate system required by the chosen QScreen. Hide the overlay before grabbing so its dimming layer is not included.

5. What the capture contains

  • Visible pixels: grabWindow reads the desktop image. Another window covering your rectangle appears in the result.
  • Cursor: Qt documents that the mouse cursor is generally not captured.
  • Overlays: Menus, notifications, video frames, and other compositor output can be present if they are visible at capture time.
  • Application content: If you need an off-screen image of your own widget regardless of desktop overlap, use the widget’s rendering or grab facilities for your Qt version instead of a screen grab.

6. Still images versus continuous capture

QScreen.grabWindow is a one-shot still-image API. It is appropriate for screenshots, bug reports, thumbnails, and user-selected regions. Qt Multimedia’s QScreenCapture is a separate API for continuous capture through a media capture session. Current Qt documentation describes an FFmpeg backend requirement and, on Wayland, an operating-system selection flow through the XDG Desktop Portal and PipeWire. Those requirements apply to the multimedia stream API, not automatically to a single grabWindow call.

7. Troubleshooting

Symptom Likely cause Fix
pixmap.isNull() is true No usable screen, invalid rectangle, or platform capture restriction. Check primaryScreen(), validate positive dimensions, intersect with screen.geometry(), and try a small in-bounds rectangle.
Image is shifted on macOS Virtual-desktop coordinates were passed where the selected screen’s origin was required. Inspect screen.geometry().topLeft() and convert the selection point before grabbing.
Only part of the requested area appears The rectangle crosses a monitor boundary or extends outside the display. Split the request per screen or reject/intersect out-of-bounds rectangles.
Output dimensions are unexpectedly large High-DPI scaling and the pixmap device-pixel ratio. Read devicePixelRatio(); resize only if a fixed physical size is required.
The selector appears in the screenshot The overlay was still visible during capture. Call hide(), allow the event loop to process the hide, then grab.
A different window appears in the result The API captures visible desktop pixels, not an isolated application surface. Move or hide the covering window, or use a widget-specific rendering method.
Wayland permissions or a picker appear You are using Qt Multimedia screen capture on Wayland. Complete the portal selection flow and ensure PipeWire/ScreenCast support; do not apply these streaming requirements to every still grab.
Saved file cannot be opened Unsupported format, unwritable path, or a failed save. Use a known extension such as .png, create the parent directory, and check the boolean result of pixmap.save().

8. Performance, reliability, and cost considerations

  • Capture cost: A single local grab is usually bounded by the compositor and image encoding. Avoid repeatedly encoding huge full-screen images when a smaller rectangle is sufficient.
  • Memory: High-DPI screenshots contain more physical pixels. Release or reuse pixmaps in loops and save directly when possible.
  • Timing: Hide selection overlays and wait for the window system to repaint before grabbing. For animated content, capture after the desired frame is visible.
  • Reliability: Log the selected screen name, geometry, requested rectangle, pixmap size, and device-pixel ratio. These values make coordinate bugs reproducible.
  • Security: A desktop screenshot can contain passwords, tokens, notifications, or personal data. Restrict output permissions and avoid writing captures to shared locations.

9. Or skip the browser setup

If your goal is a website image rather than the pixels currently displayed on your desktop, ScreenshotNeo captures a URL through one API request. The API accepts options for full-page shots, element selectors, device presets, retina scale, custom CSS and JavaScript, waits, headers, cookies, blocking, caching, PDFs, and more. See the ScreenshotNeo API documentation for the complete parameter list.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" \\
  -d access_key=YOUR_API_KEY \\
  --data-urlencode url=https://stripe.com \\
  -o shot.webp

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)

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}`);
require('fs').writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing status. ScreenshotNeo also provides an MCP server so Claude, Cursor, and other MCP clients can take screenshots, inspect pages, and capture PDFs. The Free plan includes 1,000 screenshots each month with no card, and paid plans start at $5 for 3,000 shots.

Create a free ScreenshotNeo account to try the API without a card.

10. FAQ

Can I capture a rectangle without opening a visible window?

Yes. A QGuiApplication plus QScreen.grabWindow is enough for a one-shot capture, provided the operating system permits screen access.

Why does the PNG have more pixels than my requested width?

Qt requests the rectangle in device-independent units. The returned pixmap can contain more physical pixels on a high-DPI display; use its device-pixel ratio to interpret the dimensions.

Does PyQt capture the mouse pointer?

Generally no. Qt’s QScreen documentation states that the mouse cursor is generally not grabbed.

How do I record the screen instead of taking one image?

Use the separate Qt Multimedia QScreenCapture path with a media capture session and its platform requirements.

Can one call capture an area spanning two monitors?

Do not assume that it can safely do so. Treat each display as its own QScreen, split the rectangle, and combine the resulting images if a cross-monitor composite is required.

Primary references: Qt QScreen documentation and QScreen::grabWindow.