asweigart/pyautogui

Importing pyautogui breaks monitor DPI detection with ctypes

GeorgeWashingtonABCDEFG opened this issue · 5 comments

import ctypes
# import pyautogui
dpix = ctypes.c_uint()
dpiy = ctypes.c_uint()
ctypes.windll.shcore.SetProcessDpiAwareness(2)
ctypes.windll.shcore.GetDpiForMonitor(1186359,0,ctypes.byref(dpix),ctypes.byref(dpiy))
print(dpix.value)

# output
144 # this is correct

import ctypes
import pyautogui
dpix = ctypes.c_uint()
dpiy = ctypes.c_uint()
ctypes.windll.shcore.SetProcessDpiAwareness(2)
ctypes.windll.shcore.GetDpiForMonitor(1186359,0,ctypes.byref(dpix),ctypes.byref(dpiy))
print(dpix.value)

# output
96 # this is wrong. The monitor's dpi is 144. Note the monitor is scaled in windows.
# Everything I do about detecting window size, location, etc is all broken by simply importing your module.

Here's something related:

"""
QtWarningMsg:
setProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) failed:
COM error 0x5  (Access is denied.)
"""
import pyautogui  # noqa # pylint:disable=unused-import
from PySide6.QtWidgets import QApplication

QApplication()

It outputs the warning

qt.qpa.windows: setProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) failed: COM error 0x5 (Access is denied.)

According to MS docs,

Possible errors are [..] ERROR_ACCESS_DENIED if the default API awareness mode for the process has already been set (via a previous API call or within the application manifest).

https://learn.microsoft.com/th-th/windows/win32/api/winuser/nf-winuser-setprocessdpiawarenesscontext

So it seems pyautogui sets DPI awareness somewhere before. I find it in

# Fixes the scaling issues where PyAutoGUI was reporting the wrong resolution:
try:
ctypes.windll.user32.SetProcessDPIAware()
except AttributeError:
pass

I am considering monkey-patching ctypes before importing pyautogui to remove that call.

Here's a workaround:

"""Workaround."""
import ctypes

import pytest
from PySide6.QtWidgets import QApplication

with pytest.MonkeyPatch.context() as mp:
    mp.setattr(ctypes.windll.user32, "SetProcessDPIAware", lambda: None)
    import pyautogui  # noqa # pylint:disable=unused-import

QApplication()

Much nicer workaround with the standard library:

"""Workaround."""
from unittest.mock import patch
from PySide6.QtWidgets import QApplication
with patch("ctypes.windll.user32.SetProcessDPIAware", autospec=True):
    import pyautogui  # noqa # pylint:disable=unused-import

QApplication()

Importantly, that Qt warning message will go away:
https://bugreports.qt.io/browse/PYSIDE-2105

This will solve my issue (and make my patch pointless), but it will not solve @GeorgeWashingtonABCDEFG's issue (for which my patch may still be useful).

In the end, pyautogui should probably stop calling SetProcessDPIAware unless necessary (and if so, call it as late as possible).

Related: 9cd04d3