The official blog of the Windows Presentation Foundation Team
For those targeting .NET 3.5, you will notice that RibbonWindow’s Height, Width, Left, and Top properties are a little funky and can be misreported. This article explains why RibbonWindow on .NET 3.5 has these issues, how the issues manifest, and how to work around them. .NET 4.0 has no known issues with respect to RibbonWindow dimensions. Window dimensions are a fragile area and trying to fix this at the RibbonWindow level leads to side effects, so our recommendation is to target .NET 4.0 for Ribbon apps or else use the simple workarounds described in this article.
RibbonWindow derives from Window. In .NET 3.5, there was an assumption that dimensions would be consistent for all native window instances of a given native style. This was fine for normal WPF Windows but is invalid for windows like RibbonWindow that use WindowChrome because these windows do not have the same margins between the client and non-client RECTs:
Due to this assumption, RibbonWindow.Left/Height/Width/Height get reported incorrectly and have side effects when set programmatically:
In this screenshot, ActualWidth/ActualHeight/EffectiveLeft/EffectiveTop show the correct dimensions of the RibbonWindow. Width/Height/Left/Top are reported incorrectly. So where do these deltas in value come from? Simple: they are the differences between the client RECT and the window RECT for a normal native window of a given native style (the gray shaded area in the first picture).
These exact dimensions can be determined from SystemParamaters2.WindowNonClientFrameThickness. In the screen capture, we are running under the “Classic” theme, which reports the Thickness 5, 29, 5, 5 (left, top, right, bottom) for WindowNonClientFrameThickness. RibbonWindow.Width is reported 10 pixels too large because it assumes it should add the system default 5 pixels for the left non-client edge and 5 pixels for the right non-client edge, when in reality RibbonWindow’s non-client frame has Thickness 0,0,0,0. If you look at the other dimensions here and then at SystemParameters2.WindowNonClientFrameThickness for the system defaults, it makes sense why these dimension parameters are reported the way they are for RibbonWindow on .NET 3.5.
.NET 4.0 calls GetWindowRect for the real HWND instead of some system default, and thus reports and sets Height/Width/Left/Top correctly.
For RibbonWindow on .NET 3.5, use ActualWidth and ActualHeight to read the width and height values of the RibbonWindow. These values come from UIElement.RenderSize, so RibbonWindow’s abnormal lack of non-client frame does not matter.
For reading left and top values, calculate the EffectiveLeft and EffectiveTop values. These were shipped as APIs in Ribbon RTM (July 2010), but have been removed from the Ribbon October 2010 update based on feedback. We want folks to eventually move to .NET 4.0 and beyond to take advantage of fixes for issues like this. For those still targeting .NET 3.5, it only takes a few lines of code to add EffectiveLeft and EffectiveTop properties to your window class. The attached sample project contains code for this, but in essence you just need to add WindowNonClientFrameThickness.Left to RibbonWindow.Left and add WindowNonClientFrameThickness.Top to RibbonWindow.Top when reporting Left and Top dimensions, respectively.
To effectively set RibbonWindow dimensions, we need to use interop. If you try to set dimension properties directly then you will see weird side effects. For example, setting RibbonWindow.Width will make RibbonWindow.Height grow programmatically on .NET 3.5. As another manifestation, setting RibbonWindow.Left will shrink RibbonWindow.Top by a few pixels. Here again, the degree of transformation comes from SystemParameters2.WindowNonClientFrameThickness due to assumptions about the non-client frame.
It’s actually quite easy to work around this, but we need to use a few lines of interop. SetWindowPos is the Win32 function we need to programmatically set both position and size of our RibbonWindow. When setting width or height, first set the properties directly and then call SetWindowPos with the resultant values:
// set height to 400
this.Height = 400;
IntPtr hwnd = new WindowInteropHelper(this).Handle;
NativeMethods.SetWindowPos(hwnd, IntPtr.Zero, (int)this.EffectiveLeft, (int)this.EffectiveTop, (int)this.Width, (int)this.Height, SWP.NOZORDER | SWP.NOACTIVATE);
When setting left or top, just call SetWindowPos directly:
// set left to 300
NativeMethods.SetWindowPos(hwnd, IntPtr.Zero, 300, (int)this.EffectiveTop, (int)Width, (int)Height, SWP.NOZORDER | SWP.NOACTIVATE);
Note that with these changes, RibbonWindow.Width and .Height are reported the same as .ActualWidth and .ActualHeight. This code (and some test code to drive it) is contained in the attached sample project.