Skip to content

Commit

Permalink
Updated to latest main branch
Browse files Browse the repository at this point in the history
  • Loading branch information
proneon267 committed Jul 10, 2024
1 parent e343028 commit 71196f3
Show file tree
Hide file tree
Showing 18 changed files with 377 additions and 68 deletions.
1 change: 1 addition & 0 deletions changes/2155.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DPI scaling on Windows is now improved and related bugs are fixed.
10 changes: 8 additions & 2 deletions examples/window/window/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,16 @@ def do_right(self, widget, **kwargs):
self.main_window.position = (2000, 500)

def do_left_current_screen(self, widget, **kwargs):
self.main_window.screen_position = (0, 100)
self.main_window.screen_position = (
self.main_window.screen.origin[0],
self.main_window.screen_position[1],
)

def do_right_current_screen(self, widget, **kwargs):
self.main_window.screen_position = (1080, 100)
self.main_window.screen_position = (
self.main_window.screen.size[0] - self.main_window.size[0],
self.main_window.screen_position[1],
)

def do_small(self, widget, **kwargs):
self.main_window.size = (400, 300)
Expand Down
4 changes: 2 additions & 2 deletions iOS/tests_backend/probe.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio

import toga
from toga_iOS.libs import NSRunLoop, UIScreen
from toga_iOS.libs import NSRunLoop


class BaseProbe:
Expand All @@ -21,5 +21,5 @@ async def redraw(self, message=None, delay=0):

def assert_image_size(self, image_size, size, screen):
# Retina displays render images at a higher resolution than their reported size.
scale = int(UIScreen.mainScreen.scale)
scale = int(screen._impl.native.scale)
assert image_size == (size[0] * scale, size[1] * scale)
97 changes: 97 additions & 0 deletions testbed/tests/app/test_desktop.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from toga.colors import CORNFLOWERBLUE, FIREBRICK, REBECCAPURPLE
from toga.style.pack import Pack

from ..widgets.probe import get_probe
from ..window.test_window import window_probe

####################################################################################
Expand Down Expand Up @@ -357,6 +358,102 @@ async def test_current_window(app, app_probe, main_window):
window3.close()


@pytest.mark.skipif(
toga.platform.current_platform != "windows", reason="This test is Windows specific"
)
async def test_system_dpi_change(
monkeypatch, app, app_probe, main_window, main_window_probe
):
# Store original window content
main_window_content_original = main_window.content

from toga_winforms.libs import shcore

GetScaleFactorForMonitor_original = getattr(shcore, "GetScaleFactorForMonitor")

def set_mock_dpi_scale(value):
def GetScaleFactorForMonitor_mock(hMonitor, pScale):
pScale.value = int(value * 100)

monkeypatch.setattr(
"toga_winforms.libs.shcore.GetScaleFactorForMonitor",
GetScaleFactorForMonitor_mock,
)

dpi_change_events = [
app._impl.winforms_DisplaySettingsChanged,
main_window._impl.winforms_LocationChanged,
main_window._impl.winforms_Resize,
]
for flex_direction in ("row", "column"):
main_window.content = toga.Box(
style=Pack(direction=flex_direction),
children=[
toga.Box(style=Pack(flex=1)),
toga.Button(text="hello"),
toga.Label(text="toga"),
toga.Button(text="world"),
toga.Box(style=Pack(flex=1)),
],
)
widget_dimension_to_compare = "width" if flex_direction == "row" else "height"
await main_window_probe.redraw(
"\nMain Window is ready for testing DPI scaling with "
f"window content flex direction set to: {flex_direction}"
)
for dpi_change_event in dpi_change_events:
print(
f"\nRunning DPI change event: {dpi_change_event.__func__.__qualname__}"
)

# Set initial DPI scale value
set_mock_dpi_scale(1.0)
dpi_change_events[0](None, None)
await main_window_probe.redraw(
"Setting initial DPI scale value to 1.0 before starting DPI scale testing"
)

for pScale_value_mock in (1.25, 1.5, 1.75, 2.0):
# Store original widget dimension
original_widget_dimension = dict()
for widget in main_window.content.children:
widget_probe = get_probe(widget)
original_widget_dimension[widget] = getattr(
widget_probe, widget_dimension_to_compare
)

set_mock_dpi_scale(pScale_value_mock)
# Trigger DPI change event
dpi_change_event(None, None)
await main_window_probe.redraw(
f"Triggering DPI change event for testing scaling at {pScale_value_mock} scale"
)

# Check Widget size DPI scaling
for widget in main_window.content.children:
if isinstance(widget, toga.Box):
# Dimension of spacer boxes should decrease when dpi scale increases
getattr(
get_probe(widget), widget_dimension_to_compare
) < original_widget_dimension[widget]
else:
# Dimension of other widgets should increase when dpi scale increases
getattr(
get_probe(widget), widget_dimension_to_compare
) > original_widget_dimension[widget]

