Skip to content

Commit 98e74fd

Browse files
authored
Merge pull request #8516 from radarhere/imagegrab
Allow HWND to be passed to ImageGrab.grab() on Windows
2 parents 869aa58 + 25af4f1 commit 98e74fd

File tree

5 files changed

+79
-11
lines changed

5 files changed

+79
-11
lines changed

Tests/test_imagegrab.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ def test_grab_invalid_xdisplay(self) -> None:
5757
ImageGrab.grab(xdisplay="error.test:0.0")
5858
assert str(e.value).startswith("X connection failed")
5959

60+
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
61+
def test_grab_invalid_handle(self) -> None:
62+
with pytest.raises(OSError, match="unable to get device context for handle"):
63+
ImageGrab.grab(window=-1)
64+
with pytest.raises(OSError, match="screen grab failed"):
65+
ImageGrab.grab(window=0)
66+
6067
def test_grabclipboard(self) -> None:
6168
if sys.platform == "darwin":
6269
subprocess.call(["screencapture", "-cx"])

docs/reference/ImageGrab.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ or the clipboard to a PIL image memory.
99

1010
.. versionadded:: 1.1.3
1111

12-
.. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None)
12+
.. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None, window=None)
1313
1414
Take a snapshot of the screen. The pixels inside the bounding box are returned as
1515
an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted,
@@ -39,6 +39,11 @@ or the clipboard to a PIL image memory.
3939
You can check X11 support using :py:func:`PIL.features.check_feature` with ``feature="xcb"``.
4040

4141
.. versionadded:: 7.1.0
42+
43+
:param window:
44+
HWND, to capture a single window. Windows only.
45+
46+
.. versionadded:: 11.2.0
4247
:return: An image
4348

4449
.. py:function:: grabclipboard()

docs/releasenotes/11.2.0.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`::
5151
draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify")
5252
draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify")
5353

54+
Specify window in ImageGrab on Windows
55+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
56+
57+
When using :py:meth:`~PIL.ImageGrab.grab`, a specific window can be selected using the
58+
HWND::
59+
60+
from PIL import ImageGrab
61+
ImageGrab.grab(window=hwnd)
62+
5463
Check for MozJPEG
5564
^^^^^^^^^^^^^^^^^
5665

src/PIL/ImageGrab.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,17 @@
2525

2626
from . import Image
2727

28+
TYPE_CHECKING = False
29+
if TYPE_CHECKING:
30+
from . import ImageWin
31+
2832

2933
def grab(
3034
bbox: tuple[int, int, int, int] | None = None,
3135
include_layered_windows: bool = False,
3236
all_screens: bool = False,
3337
xdisplay: str | None = None,
38+
window: int | ImageWin.HWND | None = None,
3439
) -> Image.Image:
3540
im: Image.Image
3641
if xdisplay is None:
@@ -51,8 +56,12 @@ def grab(
5156
return im_resized
5257
return im
5358
elif sys.platform == "win32":
59+
if window is not None:
60+
all_screens = -1
5461
offset, size, data = Image.core.grabscreen_win32(
55-
include_layered_windows, all_screens
62+
include_layered_windows,
63+
all_screens,
64+
int(window) if window is not None else 0,
5665
)
5766
im = Image.frombytes(
5867
"RGB",

src/display.c

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -286,29 +286,42 @@ PyImaging_DisplayModeWin32(PyObject *self, PyObject *args) {
286286
/* -------------------------------------------------------------------- */
287287
/* Windows screen grabber */
288288

289+
typedef HANDLE(__stdcall *Func_GetWindowDpiAwarenessContext)(HANDLE);
289290
typedef HANDLE(__stdcall *Func_SetThreadDpiAwarenessContext)(HANDLE);
290291

291292
PyObject *
292293
PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) {
293-
int x = 0, y = 0, width, height;
294-
int includeLayeredWindows = 0, all_screens = 0;
294+
int x = 0, y = 0, width = -1, height;
295+
int includeLayeredWindows = 0, screens = 0;
295296
HBITMAP bitmap;
296297
BITMAPCOREHEADER core;
297298
HDC screen, screen_copy;
299+
HWND wnd;
298300
DWORD rop;
299301
PyObject *buffer;
300-
HANDLE dpiAwareness;
302+
HANDLE dpiAwareness = NULL;
301303
HMODULE user32;
304+
Func_GetWindowDpiAwarenessContext GetWindowDpiAwarenessContext_function;
302305
Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function;
303306

304-
if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) {
307+
if (!PyArg_ParseTuple(
308+
args, "|ii" F_HANDLE, &includeLayeredWindows, &screens, &wnd
309+
)) {
305310
return NULL;
306311
}
307312

308313
/* step 1: create a memory DC large enough to hold the
309314
entire screen */
310315

311-
screen = CreateDC("DISPLAY", NULL, NULL, NULL);
316+
if (screens == -1) {
317+
screen = GetDC(wnd);
318+
if (screen == NULL) {
319+
PyErr_SetString(PyExc_OSError, "unable to get device context for handle");
320+
return NULL;
321+
}
322+
} else {
323+
screen = CreateDC("DISPLAY", NULL, NULL, NULL);
324+
}
312325
screen_copy = CreateCompatibleDC(screen);
313326

314327
// added in Windows 10 (1607)
@@ -317,15 +330,28 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) {
317330
SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext
318331
)GetProcAddress(user32, "SetThreadDpiAwarenessContext");
319332
if (SetThreadDpiAwarenessContext_function != NULL) {
333+
GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext
334+
)GetProcAddress(user32, "GetWindowDpiAwarenessContext");
335+
if (screens == -1 && GetWindowDpiAwarenessContext_function != NULL) {
336+
dpiAwareness = GetWindowDpiAwarenessContext_function(wnd);
337+
}
320338
// DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3)
321-
dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3);
339+
dpiAwareness = SetThreadDpiAwarenessContext_function(
340+
dpiAwareness == NULL ? (HANDLE)-3 : dpiAwareness
341+
);
322342
}
323343

324-
if (all_screens) {
344+
if (screens == 1) {
325345
x = GetSystemMetrics(SM_XVIRTUALSCREEN);
326346
y = GetSystemMetrics(SM_YVIRTUALSCREEN);
327347
width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
328348
height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
349+
} else if (screens == -1) {
350+
RECT rect;
351+
if (GetClientRect(wnd, &rect)) {
352+
width = rect.right;
353+
height = rect.bottom;
354+
}
329355
} else {
330356
width = GetDeviceCaps(screen, HORZRES);
331357
height = GetDeviceCaps(screen, VERTRES);
@@ -337,6 +363,10 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) {
337363

338364
FreeLibrary(user32);
339365

366+
if (width == -1) {
367+
goto error;
368+
}
369+
340370
bitmap = CreateCompatibleBitmap(screen, width, height);
341371
if (!bitmap) {
342372
goto error;
@@ -382,15 +412,23 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) {
382412

383413
DeleteObject(bitmap);
384414
DeleteDC(screen_copy);
385-
DeleteDC(screen);
415+
if (screens == -1) {
416+
ReleaseDC(wnd, screen);
417+
} else {
418+
DeleteDC(screen);
419+
}
386420

387421
return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer);
388422

389423
error:
390424
PyErr_SetString(PyExc_OSError, "screen grab failed");
391425

392426
DeleteDC(screen_copy);
393-
DeleteDC(screen);
427+
if (screens == -1) {
428+
ReleaseDC(wnd, screen);
429+
} else {
430+
DeleteDC(screen);
431+
}
394432

395433
return NULL;
396434
}

0 commit comments

Comments
 (0)