Skip to content

Commit 607acbf

Browse files
committed
Allow window to be supplied for ImageGrab.grab() on Windows
1 parent 5771f0e commit 607acbf

File tree

4 files changed

+56
-8
lines changed

4 files changed

+56
-8
lines changed

Tests/test_imagegrab.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ 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):
63+
ImageGrab.grab(window=-1)
64+
6065
def test_grabclipboard(self) -> None:
6166
if sys.platform == "darwin":
6267
subprocess.call(["screencapture", "-cx"])

docs/reference/ImageGrab.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 handle:
44+
HWND, to capture a single window. Windows only.
45+
46+
.. versionadded:: 11.1.0
4247
:return: An image
4348

4449
.. py:function:: grabclipboard()

src/PIL/ImageGrab.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,20 @@
2222
import subprocess
2323
import sys
2424
import tempfile
25+
from typing import TYPE_CHECKING
2526

2627
from . import Image
2728

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: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -320,25 +320,36 @@ typedef HANDLE(__stdcall *Func_SetThreadDpiAwarenessContext)(HANDLE);
320320

321321
PyObject *
322322
PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) {
323-
int x = 0, y = 0, width, height;
324-
int includeLayeredWindows = 0, all_screens = 0;
323+
int x = 0, y = 0, width = -1, height;
324+
int includeLayeredWindows = 0, screens = 0;
325325
HBITMAP bitmap;
326326
BITMAPCOREHEADER core;
327327
HDC screen, screen_copy;
328+
HWND wnd;
328329
DWORD rop;
329330
PyObject *buffer;
330331
HANDLE dpiAwareness;
331332
HMODULE user32;
332333
Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function;
333334

334-
if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) {
335+
if (!PyArg_ParseTuple(
336+
args, "|ii" F_HANDLE, &includeLayeredWindows, &screens, &wnd
337+
)) {
335338
return NULL;
336339
}
337340

338341
/* step 1: create a memory DC large enough to hold the
339342
entire screen */
340343

341-
screen = CreateDC("DISPLAY", NULL, NULL, NULL);
344+
if (screens == -1) {
345+
screen = GetDC(wnd);
346+
if (screen == NULL) {
347+
PyErr_SetString(PyExc_OSError, "unable to get device context for handle");
348+
return NULL;
349+
}
350+
} else {
351+
screen = CreateDC("DISPLAY", NULL, NULL, NULL);
352+
}
342353
screen_copy = CreateCompatibleDC(screen);
343354

344355
// added in Windows 10 (1607)
@@ -351,11 +362,17 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) {
351362
dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3);
352363
}
353364

354-
if (all_screens) {
365+
if (screens == 1) {
355366
x = GetSystemMetrics(SM_XVIRTUALSCREEN);
356367
y = GetSystemMetrics(SM_YVIRTUALSCREEN);
357368
width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
358369
height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
370+
} else if (screens == -1) {
371+
RECT rect;
372+
if (GetClientRect(wnd, &rect)) {
373+
width = rect.right;
374+
height = rect.bottom;
375+
}
359376
} else {
360377
width = GetDeviceCaps(screen, HORZRES);
361378
height = GetDeviceCaps(screen, VERTRES);
@@ -367,6 +384,10 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) {
367384

368385
FreeLibrary(user32);
369386

387+
if (width == -1) {
388+
goto error;
389+
}
390+
370391
bitmap = CreateCompatibleBitmap(screen, width, height);
371392
if (!bitmap) {
372393
goto error;
@@ -412,15 +433,23 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) {
412433

413434
DeleteObject(bitmap);
414435
DeleteDC(screen_copy);
415-
DeleteDC(screen);
436+
if (screens == -1) {
437+
ReleaseDC(wnd, screen);
438+
} else {
439+
DeleteDC(screen);
440+
}
416441

417442
return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer);
418443

419444
error:
420445
PyErr_SetString(PyExc_OSError, "screen grab failed");
421446

422447
DeleteDC(screen_copy);
423-
DeleteDC(screen);
448+
if (screens == -1) {
449+
ReleaseDC(wnd, screen);
450+
} else {
451+
DeleteDC(screen);
452+
}
424453

425454
return NULL;
426455
}

0 commit comments

Comments
 (0)