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

DPI scaling for Windows 10 and later #982

Merged
merged 20 commits into from
Aug 19, 2023

Conversation

SteffenL
Copy link
Collaborator

Scales when the DPI setting changes on the window's display:

change_dpi.webm

Scales when moving window to display with different DPI:

move_between_displays.webm

Test cases

Case Description Expected
1 Start on primary display with 100% DPI. Window not scaled.
2 Start on display with >100% DPI when primary display at 100%. Window scaled up.
3 Start on primary display with >100% DPI. Window scaled up.
4 Move window to displays of varying DPI. Window scaled with display DPI.
5 Start on display with >100% DPI without calling *set_size(). Window scaled up.
6 Change DPI for window's display. Window scaled with new DPI.

Result for 5defd8c (master)

Case OK? Reason
1 yes Window not scaled.
2 no Window at 100% and only WebView2 scaled up.
3 no Window at 100% and only WebView2 scaled up.
4 no Window not scaled, WebView2 not scaled until manual window resize.
5 no Window at 100% and only WebView2 scaled up.
6 no Window not scaled and only WebView2 scaled.

Result for b7a05b5

Case OK? Reason
1 yes Window not scaled.
2 yes Window scaled up.
3 yes Window scaled up.
4 yes Window scaled with display DPI.
5 yes Window scaled up.
6 yes Window scaled with new DPI.

webview.h Outdated Show resolved Hide resolved
webview.h Show resolved Hide resolved
webview.h Show resolved Hide resolved
webview.h Outdated Show resolved Hide resolved
webview.h Show resolved Hide resolved
webview.h Outdated Show resolved Hide resolved
webview.h Outdated Show resolved Hide resolved
webview.h Show resolved Hide resolved
@SteffenL SteffenL linked an issue Aug 13, 2023 that may be closed by this pull request
@SteffenL
Copy link
Collaborator Author

SteffenL commented Aug 13, 2023

Observations on Windows 11 when changing DPI from 100% to 200% while the app is running:

  • Width of border scales from 1 to 2 pixels.
  • Height of title bar scales from 30 to 56 pixels.
  • Client area scales from 480x320 to 950x608 (expected 960x640).
  • Window including non-client area scales...
    • From: 1x1 + 0x30 + 480x320 + 1x1 = 482x352
    • To: 2x2 + 0x56 + 950x608 + 2x2 = 954x668 (expected 964x700)

dpi_100
dpi_200

@SteffenL
Copy link
Collaborator Author

There's a problem with trusting the suggested window bounds we get when handling WM_DPICHANGED. It just doesn't scale according to expectations. When we scale in steps from 100% to 250%, the suggested bounds are seemingly wrong at every step above 100%. When we then go straight down to 100% again then the size ends up being different (480x321) from what we started with (480x320). If we don't call EnableNonClientDpiScaling then the suggested bounds are even further off.

@SteffenL
Copy link
Collaborator Author

The latest code calculates the new scaled window size inside set_size().

@SteffenL
Copy link
Collaborator Author

The work with scaling with DPI changes wasn't as easy as I imagined it would be.

@sdicker8 If you find the time to check whether this PR is satisfactory for you then it would be of great help. I'll do more testing later as well as I've only tested the new code on Windows 10. Do you see any room for optimization here? Aside for whether it works properly, I'm particularly interested in your opinion about the calculations in win32_edge_engine::set_size which I'm too tired to think about anymore.

@sdicker8
Copy link

sdicker8 commented Aug 17, 2023

It does look like you had to touch the code in several different places. I probably would have had a difficult time with some of that.

Unfortunately, this version doesn't work on my win 11 laptop. If i'm at 100% and start the app it looks fine. If i then scale up, it scales the window too much. Also, if i start the app at a scale that is greater than 100%, it's too small - just like the original problem.

As a slight aside, regarding the apparent difference between windows 10 & 11, there was a windows 10 version number associated with some of the api - i was wondering if your version was less than that.

As for the set_size function you have to know that i tend to have a very minimalistic coding style. I would personally stay away from floating point math here - i just see no need for it (but i do like your epsilon check). I would probably just use a simple 'get_window_dpi' function, use the result from that in any required logic, and completely avoid scale and the scale type. If and when you need to scale the width and height you could just make sure to divide by the default_dpi last and you will have a correct integer result.

