Files
kefu/wechat_rpa/.venv/Lib/site-packages/mss/windows/gdi.py
T
2026-09-21 10:34:06 +08:00

393 lines
14 KiB
Python

"""GDI-based backend for MSS on Microsoft Windows.
Uses user32/gdi32 APIs to capture the desktop and enumerate monitors.
This implementation uses CreateDIBSection for direct memory access to pixel data.
"""
from __future__ import annotations
import ctypes
import sys
from ctypes import POINTER, WINFUNCTYPE, Structure, WinError, _Pointer
from ctypes.wintypes import (
BOOL,
BYTE,
DWORD,
HANDLE,
HBITMAP,
HDC,
HGDIOBJ,
HMONITOR,
HWND,
INT,
LONG,
LPARAM,
LPRECT,
LPVOID,
RECT,
UINT,
WORD,
)
from typing import TYPE_CHECKING
from mss.base import MSSImplementation
from mss.exception import ScreenShotError
if TYPE_CHECKING:
from typing import Any, Callable
from mss.models import CFunctionsErrChecked, Monitor, Monitors
__all__ = ()
LPCRECT = POINTER(RECT) # Actually a const pointer, but ctypes has no const.
CAPTUREBLT = 0x40000000
DIB_RGB_COLORS = 0
SRCCOPY = 0x00CC0020
CCHDEVICENAME = 32
MONITORINFOF_PRIMARY = 0x01
EDD_GET_DEVICE_INTERFACE_NAME = 0x00000001
class BITMAPINFOHEADER(Structure):
"""Information about the dimensions and color format of a DIB."""
_fields_ = (
("biSize", DWORD),
("biWidth", LONG),
("biHeight", LONG),
("biPlanes", WORD),
("biBitCount", WORD),
("biCompression", DWORD),
("biSizeImage", DWORD),
("biXPelsPerMeter", LONG),
("biYPelsPerMeter", LONG),
("biClrUsed", DWORD),
("biClrImportant", DWORD),
)
class BITMAPINFO(Structure):
"""Structure that defines the dimensions and color information for a DIB."""
# The bmiColors entry is variable length, but it's unused the way we do things. We declare it to be four bytes,
# which is how it's declared in C.
_fields_ = (("bmiHeader", BITMAPINFOHEADER), ("bmiColors", BYTE * 4))
class MONITORINFOEXW(Structure):
"""Extended monitor information structure.
https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-monitorinfoexw
"""
_fields_ = (
("cbSize", DWORD),
("rcMonitor", RECT),
("rcWork", RECT),
("dwFlags", DWORD),
("szDevice", WORD * CCHDEVICENAME),
)
class DISPLAY_DEVICEW(Structure): # noqa: N801
"""Display device information structure.
https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-display_devicew
"""
_fields_ = (
("cb", DWORD),
("DeviceName", WORD * 32),
("DeviceString", WORD * 128),
("StateFlags", DWORD),
("DeviceID", WORD * 128),
("DeviceKey", WORD * 128),
)
MONITORNUMPROC = WINFUNCTYPE(BOOL, HMONITOR, HDC, POINTER(RECT), LPARAM)
def _errcheck(result: int | _Pointer, func: Callable, arguments: tuple) -> tuple:
"""If the result is zero, raise an exception."""
if not result:
# Notably, the errno that is in winerror may not be relevant. Use the winerror and strerror attributes
# instead.
winerror = WinError()
details = {
"func": func.__name__,
"args": arguments,
"error_code": winerror.winerror,
"error_msg": winerror.strerror,
}
if winerror.winerror == 0:
# Some functions return NULL/0 on failure without setting last error. (Example: CreateDIBSection
# with an invalid HDC.)
msg = f"Windows graphics function failed (no error provided): {func.__name__}"
raise ScreenShotError(msg, details=details)
msg = f"Windows graphics function failed: {func.__name__}: {winerror.strerror}"
raise ScreenShotError(msg, details=details) from winerror
return arguments
# C functions that will be initialised later.
#
# Available attr: gdi32, user32.
#
# Note: keep it sorted by cfunction.
CFUNCTIONS: CFunctionsErrChecked = {
# Syntax: cfunction: (attr, argtypes, restype, errcheck)
"BitBlt": ("gdi32", [HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD], BOOL, _errcheck),
"CreateCompatibleDC": ("gdi32", [HDC], HDC, _errcheck),
# CreateDIBSection: ppvBits (4th param) receives a pointer to the DIB pixel data.
# hSection is NULL and offset is 0 to have the system allocate the memory.
"CreateDIBSection": ("gdi32", [HDC, POINTER(BITMAPINFO), UINT, POINTER(LPVOID), HANDLE, DWORD], HBITMAP, _errcheck),
"DeleteDC": ("gdi32", [HDC], HDC, _errcheck),
"DeleteObject": ("gdi32", [HGDIOBJ], BOOL, _errcheck),
"EnumDisplayDevicesW": ("user32", [POINTER(WORD), DWORD, POINTER(DISPLAY_DEVICEW), DWORD], BOOL, None),
"EnumDisplayMonitors": ("user32", [HDC, LPCRECT, MONITORNUMPROC, LPARAM], BOOL, _errcheck),
# GdiFlush flushes the calling thread's current batch of GDI operations.
# This ensures DIB memory is fully updated before reading.
"GdiFlush": ("gdi32", [], BOOL, None),
# While GetSystemMetrics will return 0 if the parameter is invalid, it will also sometimes return 0 if the
# parameter is valid but the value is actually 0 (e.g., SM_CLEANBOOT on a normal boot). Thus, we do not attach an
# errcheck function here.
"GetSystemMetrics": ("user32", [INT], INT, None),
"GetMonitorInfoW": ("user32", [HMONITOR, POINTER(MONITORINFOEXW)], BOOL, _errcheck),
"GetWindowDC": ("user32", [HWND], HDC, _errcheck),
"ReleaseDC": ("user32", [HWND, HDC], INT, _errcheck),
# SelectObject returns NULL on error the way we call it. If it's called to select a region, it returns HGDI_ERROR
# on error.
"SelectObject": ("gdi32", [HDC, HGDIOBJ], HGDIOBJ, _errcheck),
}
class MSSImplGdi(MSSImplementation):
"""Multiple ScreenShots implementation for Microsoft Windows (GDI backend).
This implementation uses CreateDIBSection for direct memory access to pixel data,
which eliminates the need for GetDIBits. The DIB pixel data is written directly
to system-managed memory that we can read from.
This backend is selected by ``backend="default"`` and has no Windows-specific
constructor parameters.
.. seealso::
:py:class:`mss.MSS`
Lists constructor parameters.
"""
__slots__ = {
"_bmi",
"_dib",
"_dib_array",
"_dib_bits",
"_memdc",
"_region_width_height",
"_srcdc",
"gdi32",
"user32",
}
def __init__(self) -> None:
super().__init__()
# user32 and gdi32 should not be changed after initialization.
self.user32 = ctypes.WinDLL("user32", use_last_error=True)
self.gdi32 = ctypes.WinDLL("gdi32", use_last_error=True)
self._set_cfunctions()
self._set_dpi_awareness()
# Available instance-specific variables
self._region_width_height: tuple[int, int] | None = None
self._dib: HBITMAP | None = None
self._dib_bits: LPVOID = LPVOID() # Pointer to DIB pixel data
self._dib_array: ctypes.Array[ctypes.c_char] | None = None # Cached array view of DIB memory
self._srcdc = self.user32.GetWindowDC(0)
self._memdc = self.gdi32.CreateCompatibleDC(self._srcdc)
bmi = BITMAPINFO()
bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
# biWidth and biHeight are set in grab().
bmi.bmiHeader.biPlanes = 1 # Always 1
bmi.bmiHeader.biBitCount = 32 # 32-bit RGBX
bmi.bmiHeader.biCompression = 0 # 0 = BI_RGB (no compression)
bmi.bmiHeader.biSizeImage = 0 # Windows infers the size
bmi.bmiHeader.biXPelsPerMeter = 0 # Unspecified
bmi.bmiHeader.biYPelsPerMeter = 0 # Unspecified
bmi.bmiHeader.biClrUsed = 0
bmi.bmiHeader.biClrImportant = 0
self._bmi = bmi
def close(self) -> None:
# Clean-up
if self._dib:
self.gdi32.DeleteObject(self._dib)
self._dib = None
if self._memdc:
self.gdi32.DeleteDC(self._memdc)
self._memdc = None
if self._srcdc:
self.user32.ReleaseDC(0, self._srcdc)
self._srcdc = None
def _set_cfunctions(self) -> None:
"""Set all ctypes functions and attach them to attributes."""
cfactory = self._cfactory
attrs = {
"gdi32": self.gdi32,
"user32": self.user32,
}
for func, (attr, argtypes, restype, errcheck) in CFUNCTIONS.items():
cfactory(attrs[attr], func, argtypes, restype, errcheck)
def _set_dpi_awareness(self) -> None:
"""Set DPI awareness to capture full screen on Hi-DPI monitors."""
version = sys.getwindowsversion()[:2]
if version >= (6, 3):
# Windows 8.1+
# Here 2 = PROCESS_PER_MONITOR_DPI_AWARE, which means:
# per monitor DPI aware. This app checks for the DPI when it is
# created and adjusts the scale factor whenever the DPI changes.
# These applications are not automatically scaled by the system.
ctypes.windll.shcore.SetProcessDpiAwareness(2)
elif (6, 0) <= version < (6, 3):
# Windows Vista, 7, 8, and Server 2012
self.user32.SetProcessDPIAware()
def monitors(self) -> Monitors:
int_ = int
user32 = self.user32
get_system_metrics = user32.GetSystemMetrics
monitors = []
# All monitors
monitors.append(
{
"left": int_(get_system_metrics(76)), # SM_XVIRTUALSCREEN
"top": int_(get_system_metrics(77)), # SM_YVIRTUALSCREEN
"width": int_(get_system_metrics(78)), # SM_CXVIRTUALSCREEN
"height": int_(get_system_metrics(79)), # SM_CYVIRTUALSCREEN
},
)
# Each monitor
@MONITORNUMPROC
def callback(hmonitor: HMONITOR, _data: HDC, rect: LPRECT, _dc: LPARAM) -> bool:
"""Callback for monitorenumproc() function, it will return
a RECT with appropriate values.
"""
# Get monitor info to check if it's the primary monitor and get device name
info = MONITORINFOEXW()
info.cbSize = ctypes.sizeof(MONITORINFOEXW)
user32.GetMonitorInfoW(hmonitor, ctypes.byref(info))
rct = rect.contents
left = int_(rct.left)
top = int_(rct.top)
# Check the dwFlags field for MONITORINFOF_PRIMARY
is_primary = bool(info.dwFlags & MONITORINFOF_PRIMARY)
display_device = DISPLAY_DEVICEW()
display_device.cb = ctypes.sizeof(DISPLAY_DEVICEW)
# EnumDisplayDevicesW can get friendly name (e.g. "Generic PnP Monitor")
device_string: str | None = None
if user32.EnumDisplayDevicesW(
ctypes.cast(ctypes.addressof(info.szDevice), POINTER(WORD)),
0,
ctypes.byref(display_device),
0,
):
device_string = ctypes.wstring_at(ctypes.addressof(display_device.DeviceString))
# Get device interface name (stable per-physical-monitor ID) when supported
unique_id: str | None = None
if user32.EnumDisplayDevicesW(
ctypes.cast(ctypes.addressof(info.szDevice), POINTER(WORD)),
0,
ctypes.byref(display_device),
EDD_GET_DEVICE_INTERFACE_NAME,
):
unique_id = ctypes.wstring_at(ctypes.addressof(display_device.DeviceID))
mon_dict: dict[str, Any] = {
"left": left,
"top": top,
"width": int_(rct.right) - left,
"height": int_(rct.bottom) - top,
"is_primary": is_primary,
}
if device_string is not None:
mon_dict["name"] = device_string
if unique_id is not None:
mon_dict["unique_id"] = unique_id
monitors.append(mon_dict)
return True
user32.EnumDisplayMonitors(0, None, callback, 0)
return monitors
def grab(self, monitor: Monitor, /) -> bytearray:
"""Retrieve all pixels from a monitor using CreateDIBSection.
CreateDIBSection creates a DIB with system-managed memory backing,
allowing BitBlt to write directly to memory we can read. This eliminates
the need for a separate GetDIBits call.
Note on biHeight: A bottom-up DIB is specified by setting the height to a
positive number, while a top-down DIB is specified by setting the height
to a negative number. We use negative height for top-down orientation.
https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createdibsection
"""
srcdc, memdc = self._srcdc, self._memdc
gdi = self.gdi32
width, height = monitor["width"], monitor["height"]
if self._region_width_height != (width, height):
self._region_width_height = (width, height)
self._bmi.bmiHeader.biWidth = width
self._bmi.bmiHeader.biHeight = -height # Negative for top-down DIB
if self._dib:
gdi.DeleteObject(self._dib)
self._dib = None
# CreateDIBSection creates the DIB and returns a pointer to the pixel data
self._dib_bits = LPVOID()
self._dib = gdi.CreateDIBSection(
memdc,
self._bmi,
DIB_RGB_COLORS,
ctypes.byref(self._dib_bits),
None, # hSection = NULL (system allocates memory)
0, # offset = 0
)
gdi.SelectObject(memdc, self._dib)
# Create a ctypes array type that maps directly to the DIB memory.
# This avoids the overhead of ctypes.string_at() creating an intermediate bytes object.
size = width * height * 4
array_type = ctypes.c_char * size
self._dib_array = ctypes.cast(self._dib_bits, POINTER(array_type)).contents
# BitBlt copies screen content directly into the DIB's memory
gdi.BitBlt(memdc, 0, 0, width, height, srcdc, monitor["left"], monitor["top"], SRCCOPY | CAPTUREBLT)
# Flush GDI operations to ensure DIB memory is fully updated before reading.
# This ensures the BitBlt has completed before we access the memory.
gdi.GdiFlush()
# Read directly from DIB memory via the cached array view
assert self._dib_array is not None # noqa: S101 for type checker
return bytearray(self._dib_array)
def cursor(self) -> None:
"""Retrieve all cursor data. Pixels have to be RGB."""
return