SetWindowPlacement 触发的 WM_DPICHANGED 不正确

问题描述

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

窗口在 SetwindowPlacement 期间首先调整大小,同时它仍然在监视器 #1 上。然后将窗口移动到监视器#2,这会导致触发 WM_DPICHANGED 消息,表示应该更改窗口大小。建议的大小不正确,因为它正在更改窗口的大小,而该大小已经是监视器 #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,0))
    {
        TranslateMessage(&msg);
        dispatchMessage(&msg);
    }

    return 0;
}

LRESULT CALLBACK WindowProc(HWND hwnd,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,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>

解决方法

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

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

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

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

,

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

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

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

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...