Having to do that 'undo' stuff seemed unfortunate - i'm wondering if that would still be there once the problems that i'm seeing on win11 are ironed out.

Sorry - i wish i could have given you a better win11 report and sorry that the complexity turned out to be so much.

I'll be happy to test again -

@SteffenL
Copy link
Collaborator Author

Oh, my bad. I was focusing on the changing DPI and didn't realize that it wasn't scaling properly on startup anymore. I've looked into that also got rid of the floating point arithmetic. The complexity is still greater than I would like but first of all we need to get it to work reliably.

This now appears to work properly on my Windows 10 system but there's a problem on Windows 11.

On Windows 11, at the time we receive WM_DPICHANGED, the system has for some reason already automatically resized the window, something that is unusual to me compared to older versions of Windows. This appears to cause GetClientRect to return the already-scaled dimensions which are then used for our own calculation and messes up the scaling.

It would have been easy if we could just skip our own calculations on Windows 11 but the new dimensions of the window after the automatic scaling done by Windows 11 are really unexpected. On my system it goes from 480x320 at 100% to something greater than the expected 960x640. This result is similar to when we just use the suggested dimensions provided by WM_DPICHANGED on Windows 10.

@SteffenL
Copy link
Collaborator Author

Common Pitfalls (Win32):

Not using the suggested rectangle that is provided in WM_DPICHANGED

When Windows sends your application window a WM_DPICHANGED message, this message includes a suggested rectangle that you should use to resize your window. It is critical that your application use this rectangle to resize itself, as this will:

Ensure that the mouse cursor will stay in the same relative position on the Window when dragging between displays
Prevent the application window from getting into a recursive dpi-change cycle where one DPI change triggers a subsequent DPI change, which triggers yet another DPI change.

If you have application-specific requirements that prevent you from using the suggested rectangle that Windows provides in the WM_DPICHANGED message, see WM_GETDPISCALEDSIZE. This message can be used to give Windows a desired size you'd like used once the DPI change has occurred, while still avoiding the issues described above.

@SteffenL
Copy link
Collaborator Author

SteffenL commented Aug 17, 2023

The library uses per monitor awareness (v1, not v2).

DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE

Per monitor DPI aware. 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.

Source: https://learn.microsoft.com/en-us/windows/win32/hidpi/dpi-awareness-context

To me the statement that "processes are not automatically scaled by the system" just doesn't seem to be true on Windows 11 during a DPI change.

@sdicker8
Copy link

sdicker8 commented Aug 17, 2023

Is there anything to be learned from this simple example ? maybe the style? all of the scaling works -

#include <windows.h>
#include <stdlib.h>
#include <string>
#include <tchar.h>

static TCHAR szWindowClass[] = _T("SimpleApp");
static TCHAR szTitle[] = _T("Simple app");

HINSTANCE hInst;

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);


int CALLBACK WinMain(
        _In_ HINSTANCE hInstance,
        _In_ HINSTANCE hPrevInstance,
        _In_ LPSTR     lpCmdLine,
        _In_ int       nCmdShow
)
{
        (void)hPrevInstance;
        (void)lpCmdLine;

        WNDCLASSEX wcex;

        wcex.cbSize = sizeof(WNDCLASSEX);
        wcex.style = CS_HREDRAW | CS_VREDRAW;
        wcex.lpfnWndProc = WndProc;
        wcex.cbClsExtra = 0;
        wcex.cbWndExtra = 0;
        wcex.hInstance = hInstance;
        wcex.hIcon = NULL;
        wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
        wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
        wcex.lpszMenuName = NULL;
        wcex.lpszClassName = szWindowClass;
        wcex.hIconSm = NULL;

        if (!RegisterClassEx(&wcex))
        {
                MessageBox(NULL,
                        _T("Call to RegisterClassEx failed!"),
                        _T("Windows Desktop Guided Tour"),
                        NULL);

                return 1;
        }

        hInst = hInstance;

        HWND hWnd = CreateWindow(
                szWindowClass,
                szTitle,
                WS_OVERLAPPEDWINDOW,
                CW_USEDEFAULT, CW_USEDEFAULT,
                800, 600,
                NULL,
                NULL,
                hInstance,
                NULL
        );

        if (!hWnd)
        {
                MessageBox(NULL,
                        _T("Call to CreateWindow failed!"),
                        _T("Windows Desktop Guided Tour"),
                        NULL);

                return 1;
        }

        ShowWindow(hWnd, nCmdShow);
        UpdateWindow(hWnd);

        MSG msg;
        while (GetMessage(&msg, NULL, 0, 0))
        {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
        }

        return (int)msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
        switch (message)
        {
        case WM_SIZE:
                break;
        case WM_DESTROY:
                PostQuitMessage(0);
                break;
        default:
                return DefWindowProc(hWnd, message, wParam, lParam);
                break;
        }

        return 0;
}

