42

I'm working on an app that positions windows on the screen in a grid style. When Running this on Windows 10, there is a huge gap between the windows. Further investigation shows that GetWindowRect is returning unexpected values, including an invisible border, but I can't get it to return the real values with the visible border.

1) This thread suggests this is by design and you can "fix" it by linking with winver=6. My environment does not allow this but I've tried changing the PE MajorOperatingSystemVersion and MajorSubsystemVersion to 6 with no affect

2) That same thread also suggests using DwmGetWindowAttribute with DWMWA_EXTENDED_FRAME_BOUNDS to get the real coordinates from DWM, which works, but means changing everywhere that gets the window coordinates. It also doesn't allow the value to be set, leaving us to reverse the process to be able to set the window size.

3) This question suggests it's lack of the DPI awareness in the process. Neither setting the DPI awareness flag in the manifest, or calling SetProcessDpiAwareness had any result.

4) On a whim, I've also tried adding the Windows Vista, 7, 8, 8.1 and 10 compatibility flags, and the Windows themes manifest with no change.

Screenshot of a "fullscreen" window with gaps all round This window is moved to 0x0, 1280x1024, supposedly to fill the entire screen, and when querying the coordinates back, we get the same values. The window however is actually 14 pixels narrower, to take into account the border on older versions of Windows.

How can I convince Windows to let me work with the real window coordinates?

Community
  • 1
  • 1
Deanna
  • 23,876
  • 7
  • 71
  • 156

5 Answers5

43

Windows 10 has thin invisible borders on left, right, and bottom, it is used to grip the mouse for resizing. The borders might look like this: 7,0,7,7 (left, top, right, bottom)

When you call SetWindowPos to put the window at this coordinates:
0, 0, 1280, 1024

The window will pick those exact coordinates, and GetWindowRect will return the same coordinates. But visually, the window appears to be here:
7, 0, 1273, 1017

You can fool the window and tell it to go here instead:
-7, 0, 1287, 1031

To do that, we get Windows 10 border thickness:

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`

Then offset the rectangle like so:

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`

Unless there is a simpler solution!

