How do I compute the non-client window size in WPF?

10,150

Solution 1

For a resizable window you need to use a different set of parameters to compute the size:

var titleHeight = SystemParameters.WindowCaptionHeight
  + SystemParameters.ResizeFrameHorizontalBorderHeight;
var verticalBorderWidth = SystemParameters.ResizeFrameVerticalBorderWidth;

These sizes will change when you modify the theme.

Solution 2

I'm pretty sure that the GetSystemMetrics function (which the SystemParameters class calls internally with the appropriate arguments) is returning the correct values for your system, it's just returning the correct values in the case whether the Aero theme is disabled. By turning on Aero, you get beefier borders and taller window captions, all the name of juicy graphical goodness.

If you want to get the correct size of these window elements, regardless of the user's current theme (remember, you can run Windows Vista and beyond with the Classic theme, the Aero Basic theme, or the full Aero theme, all of which are going to have different-sized UI elements), you need to use a different method available in Vista and later.

You need to send the window a WM_GETTITLEBARINFOEX message in order to request extended title bar information. The wParam is unused and should be zero. The lParam contains a pointer to a TITLEBARINFOEX structure that will receive all of the information. The caller is responsible for allocating memory for this structure and setting its cbSize member.

To do all of this from a .NET application, you'll obviously need to do some P/Invoke. Start by defining the constants you need, as well as the TITLEBARINFOEX structure:

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

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

Then define the SendMessage function accordingly:

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

And finally, you can call all of that mess using something like the following code:

internal static TITLEBARINFOEX GetTitleBarInfoEx(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);

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

EDIT: Now tested and working on my notebook running Windows 7.

Share:
10,150
Martin Liversage
Author by

Martin Liversage

Software developer specializing in everything .NET.

Updated on June 02, 2022

Comments

  • Martin Liversage
    Martin Liversage almost 2 years

    WPF has the SystemParameters class that exposes a great number of system metrics. On my computer I have noticed that a normal window has a title that is 30 pixels high and a border that is 8 pixels wide. This is on Windows 7 with the Aero theme enabled:

    Non-client area - Aero

    However, SystemParameters return the following values:

    SystemParameters.BorderWidth = 5
    SystemParameters.CaptionHeight = 21
    

    Here I have disabled the Aero theme:

    Non-client area - classic

    Now, SystemParameters return the following values:

    SystemParameters.BorderWidth = 1
    SystemParameters.CaptionHeight = 18
    

    How do I compute the actual observed values by using SystemParameters?

  • Cody Gray
    Cody Gray almost 13 years
    Yeah, adding 8 is a hack I've seen before to make these values match the expected values for the Aero theme. But it's hardly a foolproof approach. Window themes are something that Microsoft is apparently quite fond of re-inventing, and things fall apart even under the current system unless you specifically check which theme the user is currently running—Classic, Aero Basic, or Aero.
  • Tim
    Tim almost 13 years
    I'm not really looking at that as being a hack in this case. The caption is 22 pixels, and the border is 8 pixels on all four sides. that gives you the correct values. This works when you change the theme - it returns the correct values for Classic, Aero Basic and Aero.
  • Cody Gray
    Cody Gray almost 13 years
    He's explicitly asking about the non-client area. Most of your answer and the linked post are about the client area. It's pretty silly to determine the NC area just to calculate the dimensions of the client area when dtermining the dimensions of the latter is much simpler.
  • Martin Liversage
    Martin Liversage almost 13 years
    You have provided the correct answer but I have taken the liberty to edit it to more clearly show what the answer is by removing the "edit history".
  • Tim
    Tim almost 13 years
    @Martin Liversage: Sounds good. Now that it's "settled" that makes a lot of sense. Thanks!
  • sees
    sees about 11 years
    Can anyone tell me what event occurs when you change the theme? I want to change max, min of window size
  • sees
    sees about 11 years
    It seems that here is the answer: detect-system-theme-change-in-wpf
  • sees
    sees about 11 years
    these titleheight, verticalBorderWith is not correct after some theme is changed. For example: "Windows classic" to "Windows 7 Basic" when your application is still running. I tried in WM_THEMECHANGED event
  • sees
    sees about 11 years
    The event SystemEvents.UserPreferenceChanged also does the trick. FYI: UserPreferenceChanged(in Japaense)
  • Samvel Siradeghyan
    Samvel Siradeghyan about 10 years
    In my case screen with is 1920px, maximized window ActualWidth is 1936, 8px border with, but SystemParameters.ResizeFrameVerticalBorderWidth returns 4. @sees link worked for me. I'm taking ActualWidth of Windows' content, which is 1920.
  • kol
    kol over 9 years
    On Windows 8.1, ResizeFrameVerticalBorderWidth gives me 4, but the real border width is 7.
  • Erti-Chris Eelmaa
    Erti-Chris Eelmaa over 9 years
    Seems to work. Small warning when using this code in WPF, as this code expects windows forms Rectangle, not WPF Rectangle - took me some time to figure it out, otherwise you're not going to get results.
  • yyny
    yyny almost 9 years
    @kol SystemParameters.ResizeFrameVerticalBorderWidth + SystemParameters.FixedFrameVerticalBorderWidth // + SystemParameters.BorderWidth gives me the right value (I got 8 with java, but im not sure if its 8 or 7, so idk if BorderWidth matters, I think its the inner gray line.
  • PMF
    PMF almost 8 years
    @YoYoYonnY: It seems 8 is right, so the above code with "+SystemParameters.BorderWidth" gets the right result on Win7 Aero. If possible, I'll test what happens on Win8.1 and Win10.
  • Nicke Manarin
    Nicke Manarin over 6 years
    WindowNonClientFrameThickness already contains WindowResizeBorderThickness internally. The only difference is that the former gets the height of the caption as well.
  • Victor Zakharov
    Victor Zakharov about 4 years
    This work up to windows 8.1, but does not work on windows 10, do you know how to fix? I posted a new question here - stackoverflow.com/questions/60806098/…
  • caesay
    caesay almost 2 years
    AdjustWindowRectEx is an awesome find. The provided code needs a little help but it put me on the right track. Thanks!