2
votes

I'm saving/restoring my window position using GetWindowPlacement/SetWindowPlacement on Windows 10. My application is DPI aware. The issue occurs when SetWindowPlacement is both sizing and moving the window from monitor #1 with one DPI to monitor #2 with a different DPI. The coordinates have been saved as the correct size for monitor #2 in the WINDOWPLACEMENT structure.

The window is first resized during SetWindowPlacement while it's still on monitor #1. Then window is moved to monitor #2, which causes a WM_DPICHANGED message to fire, saying the window size should be changed. The suggested size is incorrect since it's changing the size of the window which was already the correct size for monitor #2. What is the correct way to solve this? Should I be setting a flag before SetWindowPlacement to ignore WM_DPICHANGED messages until that call is done? Are there cases where that will cause me to miss a message I shouldn't be ignoring? Thanks

Edit: Attached repro for @SongZhu-MSFT. In this test case I'm using a Surface Studio 2 as my primary monitor, running at 4500x3000, using 175% scaling. On the right of that monitor, aligned to the bottom is a 1920x1080 monitor set to 100% scaling. This code attempts to open the monitor on the right monitor with a set size, however a DPICHANGE message comes through during the SetWindowPlacement() call which causes the size to be adjusted incorrectly, unless I manually avoid it. Sample code is edited from: https://docs.microsoft.com/en-us/windows/win32/learnwin32/windows-hello-world-sample

#ifndef UNICODE
#define UNICODE
#endif 

#include <windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{

    // Register the window class.
    const wchar_t CLASS_NAME[]  = L"Sample Window Class";
    
    WNDCLASS wc = { };

    wc.lpfnWndProc   = WindowProc;
    wc.hInstance     = hInstance;
    wc.lpszClassName = CLASS_NAME;

    RegisterClass(&wc);

    // Create the window.

    HWND hwnd = CreateWindowEx(
        0,                              // Optional window styles.
        CLASS_NAME,                     // Window class
        L"Learn to Program Windows",    // Window text
        WS_OVERLAPPEDWINDOW,            // Window style

        // Size and position
        CW_USEDEFAULT, CW_USEDEFAULT, 1280, 720,

        NULL,       // Parent window    
        NULL,       // Menu
        hInstance,  // Instance handle
        NULL        // Additional application data
        );

    if (hwnd == NULL)
    {
        return 0;
    }
    WINDOWPLACEMENT wp = {};
    wp.length = sizeof(wp);
    wp.showCmd = 1;
    wp.ptMaxPosition.x = -1;
    wp.ptMaxPosition.y = -1;
    wp.ptMinPosition.x = -1;
    wp.ptMinPosition.y = -1;
    wp.rcNormalPosition.left = 4510;
    wp.rcNormalPosition.top = 2320;
    wp.rcNormalPosition.right = wp.rcNormalPosition.left + 1850;
    wp.rcNormalPosition.bottom = 2909;
    
    ::SetWindowPlacement((HWND)hwnd, &wp);


    ShowWindow(hwnd, nCmdShow);

    // Run the message loop.
    MSG msg = { };
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;

    case WM_DPICHANGED:
    {
        int dpi = HIWORD(wParam);

        {
            RECT* const prcNewWindow = (RECT*)lParam;
            SetWindowPos(hwnd,
                         NULL,
                         prcNewWindow->left,
                         prcNewWindow->top,
                         prcNewWindow->right - prcNewWindow->left,
                         prcNewWindow->bottom - prcNewWindow->top,
                         SWP_NOZORDER | SWP_NOACTIVATE);
        }

        return 0;
    }

    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);

            // All painting occurs here, between BeginPaint and EndPaint.
            FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
            EndPaint(hwnd, &ps);
        }
        return 0;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

And the .manifest I'm using.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
        <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,permonitor</dpiAwareness>
        <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
    </windowsSettings>
</application>
</assembly>
2
Could you please show a minimal, reproducible sample without private information?Song Zhu
Is window visible (shown) when you apply SetWindowPlacement?Daniel Sęk
@DanielSęk I've tried both with and without it being shown, no difference there.mb13
@SongZhu-MSFT Ok, I'll try to pull one together.mb13
The suggested size is incorrect since it's changing the size of the window which was already the correct size for monitor #2. And you can provide some screenshots to illustrate the results you expect and what actually happened, which will help us solve the problem. Thank you for your understanding.Song Zhu

2 Answers

0
votes

There are two ways to solve this:

  1. Before calling SetWindowPlacement, set a flag like s_IsInsideWindowMove to true, and if it's set when WM_DPICHANGED fires, do not follow the suggestion to resize the window. Once SetWindowPlacement returns, set the flag back to false;
  2. Calculate the size passed to SetWindowPlacement as if you were placing it on the monitor with the same DPI as the monitor the window is currently on. For instance, if you're moving the window from DPI 144 to DPI 192 monitor and you want the end result size to be 800x600, ask SetWindowPlacement for a size of 600x450.

We use the first option as it's just easier to reason about and implement.

1
votes

I encountered the same issue in a slightly different context: saving and restoring the positions of other applications' windows (i.e. when un-docking a laptop all windows move to the primary display, but when re-docking we'd like them to go back where they were).

Since I don't control the third-party application windows, neither of @Sunius' suggestions work: I cannot make them ignore WM_DPICHANGED, nor can I know whether they will indeed react to WM_DPICHANGED -- if they aren't actually DPI-aware applications then pre-adjusting the window size is unnecessary and counterproductive.

My solution so far is a little clumsy, but simple and effective: when moving windows between different-DPI monitors, simply call SetWindowPlacement() twice. The first call will put it on the right monitor, but possibly with the wrong size, but the second call will immediately fix the size since it's already in the correct position.

The only catch here is that GetDpiForWindow() appeared to be unreliable for some applications, especially if the window remained minimized (I think Windows internally doesn't update the window's DPI setting for its new monitor if it's minimized). So instead I had to use MonitorFromWindow() and then GetDpiForMonitor() to detect when a window is going to change DPI, in order to trigger the second SetWindowPlacement().