Barmak Shemirani
  • 30,904
  • 6
  • 40
  • 77
  • And that's exactly what I didn't want to do, a hack to find out what the offset is and adjust the actual values we use to suit. Thank you though and I may still use this if there is no other alternative. – Deanna Dec 07 '15 at 21:52
  • 6
    This just gets better... This method only works AFTER the window has been shown :( Until then, the DWM function just returns the same as `GetWindowRect`. – Deanna Dec 08 '15 at 11:25
  • 1
    Oh, I didn't think about that. I am just going to delete this answer. I noticed other problems with it. Note that `SetWindowPos` and `GetWindowRect` are working properly. You asked for borders and the system is giving your borders. The only problem is that the borders are invisible in Windows 10, so it appears that the window is in the wrong place. Visual Studio IDE has its own tool-windows, when tool-window is docked, it uses no borders or custom `NC_PAINT`; and when its tool-window is floating, it uses default borders. I think you want something similar? – Barmak Shemirani Dec 08 '15 at 16:37
  • 2
    I'd like windows to let me work with the real, visible rectangle of the window :) This answer is useful so I'd prefer for it not to be deleted though, as it's the only solution so far... – Deanna Dec 08 '15 at 16:39
  • Can someone say if this is working for Windows Vista, 7, 8 and 8.1 also? I would prefer to have a solution without checking for the Windows version. – ConfusedSushi May 31 '16 at 21:21
  • @ConfusedSushi, older window versions don't have transparent borders. There is no telling what the next version of Windows will be. You might just want to allow for the small gap. This is a feature of Windows 10, it's not really a bug. You can use custom-draw caption to make your own visible borders, but in my experience custom-draw caption usually ends up in regret. – Barmak Shemirani Jun 01 '16 at 06:18
  • 7
    Note that `DwmGetWindowAttribute()` returns physical coordinates, but `GetWindowRect()` returns logical coordinates. So the border width will be wrong for a non-DPI aware application on a screen scaled to anything other than 100%. – Ian Goldby Jul 07 '17 at 12:56
  • 2
    In response to @IanGoldby's comment - it's a good shout but in my opinion it is much better advice to always make sure your application is always running as DPI aware if you are making system calls for coordinates. In this case DWM and User32 calls will agree and both return physical coordinates. There are many windows API functions which are inconsistent like this if you do not specify DPI Awareness – caesay Jul 08 '20 at 22:37
  • Another way to get the sizing borders is [this](https://stackoverflow.com/questions/431470/window-border-width-and-height-in-win32-how-do-i-get-it/70545045#70545045), the value retrieved will no doubt be a combination of any other border styles used for the window. – Laurie Stearn Jan 01 '22 at 12:25
4

How can I convince Windows to let me work with the real window coordinates?

You are already working with the real coordinates. Windows10 has simply chosen to hide the borders from your eyes. But nonetheless they are still there. Mousing past the edges of the window, your cursor will change to the resizing cursor, meaning that its still actually over the window.

If you want your eyes to match what Windows is telling you, you could try exposing those borders so that they are visible again, using the Aero Lite theme:

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

mikew
  • 1,634
  • 2
  • 15
  • 25
  • 3
    That's the conclusion I came to. Changing the theme however is not an option as this is a commercially available application. Forcing a theme on end users is generally considered bad practice :-) – Deanna May 23 '16 at 17:25
  • 2
    I agree changing the theme is a bad idea and something that I don't require of my users. But they still complain about the gaps :( – mikew May 28 '16 at 17:50
0

AdjustWindowRectEx (or on Windows 10 and later AdjustWindowRectExForDpi) might be of use. These functions will convert a client rectangle into a window size.

I'm guessing you don't want to overlap the borders though, so this probably isn't a full solution--but it may be part of the solution and may be useful to other people coming across this question.

Here's a quick snippet from my codebase where I've successfully used these to set the window size to get a desired client size, pardon the error handling macros:

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
));

There are still two ambiguities in the above use case:

  • My window does have a menu, but I have to pass in false for the menu param or I get the wrong size out. I'll update this answer with an explanation if I figure out why this is!
  • I haven't yet read about how Windows handles DPI awareness so I'm not sure when you want to use that function vs the non DPI aware one
Mason
  • 33
  • 1
  • 5
0

I discovered a new solution for those who need logical coordinates like how GetWindowRect returns but with correct values (without including the invisible border). Using DwmGetWindowAttribute with DWMWA_EXTENDED_FRAME_BOUNDS will give the correct value but in nonlogical coordinates, and this becomes a problem in some cases (such as where having multiple monitors with different DPI configurations)

The new solution is to use the answer from DwmGetWindowAttribute (with DWMWA_EXTENDED_FRAME_BOUNDS) and then fix the answer so it will be in logical units (like how GetWindowRect returns), using some smart math tricks, with help from another two API calls (to get additional information for the cool math)

The solution is inspired by my last discovery from Get real screen resolution using Win32 API

Here is a code of the method that will give you the correct value without invisible borders and in logical coordinates:

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
}

It was very hard to figure it out. Took me years. This is like having GetWindowRect function but without the problem that it returns a rectangle with offset from invisible borders.

gil123
  • 512
  • 6
  • 12
-1

You can respond to the WM_NCCALCSIZE message, modify WndProc's default behaviour to remove the invisible border.

As this document and this document explain, when wParam > 0, On request wParam.Rgrc[0] contains the new coordinates of the window and when the procedure returns, Response wParam.Rgrc[0] contains the coordinates of the new client rectangle.

The golang code sample:

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
    }
Deanna
  • 23,876
  • 7
  • 71
  • 156
xjdrew
  • 373
  • 4
  • 13