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

Improved DPI Scaling on Windows and Fixed related Bugs #2155

Open
wants to merge 125 commits into
base: main
Choose a base branch
from

Conversation

proneon267
Copy link
Contributor

@proneon267 proneon267 commented Oct 15, 2023

While investigating the scaling problems encountered in #1930, I found that the call to SetProcessDpiAwarenessContext is erroneous. Hence, all the toga apps are always running in the DPI Unaware mode.

This PR fixes the call and also checks if it was successful or not.

Note that this PR only has changes that fix the call to SetProcessDpiAwarenessContext, so that the call works. It doesn't fix any of the scaling issues currently present in toga. In particular, the test script from #1930, still shows the scaling bugs:

"""
My first application
"""
import toga
from toga.style import Pack
from toga.style.pack import COLUMN, ROW


class HelloWorld(toga.App):
    def startup(self):
        """
        Construct and show the Toga application.

        Usually, you would add your application to a main content box.
        We then create a main window (with a name matching the app), and
        show the main window.
        """
        main_box = toga.Box(style=Pack(direction="column"))

        self.main_window = toga.MainWindow(title=self.formal_name)
        self.main_window.content = main_box
        self.main_window.show()
        self.main_window.position = (0, 0)

        main_box.add(
            toga.Label(text=f"Window Size: {self.main_window.size}"),
            toga.Label(text=f"Window Position: {self.main_window.position}"),
            toga.Label(text=f"dpi_scale: {self.main_window._impl.dpi_scale}"),
            toga.Label(
                text=f"DpiX: {self.main_window._impl.native.CreateGraphics().DpiX}"
            ),
        )

def main():
    return HelloWorld()

At 125% scaling:
image

As you can see, the DpiX and dpi_scale will always be 96 and 1.0 respectively. Furthermore, in the DPI Aware mode, the app menu has a disproportionately larger size compared to the rest of the window elements.

Both of these bugs, indicate that there are problems in the scaling on windows. But those are for separate PRs.

Also, if required, I can add the code to correctly detect the actual system dpi.

PR Checklist:

  • All new features have been tested
  • All new features have been documented
  • I have read the CONTRIBUTING.md file
  • I will abide by the code of conduct

@proneon267
Copy link
Contributor Author

Though not my original intention, but I have fixed all dpi scaling bugs that I had encountered. Earlier, the scaling operations on windows were essentially no-ops. But the latest commit fixes the bugs, and the dpi based scaling operations are now being done properly.

At 125% scaling:
image
This correctly detects the dpi for scaling.

Now, all that remains is to add an event handler to detect dpi change while the app is running and scale correctly.

@proneon267 proneon267 changed the title Fixed bug in calling of SetProcessDpiAwarenessContext Fixed bugs related to dpi scaling on windows Oct 16, 2023
@proneon267
Copy link
Contributor Author

proneon267 commented Oct 18, 2023

I have added an event handler to detect DPI changes while the app is running. I have also modified Scalable class to:

  • Correctly get the latest DPI scale factor in real time
  • Remove dependency on a Control object
  • Allow other APIs to use the scale_in() and scale_out() methods
  • Reduce the number of calls to get the latest DPI value

The latest commit detects the DPI changes of the Primary Screen only. But this will be fixed in #1930, where the DPI scale factor of each screen will be detected individually and will be used to do the scaling.

I have tested the latest commit and it correctly detects new DPI change and scales the elements accordingly.

However, the fonts' don't seem to be using the latest DPI value and as such they are not being scaled when a new DPI change is detected while the app is running. This needs to be fixed.

Copy link
Member

@mhsmith mhsmith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for looking into this, I think you're on the right track.

The latest commit detects the DPI changes of the Primary Screen only. But this will be fixed in #1930, where the DPI scale factor of each screen will be detected individually and will be used to do the scaling.

WinForms has some per-window events for detecting DPI changes, which I think would allow this to be fixed without requiring Toga to be aware of multiple screens. That would allow us to fix all the DPI issues together in this PR, so I've added a "fixes" link to the top comment.

