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>
有两种方法可以解决这个问题:
- 在调用
SetWindowPlacement
之前,将 s_IsInsideWindowMove
之类的标志设置为 true
,如果在 WM_DPICHANGED
触发时设置,请不要按照建议调整 window。一旦 SetWindowPlacement
returns,将标志设置回 false
;
- 计算传递给
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()
.
我 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>
有两种方法可以解决这个问题:
- 在调用
SetWindowPlacement
之前,将s_IsInsideWindowMove
之类的标志设置为true
,如果在WM_DPICHANGED
触发时设置,请不要按照建议调整 window。一旦SetWindowPlacement
returns,将标志设置回false
; - 计算传递给
SetWindowPlacement
的大小,就好像您将它放在与 window 当前打开的显示器具有相同 DPI 的显示器上一样。例如,如果您将 window 从 DPI144
移动到 DPI192
显示器并且您希望最终结果大小为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()
.