Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 84 additions & 27 deletions screeninfo/enumerators/windows.py
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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
Expand All @@ -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