2

I use this code to turn on Dpi Awareness, in Python 2.7, under Win10

import ctypes
import platform
ctypes.windll.shcore.SetProcessDpiAwareness(True)

but

ctypes.windll.shcore.SetProcessDpiAwareness(False)

does not turn it off. I am looking for any suggestions please.

fthomson
  • 773
  • 3
  • 9
jvbSherman
  • 21
  • 1
  • Microsoft doesn't expect the DPI awareness to change during the running of a program. The [documentation for `SetProcessDpiAwareness`](https://learn.microsoft.com/en-us/windows/win32/api/shellscalingapi/nf-shellscalingapi-setprocessdpiawareness) is sprinkled with warnings, and they suggest you use an application manifest instead. – Mark Ransom Jul 27 '21 at 16:13

2 Answers2

3

The documentation for SetProcessDpiAwareness explicitly states:

Once API awareness is set for an app, any future calls to this API will fail.

What you're asking for simply isn't possible.

Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
3

While Mark Ransom's answer is technically correct, it's still possible to toggle the DPI awareness state thread-wise via SetThreadDpiAwarenessContext. A minimal example:

from ctypes import windll, wintypes
windll.user32.SetThreadDpiAwarenessContext(wintypes.HANDLE(-2))  # Toggle ON
windll.user32.SetThreadDpiAwarenessContext(wintypes.HANDLE(-1))  # Toggle OFF

This function was introduced in version 1607 (July 2016) of Windows 10. More DPI features were introduced later as discussed in this official blog post.

Here's an implementation of a DPI awareness manager class with a basic usage example with Tkinter:

# MIT-0 License
#
# Copyright (c) 2022 Hugo Spinelli
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import ctypes
import ctypes.wintypes
import enum
import itertools
import tkinter


class DPIAware(enum.Enum):
    # https://learn.microsoft.com/windows/win32/hidpi/dpi-awareness-context
    SYSTEM_AWARE = ctypes.wintypes.HANDLE(-2)
    PER_MONITOR_AWARE = ctypes.wintypes.HANDLE(-3)
    PER_MONITOR_AWARE_V2 = ctypes.wintypes.HANDLE(-4)


class DPIUnaware(enum.Enum):
    # https://learn.microsoft.com/windows/win32/hidpi/dpi-awareness-context
    UNAWARE = ctypes.wintypes.HANDLE(-1)
    UNAWARE_GDISCALED = ctypes.wintypes.HANDLE(-5)


class ProcessDPIAwareness(enum.Enum):
    # https://learn.microsoft.com/windows/win32/api/shellscalingapi/ne-shellscalingapi-process_dpi_awareness
    UNAWARE = 0
    SYSTEM_AWARE = 1
    PER_MONITOR_AWARE = 2


class DeviceCapsIndex(enum.Enum):
    """Item to be returned by the GetDeviceCaps function."""

    # The values are defined as macros in C, so they are not exposed to the
    # windll API. They can alternatively be obtained from the pywin32 package:
    # pip install pywin32
    # Example:
    #   >>> import win32con
    #   >>> win32con.HORZSIZE
    #   ... 4
    # https://pypi.org/project/pywin32/
    # https://learn.microsoft.com/windows/win32/api/wingdi/nf-wingdi-getdevicecaps
    
    HORZSIZE = 4
    """Width, in millimeters, of the physical screen."""
    
    VERTSIZE = 6
    """Height, in millimeters, of the physical screen."""
    
    HORZRES = 8
    """Width, in pixels, of the screen."""
    
    VERTRES = 10
    """Height, in raster lines, of the screen."""
    
    ASPECTX = 40
    """Relative width of a device pixel used for line drawing."""
    
    ASPECTY = 42
    """Relative height of a device pixel used for line drawing."""
    
    ASPECTXY = 44
    """Diagonal width of the device pixel used for line drawing."""
    
    LOGPIXELSX = 88
    """Number of pixels per logical inch along the screen width. In a system
    with multiple display monitors, this value is the same for all monitors."""
    
    LOGPIXELSY = 90
    """Number of pixels per logical inch along the screen height. In a system
    with multiple display monitors, this value is the same for all monitors."""


