GetWindowRect返回包括“隐形”边框在内的大小

43

我正在开发一个将窗口以网格形式放置在屏幕上的应用程序。当在Windows 10上运行时,窗口之间存在巨大的间隙。进一步调查显示GetWindowRect返回意外的值,包括一个不可见的边框,但我无法让它返回带有可见边框的真实值。

1)这个线程表明这是设计如此,并且可以通过与winver = 6链接来“修复”它。我的环境不允许这样做,但我已经尝试更改PE MajorOperatingSystemVersionMajorSubsystemVersion为6,但没有影响。

2) 同一线程还建议使用DwmGetWindowAttributeDWMWA_EXTENDED_FRAME_BOUNDS从DWM获取真实坐标,这很有效,但需要更改获取窗口坐标的所有位置。它也不允许设置值,因此我们需要反向该过程以便能够设置窗口大小。

3) 这个问题表明它是进程中缺少DPI感知度的原因。无论在清单中设置DPI感知度标志,还是调用SetProcessDpiAwareness都没有产生任何结果。

4) 凭直觉,我还尝试添加Windows Vista、7、8、8.1和10兼容性标志以及Windows主题清单,但没有任何变化。

全屏窗口截图,周围有间隙

这个窗口被移动到0x0, 1280x1024, 假设填满了整个屏幕,当我们查询坐标时,得到相同的值。 然而,实际上这个窗口比较旧版本的Windows中的边框要窄14个像素。

我该如何说服Windows让我使用真实的窗口坐标?

5个回答

44

Windows 10 左侧、右侧和底部有细小的不可见边框,用于调整窗口大小。这些边框看起来可能是这样的:7,0,7,7 (左,上,右,下)

当您调用 SetWindowPos 将窗口放置在以下坐标时:
0, 0, 1280, 1024

窗口将选择这些确切的坐标,并且 GetWindowRect 将返回相同的坐标。但在视觉上,窗口似乎在这里出现:
7, 0, 1273, 1017

您可以欺骗窗口告诉它去这里:
-7, 0, 1287, 1031

为此,我们获得了 Windows 10 边框的厚度:

RECT rect, frame;
GetWindowRect(hwnd, &rect);
DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frame, sizeof(RECT));

//rect should be `0, 0, 1280, 1024`
//frame should be `7, 0, 1273, 1017`

RECT border;
border.left = frame.left - rect.left;
border.top = frame.top - rect.top;
border.right = rect.right - frame.right;
border.bottom = rect.bottom - frame.bottom;

//border should be `7, 0, 7, 7`

然后像这样偏移矩形:

rect.left -= border.left;
rect.top -= border.top;
rect.right += border.left + border.right;
rect.bottom += border.top + border.bottom;

//new rect should be `-7, 0, 1287, 1031`

除非有更简单的解决方案!


6
越来越好了...这种方法只在窗口显示后才有效 :( 在那之前,DWM函数返回的结果与GetWindowRect一样。 - Deanna
1
哦,我没有考虑到这一点。我要删除这个答案。我发现其他问题。请注意,SetWindowPosGetWindowRect的功能是正常的。您要求边框,系统正在提供边框。唯一的问题是,在Windows 10中,边框是看不见的,所以窗口似乎在错误的位置。Visual Studio IDE有自己的工具窗口,当工具窗口被停靠时,它不使用边框或自定义的NC_PAINT;当其工具窗口浮动时,它使用默认边框。我想你想要类似的东西? - Barmak Shemirani
2
我希望Windows让我使用窗口的真实可见矩形来工作 :) 这个答案很有用,所以我希望它不会被删除,因为这是目前唯一的解决方案... - Deanna
7
请注意,DwmGetWindowAttribute() 返回物理坐标,而 GetWindowRect() 返回逻辑坐标。因此,在缩放比例不为100%的屏幕上,对于不支持 DPI 的应用程序,边框宽度将会出现错误。 - Ian Goldby
2
针对@IanGoldby的评论 - 这是一个好主意,但在我看来,如果您正在进行系统调用以获取坐标,则始终确保应用程序始终运行为DPI感知性会更好。 在这种情况下,DWM和User32调用将达成一致并返回物理坐标。 如果您不指定DPI感知性,则有许多Windows API函数是不一致的。 - caesay
显示剩余4条评论

4

我该如何说服Windows让我使用真实的窗口坐标?

您已经在使用真实的坐标。Windows 10只是选择隐藏窗口边框,但它们仍然存在。当鼠标穿过窗口边缘时,光标将变为调整大小的光标,这意味着它仍然位于窗口上方。

如果您想让您的眼睛与Windows所告诉您的匹配,您可以尝试使用Aero Lite主题重新显示这些边框,使它们再次可见:

http://winaero.com/blog/enable-the-hidden-aero-lite-theme-in-windows-10/


