Skip to content

Commit

Permalink
Use modern DPI awareness settings (#13254)
Browse files Browse the repository at this point in the history
Fixes #13370
Fixes #6722
Fixes #3875
Fixes #12070
Fixes #7083
Fixes #7915
Likely fixes #9531, otherwise close as stale/can't reproduce

### Summary of the issue:
When DPI for a monitor is not set to 100%, or when using multiple monitors with different DPI settings,
NVDA would:

- misplace highlight frames
- have inaccurate mouse tracking
- have inaccurate touch screen interaction

NVDA currently sets the DPI awareness via a Windows API call introduced in Windows Vista.
It is recommended to set DPI through the app manifest, rather than Windows API calls where possible.

Newer settings for DPI awareness have been introduced since Windows Vista.
Windows 8 introduced multiple monitor DPI awareness.
Windows 10 introduced a richer version of multiple monitor DPI awareness.

### Description of how this pull request fixes the issue:
Background docs: https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows#per-monitor-and-per-monitor-v2-dpi-awareness

When running as an executable, NVDA sets DPI awareness via the app manifest.
When running through source, NVDA sets DPI awareness via Windows API calls.
The most modern method available is used to set DPI awareness.

- For Windows 7, DPI awareness is unlikely to improve. There may be fixes from setting it via the app manifest instead of via Windows API calls.
- For Windows 8 and newer, NVDA has per monitor DPI awareness
- For Windows 10 1703 and newer, NVDA has [advanced per monitor DPI awareness](https://docs.microsoft.com/en-us/windows/win32/hidpi/dpi-awareness-context), including:
  - Child window DPI change notifications
  - Scaling of non-client area - All windows will automatically have their non-client area drawn in a DPI sensitive fashion. Calls to [EnableNonClientDpiScaling](https://docs.microsoft.com/en-us/windows/desktop/api/Winuser/nf-winuser-enablenonclientdpiscaling) are unnecessary.
  - Scaling of Win32 menus - All NTUSER menus created in Per Monitor v2 contexts will be scaling in a per-monitor fashion.
  - Dialog Scaling - Win32 dialogs created in Per Monitor v2 contexts will automatically respond to DPI changes.
  - Improved scaling of comctl32 controls - Various comctl32 controls have improved DPI scaling behavior in Per Monitor v2 contexts.
  - Improved theming behavior - UxTheme handles opened in the context of a Per Monitor v2 window will operate in terms of the DPI associated with that window.
  • Loading branch information
VeselovAlex authored Aug 30, 2022
1 parent 4721336 commit 28b4756
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 50 deletions.
6 changes: 4 additions & 2 deletions source/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,8 +428,10 @@ def main():
Finally, it starts the wx main loop.
"""
log.debug("Core starting")

ctypes.windll.user32.SetProcessDPIAware()
if NVDAState.isRunningAsSource():
# When running as packaged version, DPI awareness is set via the app manifest.
from winAPI.dpiAwareness import setDPIAwareness
setDPIAwareness()

import config
if not globalVars.appArgs.configPath:
Expand Down
50 changes: 50 additions & 0 deletions source/manifest.template.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly
xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"
xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"
>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel
level="asInvoker"
uiAccess="%(uiAccess)s"
/>
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 7 -->
<supportedOS
Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"
/>
<!-- Windows 8 -->
<supportedOS
Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"
/>
<!-- Windows 8.1 -->
<supportedOS
Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"
/>
<!-- Windows 10/11 -->
<supportedOS
Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"
/>
</application>
</compatibility>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware
xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"
>
true/pm
</dpiAware>
<dpiAwareness
xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings"
>
PerMonitorV2, PerMonitor
</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>
67 changes: 19 additions & 48 deletions source/setup.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,39 @@
# -*- coding: UTF-8 -*-
#setup.py
#A part of NonVisual Desktop Access (NVDA)
#Copyright (C) 2006-2018 NV Access Limited, Peter Vágner, Joseph Lee
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2022 NV Access Limited, Peter Vágner, Joseph Lee
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

import os
import sys
import copy
import gettext
gettext.install("nvda")
from setuptools import setup
import py2exe as py2exeModule
# While the import of py2exe appears unused it is required.
# py2exe monkey patches distutils when importing py2exe for the first time.
import py2exe as py2exeModule # noqa: F401, E402
from glob import glob
import fnmatch
# versionInfo names must be imported after Gettext
# Suppress E402 (module level import not at top of file)
from versionInfo import (
copyright as NVDAcopyright, # copyright is a reserved python keyword
description,
formatBuildVersionString,
name,
publisher,
url,
version,
publisher
) # noqa: E402
from versionInfo import *
from py2exe import distutils_buildexe
from py2exe.dllfinder import DllFinder
import wx
import importlib.machinery
# Explicitly put the nvda_dmp dir on the build path so the DMP library is included
sys.path.append(os.path.join("..", "include", "nvda_dmp"))
RT_MANIFEST = 24
manifest_template = """\
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel
level="asInvoker"
uiAccess="%(uiAccess)s"
/>
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 7 -->
<supportedOS
Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"
/>
<!-- Windows 8 -->
<supportedOS
Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"
/>
<!-- Windows 8.1 -->
<supportedOS
Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"
/>
<!-- Windows 10 -->
<supportedOS
Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"
/>
</application>
</compatibility>
</assembly>
"""
manifestTemplateFilePath = "manifest.template.xml"

# py2exe's idea of whether a dll is a system dll appears to be wrong sometimes, so monkey patch it.
orig_determine_dll_type = DllFinder.determine_dll_type
Expand Down Expand Up @@ -92,6 +61,8 @@ def initialize_options(self):
self.enable_uiAccess = False

def run(self):
with open(manifestTemplateFilePath, "r", encoding="utf-8") as manifestTemplateFile:
manifestTemplate = manifestTemplateFile.read()
dist = self.distribution
if self.enable_uiAccess:
# Add a target for nvda_uiAccess, using nvda_noUIAccess as a base.
Expand All @@ -108,7 +79,7 @@ def run(self):
(
RT_MANIFEST,
1,
(manifest_template % dict(uiAccess=target['uiAccess'])).encode("utf-8")
(manifestTemplate % dict(uiAccess=target['uiAccess'])).encode("utf-8")
),
]
super(py2exe, self).run()
Expand Down Expand Up @@ -167,7 +138,7 @@ def getRecursiveDataFiles(dest,source,excludes=()):
"description":"NVDA application",
"product_name":name,
"product_version":version,
"copyright":copyright,
"copyright": NVDAcopyright,
"company_name":publisher,
},
# The nvda_uiAccess target will be added at runtime if required.
Expand All @@ -180,7 +151,7 @@ def getRecursiveDataFiles(dest,source,excludes=()):
"description": name,
"product_name":name,
"product_version": version,
"copyright": copyright,
"copyright": NVDAcopyright,
"company_name": publisher,
},
{
Expand All @@ -193,7 +164,7 @@ def getRecursiveDataFiles(dest,source,excludes=()):
"description": "NVDA Ease of Access proxy",
"product_name":name,
"product_version": version,
"copyright": copyright,
"copyright": NVDAcopyright,
"company_name": publisher,
},
],
Expand All @@ -207,7 +178,7 @@ def getRecursiveDataFiles(dest,source,excludes=()):
"description": "NVDA Diff-match-patch proxy",
"product_name": name,
"product_version": version,
"copyright": f"{copyright}, Bill Dengler",
"copyright": f"{NVDAcopyright}, Bill Dengler",
"company_name": f"Bill Dengler, {publisher}",
},
],
Expand Down
19 changes: 19 additions & 0 deletions source/winAPI/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2022 NV Access Limited
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

import enum


class HResult(enum.IntEnum):
# https://docs.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values
S_OK = 0x00000000
E_ACCESS_DENIED = 0x80070005 # E_ACCESSDENIED
E_INVALID_ARG = 0x80070057 # E_INVALIDARG


class SystemErrorCodes(enum.IntEnum):
# https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
ACCESS_DENIED = 0x5
INVALID_PARAMETER = 0x57
93 changes: 93 additions & 0 deletions source/winAPI/dpiAwareness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2022 NV Access Limited
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

import ctypes

from logHandler import log

from .constants import (
HResult,
SystemErrorCodes,
)


def setDPIAwareness() -> None:
"""
Different versions of Windows inconsistently support different styles of DPI Awareness.
This function attempts to set process DPI awareness using the most modern Windows API method available.
Only call this function once per instance of NVDA.
Only call this function when running from source.
It is recommended that you set the process-default DPI awareness via application manifest.
Setting the process-default DPI awareness via these API calls can lead to unexpected application behavior.
"""
# Support is inconsistent across versions of Windows, so try/excepts are used rather than explicit
# version checks.
# https://docs.microsoft.com/en-us/windows/win32/hidpi/setting-the-default-dpi-awareness-for-a-process
try:
# An advancement over the original per-monitor DPI awareness mode,
# which enables applications to access new DPI-related scaling behaviors on a per top-level window basis.
# For more information on behaviours, refer to:
# https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setprocessdpiawarenesscontext
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4
# Method introduced in Windows 10
# https://docs.microsoft.com/en-us/windows/win32/hidpi/dpi-awareness-context
success = ctypes.windll.user32.SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
except AttributeError:
log.debug("Cannot set DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2")
else:
if success:
return
else:
errorCode = ctypes.GetLastError()
if errorCode == SystemErrorCodes.ACCESS_DENIED:
# The DPI awareness is already set,
# either by calling this API previously or through the application (.exe) manifest.
# This is unexpected as we should only set DPI awareness once.
# NVDA sets DPI awareness from the manifest,
# however this function should only be called when running from source.
log.error("DPI Awareness already set.")
return
elif errorCode == SystemErrorCodes.INVALID_PARAMETER:
log.error("DPI Awareness function provided invalid argument.")
else:
log.error(f"Unknown error setting DPI Awareness. Error code: {errorCode}")

log.debug("Falling back to older method of setting DPI Awareness")

try:
# https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/nf-shellscalingapi-setprocessdpiawareness
# This window checks for the DPI when it is created and adjusts the scale factor whenever the DPI changes.
# These processes are not automatically scaled by the system.
PROCESS_PER_MONITOR_DPI_AWARE = 2
# Method introduced in Windows 8
hResult = ctypes.windll.shcore.SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE)
except AttributeError:
log.debug("Cannot set PROCESS_PER_MONITOR_DPI_AWARE")
else:
if hResult == HResult.S_OK:
return
elif hResult == HResult.E_ACCESS_DENIED:
# The DPI awareness is already set,
# either by calling this API previously or through the application (.exe) manifest.
# This is unexpected as we should only set DPI awareness once.
# NVDA sets DPI awareness from the manifest,
# however this function should only be called when running from source.
log.error("DPI Awareness already set.")
return
elif hResult == HResult.E_INVALID_ARG:
log.error("DPI Awareness function provided invalid argument.")
else:
log.error(f"Unknown error setting DPI Awareness. HRESULT: {hResult}")

log.debug("Falling back to legacy method of setting DPI Awareness")

# Method introduced in Windows Vista
# https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setprocessdpiaware
result = ctypes.windll.user32.SetProcessDPIAware()
if result == 0:
errorCode = ctypes.GetLastError()
log.error(f"Unknown error setting DPI Awareness. Error code: {errorCode}")
9 changes: 9 additions & 0 deletions user_docs/en/changes.t2t
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ What's New in NVDA
- These fixes apply to Windows 11 Sun Valley 2 (version 22H2) and later.
- Selective registration for UI Automation events and property changes now enabled by default.
-
- NVDA is now DPI aware when using multiple monitors.
There are several fixes for using a DPI setting higher than 100% or multiple monitors.
Issues may still exist with versions of Windows older than Windows 10 1809.
Applications which NVDA interacts with also need to be DPI aware. (#13254)
- Visual highlighting frames should now be correctly placed in most applications. (#13370, #3875, #12070)
- Touch screen interaction should now be accurate for most applications. (#7083)
- Mouse tracking should now work for most applications.
Note there are still known issues with Chrome. (#6722, #7915)
-
- NVDA will announce UIA item status property changes in places such as the Visual Studio 2022 create app packages dialog. (#13973)
-

Expand Down

0 comments on commit 28b4756

Please sign in to comment.