Skip to content
Open
Show file tree
Hide file tree
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
61 changes: 61 additions & 0 deletions win/dpi_aware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Per-Monitor DPI Awareness V2 bootstrap for the OpenSpan Windows side.

Must run in EVERY OpenSpan process (control GUI, setup window, input portal)
before any Tk window or Win32 monitor/hook API is touched. The portal's edge
geometry, the monitor rectangles it / the GUI enumerate (written to
openspan_config.json) and the low-level mouse-hook coordinates all have to
live in ONE physical-pixel coordinate space. On this machine that means the
three real sizes 1600x2560 / 1920x1080 / 2560x1600 -- not the virtualized
logical sizes (800x1280 / 1536x864 / 1280x800) a DPI-unaware or mixed-DPI
process reports, which made the portal fire before the mouse reached the
real screen edge.

Pure standard library (ctypes), matching the rest of the repo.
"""

import ctypes


def _current():
"""Query the process's current DPI awareness via shcore. Returns a
label ('unaware'/'system'/'pm') or None when the API is unavailable."""
try:
val = ctypes.c_int(-1)
ctypes.windll.shcore.GetProcessDpiAwareness(
ctypes.c_void_p(0), ctypes.byref(val))
except (AttributeError, OSError):
return None
return {0: "unaware", 1: "system", 2: "pm"}.get(val.value)


def ensure():
"""Declare Per-Monitor DPI Awareness V2 if not already set.

Prefers SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_
AWARE_V2 = -4) (Windows 10 1703+). If the process is already aware --
a manifest or an earlier setter -- the call fails with
ERROR_ACCESS_DENIED and the existing awareness wins, which is fine
because _current() reports what is actually in force.

Fallbacks in order: shcore SetProcessDpiAwareness(PROCESS_PER_MONITOR_
DPI_AWARE), user32 SetProcessDPIAware (system-aware). Returns the label
in force afterwards.
"""
try:
ctx = ctypes.c_void_p(-4)
if ctypes.windll.user32.SetProcessDpiAwarenessContext(ctx):
return _current() or "v2"
except (AttributeError, OSError):
pass
try:
if ctypes.windll.shcore.SetProcessDpiAwareness(2) == 0:
return _current() or "pm"
except (AttributeError, OSError):
pass
try:
if ctypes.windll.user32.SetProcessDPIAware():
return "system"
except (AttributeError, OSError):
pass
return _current() or "none"
2 changes: 2 additions & 0 deletions win/openspan_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ def _traytest():

if __name__ == "__main__":
_ensure_std_streams()
import dpi_aware # noqa: E402
dpi_aware.ensure() # physical-pixel space for every role (see dpi_aware.py)
arg = sys.argv[1] if len(sys.argv) > 1 else ""
if arg == "--portal":
del sys.argv[1]
Expand Down
7 changes: 7 additions & 0 deletions win/openspan_portal.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
import time
import queue

# Physical-pixel coordinate space BEFORE any Win32 monitor / hook call:
# portal edges (openspan_config.json), hook coords (ms.pt) and monitor
# rectangles all have to be the SAME space, or the portal fires early on
# mixed-DPI desktops.
import dpi_aware # noqa: E402
dpi_aware.ensure() # noqa: E402

user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32

Expand Down
5 changes: 5 additions & 0 deletions win/openspan_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
import tkinter as tk
from tkinter import ttk, messagebox

# Same physical-pixel space for the GUI, the setup window and the portal:
# must be declared before any Tk window / GetMonitorInfo call.
import dpi_aware # noqa: E402
dpi_aware.ensure() # noqa: E402

if getattr(sys, "frozen", False): # frozen exe: data lives at the exe
CONFIG_PATH = os.path.join(
os.path.dirname(os.path.abspath(sys.executable)),
Expand Down