A few more comments:

winforms/src/toga_winforms/app.py Outdated Show resolved Hide resolved
winforms/src/toga_winforms/app.py Outdated Show resolved Hide resolved
winforms/src/toga_winforms/app.py Outdated Show resolved Hide resolved
winforms/src/toga_winforms/app.py Outdated Show resolved Hide resolved
@proneon267
Copy link
Contributor Author

WinForms has some per-window events for detecting DPI changes, which I think would allow this to be fixed without requiring Toga to be aware of multiple screens.

I have tested them previously and was thinking about using DpiChanged event & DeviceDpiNew: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.dpichangedeventargs.devicedpinew?view=windowsdesktop-7.0#system-windows-forms-dpichangedeventargs-devicedpinew

But, none of these events trigger when the system DPI changes. Only the SystemEvents.DisplaySettingsChanged event is triggered consistently when system DPI changes.

I am searching for a proper way to address this and will let you know as soon as I find a viable solution.

@proneon267
Copy link
Contributor Author

I have added support so that the font scales when the DPI changes while the app is running. I have also added Screen as an optional dependency to Scalable class, so that the DPI can be found for the current screen, without the need for the API in #1930, while still allowing other APIs to use scale_in and scale_out methods.

The following is the same run of the test script with the latest commit:
At 125% scaling:
image

At 175% scaling:
image

The fonts scale correctly and detect the DPI changes while the app is running.

@proneon267
Copy link
Contributor Author

When the app is scaled from 125% -> 100% -> 175%, then the menu bar appears to be clipped, due to incorrect calculation of main_window.context box:
Screenshot (20)
This needs to be fixed.

@proneon267
Copy link
Contributor Author

proneon267 commented Oct 22, 2023

Nevermind. The bug was caused due to the faulty implementation of WeakRef introduced in #2066 and reported in #2163.

self.native.Resize += WeakrefCallable(self.winforms_Resize)

After removing the WeakRef wrapping, the resizing event handler appears to fire consistently when the DPI scale is changed. Here is the app after fixing the bug:
image
So, this bug will be resolved automatically when #2163 is addressed. Looks like the weakref calls in different places are causing problems. I wonder if this is also leading to failing of the tests on windows testbed.

@freakboy3742
Copy link
Member

So, this bug will be resolved automatically when #2163 is addressed. Looks like the weakref calls in different places are causing problems. I wonder if this is also leading to failing of the tests on windows testbed.

To be clear - the tests are not failing on main at present. If they're failing in this PR, it's either an unintended side effect of something in this PR, or an error that isn't 100% reproducible.

The latter does happen sometimes - it's the nature of running a GUI test that sometimes, the GUI doesn't respond quite quickly enough, which results in a test failure. Re-running the test will (usually) fix these problems; however, I've just re-run the tests, and the same problem is occurring, which suggests it likely isn't a transient problem - it's an unintended side effect.

From a quick inspection, I can't see any obvious connection between this PR's changes and OptionContainer. However, I can confirm that when I run the optioncontainer tests, I see the problem locally, and if I remove the Weakref usage from OptionContainer, the problem remains.

@proneon267
Copy link
Contributor Author

In that case, I'll search further and report back what is causing the tests to fail.

@proneon267
Copy link
Contributor Author

proneon267 commented Oct 23, 2023

Turns out the bugs were related to Hwnds being created at inappropriate times.

As discussed in #2155 (comment), the Hwnds are being created even before the Application instance is created.

I have fixed the current bug by initializing and disposing a graphics context at the time of widget Hwnd creation.

But, I recon more bugs related to Hwnd will be encountered in the future due to the way toga app execution flow works. But that's for the future.

@proneon267 proneon267 changed the title Fixed bugs related to dpi scaling on windows Fixed Bugs related to DPI Scaling on Windows Oct 23, 2023
@proneon267 proneon267 reopened this Jul 10, 2024
@proneon267
Copy link
Contributor Author

proneon267 commented Jul 10, 2024

