From 119c26a6bc2d9e306ca97dfa551031a28a71992e Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 13 Apr 2020 13:37:12 +0200 Subject: [PATCH 1/4] fix 'No enumerators available' - on Windows 10 Fixes #30 dc_full is a pointer, if interpretted as a signed number may be negative. Null -> 0 indicates failure. Set return types for foriegn function so return value can be compared to None. ReleaseDC was not being called with enough arguments, it should also take the same Window Handle (HWND) as GetDC. --- screeninfo/enumerators/windows.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/screeninfo/enumerators/windows.py b/screeninfo/enumerators/windows.py index 53685ba..458bcce 100644 --- a/screeninfo/enumerators/windows.py +++ b/screeninfo/enumerators/windows.py @@ -64,28 +64,38 @@ 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) + # Type of HWND resolves to void * + # HWND -> HANDLE -> PVOID -> void * + # See https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types + hwnd = ctypes.c_void_p(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 (type: HDC) + # HDC eventually resolves to void * + # HDC -> HANDLE -> PVOID -> void * + # See https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types + GetDC = ctypes.windll.user32.GetDC + GetDC.restype = ctypes.c_void_p + dc_full = GetDC(hwnd) + if dc_full is not None: # Got a valid DC, break. break - ctypes.windll.user32.ReleaseDC(dc_full) + ctypes.windll.user32.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 = ctypes.c_void_p(None) # 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 ) - ctypes.windll.user32.ReleaseDC(dc_full) + ctypes.windll.user32.ReleaseDC(hwnd, dc_full) yield from monitors From c93f9f214230498c48926e73b524e2a87cc65766 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 15 Apr 2020 20:55:05 +0200 Subject: [PATCH 2/4] Add typing for all ctypes calls --- screeninfo/enumerators/windows.py | 105 ++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 34 deletions(-) diff --git a/screeninfo/enumerators/windows.py b/screeninfo/enumerators/windows.py index 458bcce..6702f23 100644 --- a/screeninfo/enumerators/windows.py +++ b/screeninfo/enumerators/windows.py @@ -1,46 +1,85 @@ + 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 + ) + + # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors + EnumDisplayMonitors = ctypes.windll.user32.EnumDisplayMonitors + EnumDisplayMonitors.restype = BOOL + EnumDisplayMonitors.argtype = (HDC, LPRECT, MonitorEnumProc, LPARAM) + monitors = [] def callback(monitor: T.Any, dc: T.Any, rect: T.Any, data: T.Any) -> int: info = MONITORINFOEXW() info.cbSize = ctypes.sizeof(MONITORINFOEXW) - if ctypes.windll.user32.GetMonitorInfoW(monitor, ctypes.byref(info)): + if GetMonitorInfoW(monitor, ctypes.byref(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( @@ -64,38 +103,36 @@ 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) - # Type of HWND resolves to void * - # HWND -> HANDLE -> PVOID -> void * - # See https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types - hwnd = ctypes.c_void_p(None) + hwnd = HDC(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. - # On failure, GetDC returns NULL (type: HDC) - # HDC eventually resolves to void * - # HDC -> HANDLE -> PVOID -> void * - # See https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types - GetDC = ctypes.windll.user32.GetDC - GetDC.restype = ctypes.c_void_p - dc_full = GetDC(hwnd) + # 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(hwnd, 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 = ctypes.c_void_p(None) + dc_full = HDC(None) # 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, + 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. + MonitorEnumProc(callback), + 0 ) - ctypes.windll.user32.ReleaseDC(hwnd, dc_full) + ReleaseDC(hwnd, dc_full) yield from monitors From 52cf127503e43d89da2f07f0ba644189de2fe06e Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 15 Apr 2020 21:12:30 +0200 Subject: [PATCH 3/4] Add explicit types for EnumDisplayMonitors and correct hwnd init --- screeninfo/enumerators/windows.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/screeninfo/enumerators/windows.py b/screeninfo/enumerators/windows.py index 6702f23..450d80a 100644 --- a/screeninfo/enumerators/windows.py +++ b/screeninfo/enumerators/windows.py @@ -103,7 +103,7 @@ 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 = HDC(None) + 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): @@ -124,14 +124,15 @@ def callback(monitor: T.Any, dc: T.Any, rect: T.Any, data: T.Any) -> int: # 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. + EnumDisplayMonitors( dc_full, - None, + LPRECT(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. MonitorEnumProc(callback), - 0 + LPARAM(0) ) ReleaseDC(hwnd, dc_full) From 03e2082740114b6f4f5b0e840c1e956330258a6f Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Sat, 18 Apr 2020 18:55:58 +0200 Subject: [PATCH 4/4] Keep instance of MonitorEnumProc Adds more typing. --- screeninfo/enumerators/windows.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/screeninfo/enumerators/windows.py b/screeninfo/enumerators/windows.py index 450d80a..caebb50 100644 --- a/screeninfo/enumerators/windows.py +++ b/screeninfo/enumerators/windows.py @@ -63,17 +63,18 @@ class MONITORINFOEXW(ctypes.Structure): 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, LPRECT, MonitorEnumProc, LPARAM) + 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 GetMonitorInfoW(monitor, ctypes.byref(info)): + if GetMonitorInfoW(monitor, ctypes.pointer(info)): name = info.szDevice else: name = None @@ -93,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 @@ -121,17 +122,25 @@ def callback(monitor: T.Any, dc: T.Any, rect: T.Any, data: T.Any) -> int: # passed to the callback which means that physical # sizes can't be read. 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. - EnumDisplayMonitors( dc_full, - LPRECT(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. - MonitorEnumProc(callback), + LPCRECT(), + monEnumProc, LPARAM(0) ) ReleaseDC(hwnd, dc_full)