# Restore original state
monkeypatch.setattr(
"toga_winforms.libs.shcore.GetScaleFactorForMonitor",
GetScaleFactorForMonitor_original,
)
dpi_change_events[0](None, None)
main_window.content.window = None
main_window.content = main_window_content_original
main_window.show()
await main_window_probe.redraw("\nRestoring original state of Main Window")


async def test_session_based_app(
monkeypatch,
app,
Expand Down
18 changes: 18 additions & 0 deletions winforms/src/toga_winforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

import toga

from .libs.user32 import (
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
SetProcessDpiAwarenessContext,
)

# Add a reference to the Winforms assembly
clr.AddReference("System.Windows.Forms")

Expand All @@ -16,4 +21,17 @@
"WindowsBase, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
)

# Enable DPI awareness. This must be done before calling any other UI-related code
# (https://learn.microsoft.com/en-us/dotnet/desktop/winforms/high-dpi-support-in-windows-forms).
import System.Windows.Forms as WinForms # noqa: E402

WinForms.Application.EnableVisualStyles()
WinForms.Application.SetCompatibleTextRenderingDefault(False)

if SetProcessDpiAwarenessContext is not None:
if not SetProcessDpiAwarenessContext(
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
): # pragma: no cover
print("WARNING: Failed to set the DPI Awareness mode for the app.")

__version__ = toga._package_version(__file__, __name__)
42 changes: 14 additions & 28 deletions winforms/src/toga_winforms/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import re
import sys
import threading
from ctypes import windll

import System.Windows.Forms as WinForms
from System import Environment, Threading
from Microsoft.Win32 import SystemEvents
from System import Threading
from System.Media import SystemSounds
from System.Net import SecurityProtocolType, ServicePointManager
from System.Windows.Threading import Dispatcher
Expand Down Expand Up @@ -79,32 +79,10 @@ def create(self):
self.app_context = WinForms.ApplicationContext()
self.app_dispatcher = Dispatcher.CurrentDispatcher

# Check the version of windows and make sure we are setting the DPI mode
# with the most up to date API
# Windows Versioning Check Sources : https://www.lifewire.com/windows-version-numbers-2625171
# and https://docs.microsoft.com/en-us/windows/release-information/
win_version = Environment.OSVersion.Version
if win_version.Major >= 6: # Checks for Windows Vista or later
# Represents Windows 8.1 up to Windows 10 before Build 1703 which should use
# SetProcessDpiAwareness(True)
if (win_version.Major == 6 and win_version.Minor == 3) or (
win_version.Major == 10 and win_version.Build < 15063
): # pragma: no cover
windll.shcore.SetProcessDpiAwareness(True)
print(
"WARNING: Your Windows version doesn't support DPI-independent rendering. "
"We recommend you upgrade to at least Windows 10 Build 1703."
)
# Represents Windows 10 Build 1703 and beyond which should use
# SetProcessDpiAwarenessContext(-2)
elif win_version.Major == 10 and win_version.Build >= 15063:
windll.user32.SetProcessDpiAwarenessContext(-2)
# Any other version of windows should use SetProcessDPIAware()
else: # pragma: no cover
windll.user32.SetProcessDPIAware()

self.native.EnableVisualStyles()
self.native.SetCompatibleTextRenderingDefault(False)
# Register the DisplaySettingsChanged event handler
SystemEvents.DisplaySettingsChanged += WeakrefCallable(
self.winforms_DisplaySettingsChanged
)

# Ensure that TLS1.2 and TLS1.3 are enabled for HTTPS connections.
# For some reason, some Windows installs have these protocols
Expand All @@ -129,6 +107,14 @@ def create(self):
# Call user code to populate the main window
self.interface._startup()

######################################################################
# Native event handlers
######################################################################

def winforms_DisplaySettingsChanged(self, sender, event):
for window in self.interface.windows:
window._impl.update_dpi()

######################################################################
# Commands and menus
######################################################################
Expand Down
11 changes: 10 additions & 1 deletion winforms/src/toga_winforms/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@

class Container(Scalable):
def __init__(self, native_parent):
self.init_scale(native_parent)
self.native_parent = native_parent
self.native_width = self.native_height = 0
self.content = None

self.native_content = WinForms.Panel()
native_parent.Controls.Add(self.native_content)

self.native_content.CreateGraphics().Dispose()

@property
def dpi_scale(self):
window = self.content.interface.window
if window:
return window._impl.dpi_scale
else:
return 1