I have updated to latest main branch. The dpi scaling is working properly. But between the last time I had updated to the latest main branch and now, something has changed in the main branch due to which it reports wrong screen_position, when the dpi scale is not 1. Also, somehow screen.get_image_data() now automatically takes correct screenshot of correct size without any scale_in applied to the screen's origin and size.

I suspect these changes have happened after the new named tuples - Position and Size were introduced. I took a quick look at the changes but couldn't find anything that would cause this problem. I might need to do deeper investigation to find the source of the bugs.
Only Partially true since that pull request corrected screen.size, which earlier was reporting scaled out size, and so these bugs remained hidden.

P.S. If you suspect anything that might be causing these bugs, then please let me know.

@proneon267
Copy link
Contributor Author

On doing a deeper inspection, I have found that when dpi awareness is enabled, the following things happen:

Object Operates in Scaling Required
Form.Position dpi in-dependent coordinates No
Form.Size dpi dependent coordinates Yes
Screen.Bounds dpi in-dependent coordinates No

This complicates things, since earlier, to move a window to the right side of the screen, we could do:

self.main_window.screen_position = (
            (self.main_window.screen.size.width - self.main_window.size.width),
            self.main_window.position.y,
        )

But now, we need to do:

self.main_window.screen_position = (
            (self.main_window.screen.size.width - self.main_window.size.width*(dpi_scale)),
            self.main_window.position.y,
        )

@proneon267
Copy link
Contributor Author

@mhsmith I would greatly appreciate your feedback regarding this new issue.

@proneon267
Copy link
Contributor Author

@mhsmith friendly reminder to take a look look at this, as soon as you are free.

@proneon267
Copy link
Contributor Author

@mhsmith Just a gentle nudge to take a look at this whenever you are free.

@mhsmith
Copy link
Member

mhsmith commented Nov 4, 2024

You should be able to test this by setting up a situation where some widgets are set to their minimum size, which is smaller than the window, and then checking that they scale correctly. For example:

toga.Box(
    style=Pack(direction="row"),
    children=[
        toga.Box(style=Pack(flex=1)),
        toga.Button(text="hello"),
        toga.Button(text="world"),
        toga.Box(style=Pack(flex=1)),
    ]
)

When the scale factor increases, the buttons should get bigger, and the spacer boxes should get smaller.

This was a bad idea, because it assumes that the window's physical size remains unchanged, which we've already found wasn't happening on @proneon267's machine. But the tests still passed for him because they didn't actually contain any assertions, only comparisons with no assert keyword.

I'll replace this with a different approach.

@mhsmith
Copy link
Member

mhsmith commented Nov 4, 2024

Actually, disregard my previous comment (apart from the tests having no assertions) -- the window's physical size will always remain unchanged, because it's only a mock DPI change, not a real one.

@mhsmith
Copy link
Member

mhsmith commented Nov 5, 2024

I'm working on the tests at the moment, so you don't need to push any more changes.

@proneon267
Copy link
Contributor Author

Thanks for helping :) and apologies for causing any trouble.

@mhsmith
Copy link
Member

mhsmith commented Nov 6, 2024

I had earlier found somewhere in Dotnet source code that DpiChanged event is conditionally turned off from firing when DPI settings are changed.

I see some mention of that here, and the relevant .NET code is indeed very difficult to understand. It looks like the events may only be enabled if you've set some metadata on the executable, but this isn't an option for Toga, unless we can set the metadata at runtime before .NET reads it.

They mention DpiChangedBeforeParent and DpiChangedAfterParent as possibly being more reliable. But let's continue with your current approach for now.

DpiChangedBeforeParent and DpiChangedAfterParent are not firing either.

@proneon267
Copy link
Contributor Author

DpiChangedBeforeParent and DpiChangedAfterParent are not firing either.

I think all these newer DPI related events will not be triggered unless DPI awareness is specified in a manifest file, which I haven't been able to figure out.

Btw, the new test looks great :)

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

Successfully merging this pull request may close these issues.

Text fuzzy on Windows with per-monitor scaling WinForms DPI scaling will look blurry for most users
3 participants