diff --git a/screeninfo/enumerators/windows.py b/screeninfo/enumerators/windows.py index 53685ba..caebb50 100644 --- a/screeninfo/enumerators/windows.py +++ b/screeninfo/enumerators/windows.py @@ -1,46 +1,86 @@ + import typing as T +import ctypes +import ctypes.wintypes +from ctypes.wintypes import ( + HMONITOR, + HDC, + LPARAM, + LPRECT, + WCHAR, + DWORD, + RECT, + BOOL, HWND, +) from screeninfo.common import Monitor def enumerate_monitors() -> T.Iterable[Monitor]: - import ctypes - import ctypes.wintypes CCHDEVICENAME = 32 # gdi32.GetDeviceCaps keys for monitor size in mm HORZSIZE = 4 VERTSIZE = 6 - MonitorEnumProc = ctypes.WINFUNCTYPE( - ctypes.c_int, - ctypes.c_ulong, - ctypes.c_ulong, - ctypes.POINTER(ctypes.wintypes.RECT), - ctypes.c_double, - ) - + # https://docs.microsoft.com/en-gb/windows/win32/api/winuser/ns-winuser-monitorinfoexw class MONITORINFOEXW(ctypes.Structure): _fields_ = [ - ("cbSize", ctypes.wintypes.DWORD), - ("rcMonitor", ctypes.wintypes.RECT), - ("rcWork", ctypes.wintypes.RECT), - ("dwFlags", ctypes.wintypes.DWORD), - ("szDevice", ctypes.wintypes.WCHAR * CCHDEVICENAME), + ("cbSize", DWORD), + ("rcMonitor", RECT), + ("rcWork", RECT), + ("dwFlags", DWORD), + ("szDevice", WCHAR * CCHDEVICENAME), ] + LPMONITORINFO = ctypes.POINTER(MONITORINFOEXW) + # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmonitorinfow + GetMonitorInfoW = ctypes.windll.user32.GetMonitorInfoW + GetMonitorInfoW.restype = BOOL + GetMonitorInfoW.argtypes = (HMONITOR, LPMONITORINFO) + + # https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-getdevicecaps + GetDeviceCaps = ctypes.windll.gdi32.GetDeviceCaps + GetDeviceCaps.restype = ctypes.c_int + GetDeviceCaps.argtype = (HDC, ctypes.c_int) + + # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdc + GetDC = ctypes.windll.user32.GetDC + GetDC.restype = HDC + GetDC.argtype = (HWND, ) + + # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-releasedc + ReleaseDC = ctypes.windll.user32.ReleaseDC + ReleaseDC.restype = ctypes.c_int + ReleaseDC.argtype = (HWND, HDC) + + # https://docs.microsoft.com/en-gb/windows/win32/api/winuser/nc-winuser-monitorenumproc + MonitorEnumProc = ctypes.WINFUNCTYPE( + BOOL, # resType + HMONITOR, # monitor + HDC, # dc + LPRECT, # rect + LPARAM, # data + ) + + LPCRECT = ctypes.POINTER(RECT) + # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors + EnumDisplayMonitors = ctypes.windll.user32.EnumDisplayMonitors + EnumDisplayMonitors.restype = BOOL + EnumDisplayMonitors.argtype = (HDC, LPCRECT, MonitorEnumProc, LPARAM) + monitors = [] - def callback(monitor: T.Any, dc: T.Any, rect: T.Any, data: T.Any) -> int: + def callback(monitor: int, dc: int, rect: LPRECT, data: int) -> bool: info = MONITORINFOEXW() info.cbSize = ctypes.sizeof(MONITORINFOEXW) - if ctypes.windll.user32.GetMonitorInfoW(monitor, ctypes.byref(info)): + if GetMonitorInfoW(monitor, ctypes.pointer(info)): name = info.szDevice else: name = None - h_size = ctypes.windll.gdi32.GetDeviceCaps(dc, HORZSIZE) - v_size = ctypes.windll.gdi32.GetDeviceCaps(dc, VERTSIZE) + h_size = GetDeviceCaps(dc, HORZSIZE) + v_size = GetDeviceCaps(dc, VERTSIZE) rct = rect.contents monitors.append( @@ -54,7 +94,7 @@ def callback(monitor: T.Any, dc: T.Any, rect: T.Any, data: T.Any) -> int: name=name, ) ) - return 1 + return True # Make the process DPI aware so it will detect the actual # resolution and not a virtualized resolution reported by @@ -64,28 +104,45 @@ def callback(monitor: T.Any, dc: T.Any, rect: T.Any, data: T.Any) -> int: # multiple monitors have different DPIs. ctypes.windll.shcore.SetProcessDpiAwareness(2) + hwnd = HWND(None) # On Python 3.8.X GetDC randomly fails returning an invalid DC. # To workaround this request a number of DCs until a valid DC is returned. for retry in range(100): # Create a Device Context for the full virtual desktop. - dc_full = ctypes.windll.user32.GetDC(None) - if dc_full > 0: + # On failure, GetDC returns NULL + dc_full: HDC = GetDC(hwnd) + if dc_full is not None: # Got a valid DC, break. break - ctypes.windll.user32.ReleaseDC(dc_full) + ReleaseDC(hwnd, dc_full) else: # Fallback to device context 0 that is the whole # desktop. This allows fetching resolutions # but monitor specific device contexts are not # passed to the callback which means that physical # sizes can't be read. - dc_full = 0 + dc_full = HDC(None) + + # Make sure you keep references to CFUNCTYPE() or WINFUNCTYPE() objects as long as they are + # used from C code. ctypes doesn’t, and if you don’t, they may be garbage collected, + # crashing your program when a callback is made. + monEnumProc = MonitorEnumProc(callback) + monEnumProc.restype = BOOL + monEnumProc.argtypes = [ + HMONITOR, # monitor + HDC, # dc + LPRECT, # rect + LPARAM, # data + ] # Call EnumDisplayMonitors with the non-NULL DC # so that non-NULL DCs are passed onto the callback. # We want monitor specific DCs in the callback. - ctypes.windll.user32.EnumDisplayMonitors( - dc_full, None, MonitorEnumProc(callback), 0 + EnumDisplayMonitors( + dc_full, + LPCRECT(), + monEnumProc, + LPARAM(0) ) - ctypes.windll.user32.ReleaseDC(dc_full) + ReleaseDC(hwnd, dc_full) yield from monitors