@property
def width(self):
return self.scale_out(self.native_width)
Expand Down
48 changes: 38 additions & 10 deletions winforms/src/toga_winforms/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
ContentAlignment,
Font as WinFont,
FontFamily,
FontStyle,
SystemFonts,
)
from System.Windows.Forms import DialogResult, MessageBoxButtons, MessageBoxIcon

from .libs.user32 import DPI_AWARENESS_CONTEXT_UNAWARE, SetThreadDpiAwarenessContext
from .libs.wrapper import WeakrefCallable


Expand Down Expand Up @@ -98,6 +97,24 @@ class StackTraceDialog(BaseDialog):
def __init__(self, title, message, content, retry):
super().__init__()

# This dialog uses a fixed layout, so we create it as DPI-unaware so it will be
# scaled by the system. "When a window is created, its DPI awareness is defined
# as the DPI awareness of the calling thread at that time."
# (https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-improvements-for-desktop-applications).
self.prev_dpi_context = None
if SetThreadDpiAwarenessContext is not None: # pragma: no branch
self.prev_dpi_context = SetThreadDpiAwarenessContext(
DPI_AWARENESS_CONTEXT_UNAWARE
)
if not self.prev_dpi_context: # pragma: no cover
print("WARNING: Failed to set DPI Awareness for StackTraceDialog")

# Changing the DPI awareness re-scales all pre-existing Font objects, including
# the system fonts.
font_size = 8.25
message_font = WinFont(FontFamily.GenericSansSerif, font_size)
monospace_font = WinFont(FontFamily.GenericMonospace, font_size)

self.native = WinForms.Form()
self.native.MinimizeBox = False
self.native.FormBorderStyle = self.native.FormBorderStyle.FixedSingle
Expand All @@ -114,22 +131,18 @@ def __init__(self, title, message, content, retry):
textLabel.Width = 520
textLabel.Alignment = ContentAlignment.MiddleCenter
textLabel.Text = message

textLabel.Font = message_font
self.native.Controls.Add(textLabel)

# A scrolling text box for the stack trace.
trace = WinForms.RichTextBox()
trace.Left = 10
trace.Top = 30
trace.Top = 35
trace.Width = 504
trace.Height = 210
trace.Height = 205
trace.Multiline = True
trace.ReadOnly = True
trace.Font = WinFont(
FontFamily.GenericMonospace,
float(SystemFonts.DefaultFont.Size),
FontStyle.Regular,
)
trace.Font = monospace_font
trace.Text = content

self.native.Controls.Add(trace)
Expand All @@ -141,6 +154,7 @@ def __init__(self, title, message, content, retry):
retry.Top = 250
retry.Width = 100
retry.Text = "&Retry"
retry.Font = message_font
retry.Click += WeakrefCallable(self.winforms_Click_retry)

self.native.Controls.Add(retry)
Expand All @@ -150,6 +164,7 @@ def __init__(self, title, message, content, retry):
quit.Top = 250
quit.Width = 100
quit.Text = "&Quit"
quit.Font = message_font
quit.Click += WeakrefCallable(self.winforms_Click_quit)

self.native.Controls.Add(quit)
Expand All @@ -159,6 +174,7 @@ def __init__(self, title, message, content, retry):
accept.Top = 250
accept.Width = 100
accept.Text = "&OK"
accept.Font = message_font
accept.Click += WeakrefCallable(self.winforms_Click_accept)

self.native.Controls.Add(accept)
Expand All @@ -173,6 +189,18 @@ def winforms_FormClosing(self, sender, event):
# event will be triggered.
if not self.future.done():
event.Cancel = True # pragma: no cover
else:
# Reverting the DPI awareness at the end of __init__ would cause the window
# to be DPI-aware, presumably because the window isn't actually "created"
# until we call ShowDialog.
#
# This cleanup doesn't make any difference to the dialogs example, because
# "When the window procedure for a window is called [e.g. when clicking a
# button], the thread is automatically switched to the DPI awareness context
# that was in use when the window was created." However, other apps may do
# things outside of the context of a window event.
if self.prev_dpi_context: # pragma: no branch
SetThreadDpiAwarenessContext(self.prev_dpi_context)

def winforms_Click_quit(self, sender, event):
self.future.set_result(False)
Expand Down
5 changes: 5 additions & 0 deletions winforms/src/toga_winforms/libs/shcore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from ctypes import HRESULT, POINTER, windll, wintypes

GetScaleFactorForMonitor = windll.shcore.GetScaleFactorForMonitor
GetScaleFactorForMonitor.restype = HRESULT
GetScaleFactorForMonitor.argtypes = [wintypes.HMONITOR, POINTER(wintypes.UINT)]
Loading

0 comments on commit 71196f3

Please sign in to comment.