cl /std:c++17 /W4 /EHsc /D_UNICODE /DUNICODE /DWIN32 /D_WINDOWS /Fobuild\ simple.cpp /link user32.lib

@SteffenL
Copy link
Collaborator Author

DPI unaware programs such as the code you provided can be scaled by the system but I'll be surprised if it isn't blurry due to stretching.

@sdicker8
Copy link

@SteffenL
Copy link
Collaborator Author

SteffenL commented Aug 17, 2023

I don't know about Windows 11 but this is how I expect DPI unaware programs to look on earlier OS:

image

Here I didn't embed WebView2 but added a button to the window and modified the code a bit to add styling (font needs more code).

@SteffenL
Copy link
Collaborator Author

Tried it and it looks like this on Windows 11:

image

Not blurry as on Windows 10 but pixelated (don't look too much at the font).

@SteffenL
Copy link
Collaborator Author

SteffenL commented Aug 17, 2023

If we just enable DPI awareness through a manifest file without also actually adding the logic to make it DPI-aware then it looks like this:

Windows 10:
image

Windows 11:
image

Here the button isn't scaled in either example, and the title bar isn't scaled on Windows 10 (oops, that seems to scale after all because the manifest file specified per monitor v2 awareness). With per monitor v1 awareness the titlebar doesn't scale on either system.

The top-level window scales automatically on Windows 11 (weird) but not on Windows 10.

@sdicker8
Copy link

@SteffenL
Copy link
Collaborator Author

On Windows 10, the size we get when handling WM_DPICHANGED is exactly what we supplied when handling WM_GETDPISCALEDSIZE.

On Windows 11, what we receive when handling WM_DPICHANGED is something else.

It's hard to believe that this is functioning as intended.

@sdicker8
Copy link

Still looks good on my machine. And, for what i understand, i like your changes. Wish i had more experience with the win api so that i could help with understanding the weirdness -- seems to me like you are really getting there though -

@SteffenL
Copy link
Collaborator Author

OS Scales State DPI change effective when?
7 no ok logoff
8 no ok logoff
8.1 no ok logoff
10 (1607) yes ok immediately
10 (22H2) yes ok immediately
11 (22H2) yes ok Immediately

@SteffenL
Copy link
Collaborator Author

I believe this is finally good to go.

@sdicker8
Copy link

Still looking good on my machine.

I get a couple of minor warnings with /W4

Great work with significant impact! thank you -

@SteffenL
Copy link
Collaborator Author

Thank you! I'll take a look at the warnings.

Do you see any new warnings other than warning C4456: declaration of 'fn' hides previous local declaration?

@sdicker8
Copy link

these are what i see:

webview.h(1525): warning C4456: declaration of 'fn' hides previous local declaration
webview.h(1517): note: see declaration of 'fn'
webview.h(1527): warning C4456: declaration of 'fn' hides previous local declaration
webview.h(1525): note: see declaration of 'fn'
webview.h(1974): warning C4100: 'sender': unreferenced formal parameter

@SteffenL
Copy link
Collaborator Author

I've sorted out the new C4456 warnings.

@SteffenL
Copy link
Collaborator Author

I've submitted #995 for the C4100 warnings.

@SteffenL SteffenL merged commit 7e74e31 into webview:master Aug 19, 2023
30 checks passed
@SteffenL SteffenL deleted the win10-dpi-scaling branch August 19, 2023 00:06
@SteffenL SteffenL added this to the v0.10.0 milestone Sep 1, 2023
@SteffenL SteffenL added the enhancement New feature, enhancement of an existing feature or a request label Sep 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature, enhancement of an existing feature or a request
Development

Successfully merging this pull request may close these issues.

Windows display scaling problem
2 participants