SetWindowPlacement 触发的 WM_DPICHANGED 不正确

Incorrect WM_DPICHANGED triggered by SetWindowPlacement

我 saving/restoring 我的 window 位置使用 GetWindowPlacement/SetWindowPlacement 在 Windows 10。我的应用程序是 DPI 感知的。当 SetWindowPlacement 正在调整大小并将 window 从具有一个 DPI 的监视器 #1 移动到具有不同 DPI 的监视器 #2 时,会出现此问题。坐标已保存为 WINDOWPLACEMENT 结构中监视器 #2 的正确大小。

window 在 SetWindowPlacement 期间首次调整大小,同时它仍在监视器 #1 上。然后 window 被移动到监视器 #2,这导致触发 WM_DPICHANGED 消息,说 window 大小应该更改。建议的大小不正确,因为它正在更改 window 的大小,这已经是监视器 #2 的正确大小。 解决这个问题的正确方法是什么?我是否应该在 SetWindowPlacement 之前设置一个标志以忽略 WM_DPICHANGED 消息,直到该调用完成?在某些情况下,这会导致我错过一条我不应该忽略的消息吗? 谢谢

编辑:@SongZhu-MSFT 的附加复制。 在此测试案例中,我使用 Surface Studio 2 作为我的主显示器,运行 分辨率为 4500x3000,缩放比例为 175%。在该显示器的右侧,与底部对齐的是设置为 100% 缩放比例的 1920x1080 显示器。此代码尝试以设置的大小在右侧监视器上打开监视器,但是在 SetWindowPlacement() 调用期间会出现 DPICHANGE 消息,这会导致大小调整不正确,除非我手动避免它。示例代码编辑自: 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);
}

还有我正在使用的 .manifest。

<?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>

有两种方法可以解决这个问题:

  1. 在调用 SetWindowPlacement 之前,将 s_IsInsideWindowMove 之类的标志设置为 true,如果在 WM_DPICHANGED 触发时设置,请不要按照建议调整 window。一旦 SetWindowPlacement returns,将标志设置回 false
  2. 计算传递给 SetWindowPlacement 的大小,就好像您将它放在与 window 当前打开的显示器具有相同 DPI 的显示器上一样。例如,如果您将 window 从 DPI 144 移动到 DPI 192 显示器并且您希望最终结果大小为 800x600,请询问 SetWindowPlacement尺寸为 600x450。

我们使用第一个选项,因为它更容易推理和实施。

我在略有不同的上下文中遇到了同样的问题:保存和恢复 其他 应用程序的 windows 的位置(即当取消对接笔记本电脑时所有 windows 移动到主显示器,但在重新对接时我们希望它们回到原来的位置。

由于我不控制第三方应用程序 windows,@Sunius 的建议都不起作用:我不能让他们忽略 WM_DPICHANGED,我也不知道他们是否真的会做出反应到 WM_DPICHANGED -- 如果它们实际上不是 DPI 感知应用程序,那么预先调整 window 大小是不必要的并且会适得其反。

到目前为止,我的解决方案有点笨拙,但简单有效:在不同 DPI 显示器之间移动 windows 时,只需调用 SetWindowPlacement() 两次。第一次调用会将它放在正确的显示器上,但可能尺寸错误,但第二次调用会立即修复尺寸,因为它已经在正确的位置。

这里唯一的问题是 GetDpiForWindow() 对于某些应用程序似乎不可靠,特别是如果 window 保持最小化(我认为 Windows 内部不会更新 window 的新显示器的 DPI 设置(如果最小化)。因此,我不得不使用 MonitorFromWindow() 然后 GetDpiForMonitor() 来检测 window 何时会更改 DPI,以便触发第二个 SetWindowPlacement().