class DPIManager:
    """DPI awareness manager."""
    
    def __init__(self,
                 aware_state: DPIAware = DPIAware.PER_MONITOR_AWARE_V2,
                 unaware_state: DPIUnaware = DPIUnaware.UNAWARE_GDISCALED):
        """Parameters:
        - `aware_state`: Default DPI-aware state. Possible values:
           - `DPIAware.SYSTEM_AWARE`
           - `DPIAware.PER_MONITOR_AWARE`
           - `DPIAware.PER_MONITOR_AWARE_V2`
        - `unaware_state`: Default DPI-unaware state. Possible values:
           - `DPIUnaware.UNAWARE`
           - `DPIUnaware.UNAWARE_GDISCALED`
        """
        ctx = ctypes.windll.user32.GetThreadDpiAwarenessContext()
        self._original_awareness = self.get_awareness(ctx)
        self.aware_state = aware_state
        self.unaware_state = unaware_state

    @staticmethod
    def set(awareness: DPIAware | DPIUnaware | ProcessDPIAwareness):
        """Set the DPI awareness state."""
        if awareness in itertools.chain(DPIAware, DPIUnaware):
            ctypes.windll.user32.SetThreadDpiAwarenessContext(awareness.value)
        elif awareness in ProcessDPIAwareness:
            # WARNING: This affects all threads in the current process and
            # can only be reversed thread-by-thread
            ctypes.windll.shcore.SetProcessDpiAwareness(awareness.value)
        else:
            raise ValueError(f'Invalid argument type {type(awareness)!r}')

    def toggle(self):
        """Toggle DPI awareness states."""
        if self.is_aware():
            self.set(self.unaware_state)
        else:
            self.set(self.aware_state)

    def restore(self):
        """Restore the original DPI awareness state."""
        self.set(self._original_awareness)

    def is_aware(self, awareness: DPIAware | DPIUnaware | None = None) -> bool:
        """Check if the state is DPI-aware."""
        if awareness is None:
            awareness = self.get_awareness()
        return awareness in DPIAware

    @staticmethod
    def get_awareness(_ctx: int | None = None) -> DPIAware | DPIUnaware:
        """Get the current DPIAwarenessContext parameter."""
        if _ctx is None:
            _ctx = ctypes.windll.user32.GetThreadDpiAwarenessContext()
        for ctx_type in itertools.chain(DPIAware, DPIUnaware):
            if ctypes.windll.user32.AreDpiAwarenessContextsEqual(
                _ctx, ctx_type.value
            ):
                return ctx_type
        raise ValueError(f'Unknown DPI context type ({_ctx!r})')

    @staticmethod
    def get_dpi(_ctx: int | None = None) -> int:
        if _ctx is None:
            _ctx = ctypes.windll.user32.GetThreadDpiAwarenessContext()
        # This might fail and return 0 for PER_MONITOR_AWARE and
        # PER_MONITOR_AWARE_V2. This is because the DPI of a per-monitor-aware
        # window can change, and the actual DPI cannot be returned without the
        # window's HWND.
        if dpi := ctypes.windll.user32.GetDpiFromDpiAwarenessContext(_ctx):
            return dpi
        # Set up the DPI awareness context to be checked
        original_ctx = ctypes.windll.user32.GetThreadDpiAwarenessContext(_ctx)
        # Create a temporary window to probe the screen DPI
        temp_window = tkinter.Tk()
        dc = ctypes.windll.user32.GetDC(temp_window.winfo_id())
        dpi = ctypes.windll.gdi32.GetDeviceCaps(
            dc, DeviceCapsIndex.LOGPIXELSX.value
        )
        temp_window.destroy()
        # Restore the original DPI awareness context
        ctypes.windll.user32.SetThreadDpiAwarenessContext(original_ctx)
        return dpi

    @staticmethod
    def get_scale() -> float:
        """Get the scale factor of the monitor."""
        DEVICE_PRIMARY = 0
        # DEVICE_IMMERSIVE = 1
        # https://learn.microsoft.com/windows/win32/api/shellscalingapi/ne-shellscalingapi-display_device_type
        return ctypes.windll.shcore.GetScaleFactorForDevice(DEVICE_PRIMARY)/100


if __name__ == '__main__':
    import tkinter.font
    
    dpi_manager = DPIManager()
    
    def spawn_window(title: str, scale_window: bool = True):
        WIDTH = 260
        HEIGHT = 70

        dpi = dpi_manager.get_dpi()
        awareness = dpi_manager.get_awareness()
        is_aware = dpi_manager.is_aware(awareness)
        scale = dpi_manager.get_scale() if is_aware else 1
        
        root = tkinter.Tk()
        root.title(title)
        width = round(WIDTH*scale) if scale_window else WIDTH
        height = round(HEIGHT*scale) if scale_window else HEIGHT
        root.geometry(f'{width}x{height}')
        font_size = tkinter.font.Font(font='TkDefaultFont').actual()['size']
        label_texts = [
            f'DPI: {dpi}',
            f'Awareness: {awareness.name}',
            f'Font size: {font_size}',
        ]
        canvas = tkinter.Canvas(root)
        labels = [tkinter.Label(canvas, text=text) for text in label_texts]
        for label in labels:
            label.pack()
        canvas.pack(expand=True)
        tkinter.mainloop()

    # Original state (DPI-unaware)
    spawn_window('1')
    # Toggle state (DPI-aware)
    dpi_manager.toggle()
    # Original settings for the window size
    spawn_window('2', scale_window=False)
    # Properly scaled window
    spawn_window('3', scale_window=True)
    # Restore state (DPI-unaware)
    dpi_manager.toggle()  # or .restore()
    spawn_window('4')

On my screen, with 96 DPI and scale factor of 150%, the windows pop up as following:

DPI-Awareness-Tkinter

Window 2 has the original size (geometry), which looks smaller on a DPI-aware context. Window 3 had its size multiplied by the scale factor to look the same size as window 1. Note that the font size is automatically adjusted by Tkinter. The default size 9 is multiplied by 150% and rounded to 14. I'm not sure if window 4 was supposed to look better than window 1 with the UNAWARE_GDISCALED option, but they look the same to me.

Wood
  • 271
  • 1
  • 8
  • 1
    Just a heads-up that I think you might want to **Set** `ctypes.windll.user32.SetThreadDpiAwarenessContext(original_ctx)` instead of **Get** at the end of `get_dpi` – JRiggles Apr 06 '23 at 15:52