Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Importing pyautogui breaks monitor DPI detection with ctypes #663

Open
GeorgeWashingtonABCDEFG opened this issue Jan 31, 2022 · 7 comments

Comments

@GeorgeWashingtonABCDEFG
Copy link

GeorgeWashingtonABCDEFG commented Jan 31, 2022

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.
@bersbersbers
Copy link

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.

@bersbersbers
Copy link

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()

@bersbersbers
Copy link

bersbersbers commented Oct 31, 2022

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()

@bersbersbers
Copy link

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).

@bersbersbers
Copy link

Related: 9cd04d3

Avasam added a commit to Toufool/AutoSplit that referenced this issue Jun 10, 2023
Avasam added a commit to Toufool/AutoSplit that referenced this issue Jun 11, 2023
Avasam added a commit to Toufool/AutoSplit that referenced this issue Jun 23, 2023
@Avasam
Copy link

Avasam commented Oct 20, 2023

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()

The unittest workaround significantly increases build time, boot time and build size with PyInstaller. So I went with some sort of postinstall powershell script that patches all offending libraries, and uninstalls those I don't actually need.

# Prevent pyautogui and pywinctl from setting Process DPI Awareness, which Qt tries to do then throws warnings about it.
# The unittest workaround significantly increases build time, boot time and build size with PyInstaller.
# https://github.com/asweigart/pyautogui/issues/663#issuecomment-1296719464
$libPath = python -c 'import pyautogui as _; print(_.__path__[0])'
(Get-Content "$libPath/_pyautogui_win.py").replace('ctypes.windll.user32.SetProcessDPIAware()', 'pass') |
  Set-Content "$libPath/_pyautogui_win.py"
$libPath = python -c 'import pymonctl as _; print(_.__path__[0])'
(Get-Content "$libPath/_pymonctl_win.py").replace('ctypes.windll.shcore.SetProcessDpiAwareness', 'pass # ') |
  Set-Content "$libPath/_pymonctl_win.py"
$libPath = python -c 'import pywinbox as _; print(_.__path__[0])'
(Get-Content "$libPath/_pywinbox_win.py").replace('ctypes.windll.shcore.SetProcessDpiAwareness', 'pass # ') |
  Set-Content "$libPath/_pywinbox_win.py"
# Uninstall optional dependencies that PyInstaller picks up
python -m pip uninstall pyscreeze mouseinfo pyperclip -y

To find where it's called in the first place, I simply added the following at the top of my main/entrypoint module:

# flake8: noqa
def find_culprit(*_):
    raise Exception
import ctypes
ctypes.windll.shcore.SetProcessDpiAwareness = find_culprit
ctypes.windll.user32.SetProcessDPIAware = find_culprit

@Avasam
Copy link

Avasam commented Jun 16, 2024

I think I found an even better workaround. This is what I'm using now. If you did need SetProcessDPIAware or SetProcessDpiAwareness yourself, just assign it to a variable first.

import sys

# Prevent PyAutoGUI and pywinctl from setting Process DPI Awareness, which Qt tries to do then throws warnings about it.
# The unittest workaround significantly increases build time, boot time and build size with PyInstaller.
# https://github.com/asweigart/pyautogui/issues/663#issuecomment-1296719464
# QT doesn't call those from Python/ctypes, meaning we can stop other programs from setting it.
if sys.platform == "win32":
    import ctypes
    # pyautogui._pyautogui_win.py
    ctypes.windll.user32.SetProcessDPIAware = lambda: None  # pyright: ignore[reportAttributeAccessIssue]
    # pymonctl._pymonctl_win.py
    # pywinbox._pywinbox_win.py
    ctypes.windll.shcore.SetProcessDpiAwareness = (  # pyright: ignore[reportAttributeAccessIssue]
        lambda _: None  # pyright: ignore[reportUnknownLambdaType]
    )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants