2

enter image description here

My initial approach to this was using GetSystemMetrics with SystemMetric.SM_CXSIZE and some simple math based on which buttons are available (times 3, or times 1), via WindowStyle.

[DllImport("user32.dll")]
private static extern int GetSystemMetrics(SystemMetric smIndex);

This has an issue on Windows 10, where the calculated width is approximately 70% of actual. So the width covers just two buttons - maximize and close. Windows 7 and 8.1 are fine, same DPI setting, where it covers all buttons.

I checked a few existing questions on Stack Overflow, and had most success with this one from 2011:

Unfortunately, while the suggested approach does work in windows 8.1, it calculates 0 on Windows 10 (latest version, all recommended updates). Is there a way that works on all OS from 7 to 10?

Code was taken from the above answer and modified to calculate total width of window's control buttons, by window handle (hwnd), and changed marshalling to RECT from Rectangle (then I get correct values of left/right).

public static int GetControlButtonsWidth(IntPtr hwnd)
{
    // Create and initialize the structure
    TITLEBARINFOEX tbi = new TITLEBARINFOEX();
    tbi.cbSize = Marshal.SizeOf(typeof(TITLEBARINFOEX));

    // Send the WM_GETTITLEBARINFOEX message
    SendMessage(hwnd, WM_GETTITLEBARINFOEX, IntPtr.Zero, ref tbi);

    int sum = tbi.rgrect.Sum(r => r.right - r.left);

    // Return the filled-in structure
    return sum;
}

internal const int WM_GETTITLEBARINFOEX = 0x033F;
internal const int CCHILDREN_TITLEBAR = 5;

[StructLayout(LayoutKind.Sequential)]
internal struct TITLEBARINFOEX
{
    public int cbSize;
    public RECT rcTitleBar;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = CCHILDREN_TITLEBAR + 1)]
    public int[] rgstate;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = CCHILDREN_TITLEBAR + 1)]
    public RECT[] rgrect;
}

[DllImport("user32.dll", CharSet = CharSet.Auto)]
internal static extern IntPtr SendMessage(
        IntPtr hWnd,
        int uMsg,
        IntPtr wParam,
        ref TITLEBARINFOEX lParam);

[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
    public int left, top, right, bottom;
}
Victor Zakharov
  • 25,801
  • 18
  • 85
  • 151
  • According to the document [`TITLEBARINFOEX`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-titlebarinfoex) the structure needs `RECT` . – Drake Wu Mar 23 '20 at 01:55
  • That measure can be quite aleatory. You can get a *sound* value from a Win32 or a standard WinForm Window (which doesn't use DWM Composition). Other Windows, you don't know what measure you get. For example, the Chrome WebBrowser: it doesn't have a TitleBar. Or, it does have one, but it's squeezed in the upper-left corner of the main frame and it doesn't contain those buttons. The Min/Max/Close button are created elsewhere (a child panel of the upper band). The same applies to many other apps. The actual Window is composited. But you can get those buttons with UIAutomation. – Jimi Mar 23 '20 at 07:04
  • @DrakeWu-MSFT Thanks, I changed to RECT shortly after writing this question. – Victor Zakharov Mar 23 '20 at 11:40
  • @Jimi It currently works on my chrome, the button is misplaced by a few pixels relative to the middle of the minimize button, but a few pixels error left or right is acceptable for my use case. This is to fix open source software on github, and slightly improve usability. – Victor Zakharov Mar 23 '20 at 11:41
  • 1
    Yes, that's because you send a `WM_GETTITLEBARINFOEX` to the Window, which is responsible to answer back a value. So the Window will return a value *compatible* with the layout of a standard Caption (where those buttons occupy a specific position - DWM does ~the same thing). The measure returned is *relatively* precise: the Buttons are not actually *there*. Of course, I don't know what you're using this information for, so my comment may just come in handy if something doesn't *add up*, at some point, for any reason (maybe because you need to access those elements). – Jimi Mar 23 '20 at 13:17

1 Answers1

2

You can use DwmGetWindowAttribute, the combined width for those 3 buttons should be 185 pixels on Windows 10, at 125% DPI. Note that if your application is not DPI aware, then the result will still be the same, 185 for example.

[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
    public int left;
    public int top;
    public int right;
    public int bottom;
}

[DllImport("dwmapi.dll")]
public static extern int DwmGetWindowAttribute(
    IntPtr hwnd, int attr, out RECT ptr, int size);

public void foo()
{
    int DWMWA_CAPTION_BUTTON_BOUNDS = 5;
    RECT rc;
    if (0 != DwmGetWindowAttribute(this.Handle, DWMWA_CAPTION_BUTTON_BOUNDS,
        out rc, Marshal.SizeOf(typeof(RECT))))
    {
        //error
    }
    int width = rc.right - rc.left;
}
Barmak Shemirani
  • 30,904
  • 6
  • 40
  • 77
  • It worked both on win 8.1 and 10, also with 150% DPI. Big thanks! – Victor Zakharov Mar 23 '20 at 11:55
  • 1
    Just as a side note, if the code is running on W10, the client area must have the transparent resize borders correctly placed, otherwise the caption button's `RECT` will be shifted to the left by the border's thickness. – Arush Agarampur Mar 23 '20 at 21:10