3
这是我得出的结论。然而,改变主题不是一个选项,因为这是一款商业可用的应用程序。强制给最终用户使用主题通常被认为是一种不好的做法 :-) - Deanna
2
我同意更改主题是一个坏主意,也不是我要求我的用户做的事情。但他们仍然抱怨有间隙 :( - mikew

1
我发现了一个新的解决方案,适用于那些需要类似于GetWindowRect返回的逻辑坐标,但是带有正确的值(不包括不可见边框)的人。 使用DwmGetWindowAttributeDWMWA_EXTENDED_FRAME_BOUNDS将给出正确的值,但是以非逻辑坐标表示,这在某些情况下会成为问题(例如,在具有不同DPI配置的多个监视器上)。
新的解决方案是使用DwmGetWindowAttribute(带有DWMWA_EXTENDED_FRAME_BOUNDS)的答案,然后修复答案,使其以逻辑单位表示(就像GetWindowRect返回的方式),使用一些巧妙的数学技巧,并借助另外两个API调用(以获取有关这种酷炫数学的其他信息)。
该解决方案受到我最近从使用Win32 API获取真实屏幕分辨率的发现的启发。
以下是一个方法的代码,它将为您提供正确的值,没有不可见边框,并且以逻辑坐标表示:
bool GetWindowRectNoInvisibleBorders(HWND hWnd, RECT* rect)
{
    // Get the physical coordinates of the window (this is without the additional offsets)
    RECT dwmRect;
    HRESULT hresult = DwmGetWindowAttribute(hWnd, DWMWA_EXTENDED_FRAME_BOUNDS, &dwmRect, sizeof(RECT));
    // Return false in such case. error handling was done only here because after this check, the rest of the code
    // use the API correctly so error is unlikely 
    if (hresult != S_OK)
        return false;

    // Get information from the monitor where the window located.
    // We need it for getting its RECT in logical coordinates (rcMonitor.*)
    HMONITOR monitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);
    MONITORINFOEX monInfo;
    monInfo.cbSize = sizeof(MONITORINFOEX);
    GetMonitorInfo(monitor, &monInfo);

    // Get additional information from this monitor. we need it for getting the physical
    // coordinates of its position (dmPosition.x and dmPosition.y) and its physical coordinates
    // of its size (dmPelsWidth)
    DEVMODE monDeviceConfig;
    monDeviceConfig.dmSize = sizeof(DEVMODE);
    EnumDisplaySettings(monInfo.szDevice, ENUM_CURRENT_SETTINGS, &monDeviceConfig);

    // Calculate the ratio between the logical size and the physical size of the monitor (part of math to handle DPI changes)
    auto scalingRatio = (monInfo.rcMonitor.right - monInfo.rcMonitor.left) / (double)monDeviceConfig.dmPelsWidth;

    // Calculate the final answer in logical coordinates
    rect->left = (dwmRect.left - monDeviceConfig.dmPosition.x) * scalingRatio + monInfo.rcMonitor.left;
    rect->right = (dwmRect.right - monDeviceConfig.dmPosition.x) * scalingRatio + monInfo.rcMonitor.left;
    rect->top = (dwmRect.top - monDeviceConfig.dmPosition.y) * scalingRatio + monInfo.rcMonitor.top;
    rect->bottom = (dwmRect.bottom - monDeviceConfig.dmPosition.y) * scalingRatio + monInfo.rcMonitor.top;

    return true; // success
}


这真的很难弄清楚。花了我好几年的时间。 这就像有一个GetWindowRect函数,但没有返回一个带有不可见边界偏移的矩形的问题。

0

AdjustWindowRectEx(或在Windows 10及更高版本上使用{{link2:AdjustWindowRectExForDpi}})可能会有用。 这些函数将把客户区矩形转换为窗口尺寸。

我猜你不想重叠边框,所以这可能不是完整的解决方案——但它可能是解决方案的一部分,并且对于其他遇到这个问题的人可能有用。

以下是我的代码库中的一个快速片段,我已成功使用它们将窗口大小设置为所需的客户端大小,请原谅错误处理宏:

DWORD window_style = (DWORD)GetWindowLong(global_context->window, GWL_STYLE);
CHECK_CODE(window_style);
CHECK(window_style != WS_OVERLAPPED); // Required by AdjustWindowRectEx

DWORD window_style_ex = (DWORD)GetWindowLong(global_context->window, GWL_EXSTYLE);
CHECK_CODE(window_style_ex);

// XXX: Use DPI aware version?
RECT requested_size = {};
requested_size.right = width;
requested_size.bottom = height;
AdjustWindowRectEx(
    &requested_size,
    window_style,
    false, // XXX: Why always false here?
    window_style_ex
);

UINT set_window_pos_flags = SWP_NOACTIVATE | SWP_NOCOPYBITS | SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOZORDER;
CHECK_CODE(SetWindowPos(
    global_context->window,
    nullptr,
    0,
    0,
    requested_size.right - requested_size.left,
    requested_size.bottom - requested_size.top,
    set_window_pos_flags
));

上述用例中仍存在两个不明确的地方:

  • 我的窗口确实有一个菜单,但如果我将menu参数设置为false,则会得到错误的大小。如果我弄清楚原因,我会更新这个答案并解释一下!
  • 我还没有阅读关于Windows如何处理DPI感知的内容,所以我不确定何时使用该函数与非DPI感知函数。

-1

您可以响应 WM_NCCALCSIZE 消息,修改 WndProc 的默认行为以去除不可见边框。

正如 本文档本文档 所解释的那样,当 wParam > 0 时,在请求中 wParam.Rgrc[0] 包含窗口的新坐标,当过程返回时,响应 wParam.Rgrc[0] 包含新客户端矩形的坐标。

Golang 代码示例:

case win.WM_NCCALCSIZE:
    log.Println("----------------- WM_NCCALCSIZE:", wParam, lParam)

    if wParam > 0 {
        params := (*win.NCCALCSIZE_PARAMS)(unsafe.Pointer(lParam))
        params.Rgrc[0].Top = params.Rgrc[2].Top
        params.Rgrc[0].Left = params.Rgrc[0].Left + 1
        params.Rgrc[0].Bottom = params.Rgrc[0].Bottom - 1
        params.Rgrc[0].Right = params.Rgrc[0].Right - 1
        return 0x0300
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接