WPF RibbonWindow + Ribbon = Title outside screen?

21,184

Solution 1

The real problem

Under the hood, the WindowChrome class binds its ResizeBorderThickness to SystemParameters.WindowResizeBorderThickness which in turns uses the Win32 API GetSystemMetrics to determine the system border size.

However, the behavior of this method changes depending of the subsystem version set in the executable PE header. If compiled only for Windows Vista and later (version >= 6.0), it will return thinner borders than if compiled for older operating systems. More information on this in this SO answer.

When compiling against .NET 4.5, the C# compiler sets this version to 6.0 since .NET 4.5 cannot be used on XP. However, the WindowChrome class seems to rely on the legacy behavior and thus fails to calculate the glass size correctly on Windows Vista and 7.

Solutions

Use .NET 4

You can compile against .NET 4 to force the compiler to use 4.0 as its subsystem version value. The ribbon is available for WPF 4 as a separate download. Note that even with this solution, you should uncheck "Enable the Visual Studio hosting process" in the project properties for debugging purposes. Otherwise, the vshost.exe process will be used, which is flagged with a subsystem version of 6.0.

Change the subsystem version

Edit: Olly provided a way to do this in the comments:

Add a property in the project file <subsystemversion>5.01</subsystemversion> that falsely indicates that the code can run on Windows XP.

Ignore the system

You can change the WindowChrome.WindowChrome attached property on your window and use the values that you want, thus completely ignoring the system values. You should never do that, but you can.

Fill a bug

There is an existing bug on Connect about the change in behavior of GetSystemMetrics but it all comes down to the subsystem version so it's rather a feature from a Microsoft view point. However, the WindowChrome class should really be fixed to work correctly under Vista/7, especially since it's now built in .NET 4.5.

Solution 2

Here is another WorkAround, very easy and simple way. Simply add a negative margin at the toolbar. You need to keep the original Window Class and not the RibbonWindow !

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Application Name" Height="350" Width="525" Loaded="Window_Loaded" SizeChanged="Window_SizeChanged">

Just add this margin to the Ribbon Title

<Ribbon Title="" Foreground="#333333" Margin="0,-22,0,0">

Now when you maximize the window, everything stay right

Solution 3

For anyone who reads this question, I'm answering it myself. Forget about the horrible bundled ribbon control and use something else. Look for some of the alternatives here: What is the Best WPF Ribbon Control Suite? (like all good questions, it's closed though).

So far, Fluent Ribbon Control Suite looks like the best free option to me. Basic functionality just works (no problems with borders and maximizing, resising window isn't slow as hell etc.). It has Office styles and preserves them if glass is disabled (that means you won't see Windows9x-ish window in Metro). Its interface (backstage, QAT) is more like Office 2010.

Maybe in some distant future, Microsoft will fix its Ribbon, but for now, look for alternatives.

Solution 4

I had the same Problem with the title in the RibbonWindow. I solved it by setting the global style of the TextBlock within the RibbonTitlePanel.

    <Style TargetType="{x:Type TextBlock}"> 
    <Style.Triggers>
        <MultiDataTrigger>
            <MultiDataTrigger.Conditions>
                <Condition Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type primitives:RibbonTitlePanel}},Path=Visibility}" Value="Visible"></Condition>
                <Condition Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type RibbonWindow}},Path=WindowState}" Value="Maximized"></Condition>
            </MultiDataTrigger.Conditions>
            <MultiDataTrigger.Setters>
                <Setter Property="VerticalAlignment" Value="Center"></Setter>
            </MultiDataTrigger.Setters>
        </MultiDataTrigger>
    </Style.Triggers>
</Style>

Solution 5

This is not a solution, perhaps not even a work around, but rather a poorly hack, that I hope to only use for a short time until the problem is fixed in the framework.

Code is mainly copy+paste from this question https://stackoverflow.com/a/8082816/44726

I've changed the allowed screen position, which seems to help the problem, not fix it.

Call is like this in the code behind

        InitializeComponent();
        RibbonWindowService.FixMaximizedWindowTitle(this);


public static class RibbonWindowService
{
    public static void FixMaximizedWindowTitle(Window window)
    {
        window.SourceInitialized += WinSourceInitialized;
    }

    [DllImport("user32")]
    internal static extern bool GetMonitorInfo(IntPtr hMonitor, MONITORINFO lpmi);

    [DllImport("User32")]
    internal static extern IntPtr MonitorFromWindow(IntPtr handle, int flags);

    private static IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        switch (msg)
        {
            case 0x0024:
                WmGetMinMaxInfo(hwnd, lParam);
                handled = true;
                break;
        }

        return (IntPtr)0;
    }

    private static void WmGetMinMaxInfo(IntPtr hwnd, IntPtr lParam)
    {
        MINMAXINFO mmi = (MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(MINMAXINFO));

        // Adjust the maximized size and position to fit the work area of the correct monitor
        int MONITOR_DEFAULTTONEAREST = 0x00000002;
        IntPtr monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);

        if (monitor != IntPtr.Zero)
        {
            MONITORINFO monitorInfo = new MONITORINFO();
            GetMonitorInfo(monitor, monitorInfo);
            RECT rcWorkArea = monitorInfo.rcWork;
            RECT rcMonitorArea = monitorInfo.rcMonitor;

            // Offset top and left 1 pixel improves the situation
            rcMonitorArea.top += 1;
            rcMonitorArea.left += 1;

            mmi.ptMaxPosition.x = Math.Abs(rcWorkArea.left - rcMonitorArea.left);
            mmi.ptMaxPosition.y = Math.Abs(rcWorkArea.top - rcMonitorArea.top);
            mmi.ptMaxSize.x = Math.Abs(rcWorkArea.right - rcWorkArea.left);
            mmi.ptMaxSize.y = Math.Abs(rcWorkArea.bottom - rcWorkArea.top);
        }

        Marshal.StructureToPtr(mmi, lParam, true);
    }

    private static void WinSourceInitialized(object sender, EventArgs e)
    {
        IntPtr handle = (new WinInterop.WindowInteropHelper((Window)sender)).Handle;
        WinInterop.HwndSource.FromHwnd(handle).AddHook(WindowProc);
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct MINMAXINFO
    {
        public POINT ptReserved;
        public POINT ptMaxSize;
        public POINT ptMaxPosition;
        public POINT ptMinTrackSize;
        public POINT ptMaxTrackSize;
    };

    [StructLayout(LayoutKind.Sequential)]
    public struct POINT
    {
        /// <summary>
        /// x coordinate of point.
        /// </summary>
        public int x;

        /// <summary>
        /// y coordinate of point.
        /// </summary>
        public int y;

        /// <summary>
        /// Construct a point of coordinates (x,y).
        /// </summary>
        public POINT(int x, int y)
        {
            this.x = x;
            this.y = y;
        }
    }

    [StructLayout(LayoutKind.Sequential, Pack = 0)]
    public struct RECT
    {
        /// <summary> Win32 </summary>
        public int left;

        /// <summary> Win32 </summary>
        public int top;

        /// <summary> Win32 </summary>
        public int right;

        /// <summary> Win32 </summary>
        public int bottom;

        /// <summary> Win32 </summary>
        public static readonly RECT Empty = new RECT();

        /// <summary> Win32 </summary>
        public int Width
        {
            get { return Math.Abs(right - left); } // Abs needed for BIDI OS
        }

        /// <summary> Win32 </summary>
        public int Height
        {
            get { return bottom - top; }
        }

        /// <summary> Win32 </summary>
        public RECT(int left, int top, int right, int bottom)
        {
            this.left = left;
            this.top = top;
            this.right = right;
            this.bottom = bottom;
        }

        /// <summary> Win32 </summary>
        public RECT(RECT rcSrc)
        {
            left = rcSrc.left;
            top = rcSrc.top;
            right = rcSrc.right;
            bottom = rcSrc.bottom;
        }

        /// <summary> Win32 </summary>
        public bool IsEmpty
        {
            get
            {
                // BUGBUG : On Bidi OS (hebrew arabic) left > right
                return left >= right || top >= bottom;
            }
        }

        /// <summary> Return a user friendly representation of this struct </summary>
        public override string ToString()
        {
            if (this == Empty)
            {
                return "RECT {Empty}";
            }
            return "RECT { left : " + left + " / top : " + top + " / right : " + right + " / bottom : " + bottom + " }";
        }

        /// <summary> Determine if 2 RECT are equal (deep compare) </summary>
        public override bool Equals(object obj)
        {
            if (!(obj is Rect))
            {
                return false;
            }
            return (this == (RECT)obj);
        }

        /// <summary>Return the HashCode for this struct (not garanteed to be unique)</summary>
        public override int GetHashCode()
        {
            return left.GetHashCode() + top.GetHashCode() + right.GetHashCode() + bottom.GetHashCode();
        }

        /// <summary> Determine if 2 RECT are equal (deep compare)</summary>
        public static bool operator ==(RECT rect1, RECT rect2)
        {
            return (rect1.left == rect2.left && rect1.top == rect2.top && rect1.right == rect2.right && rect1.bottom == rect2.bottom);
        }

        /// <summary> Determine if 2 RECT are different(deep compare)</summary>
        public static bool operator !=(RECT rect1, RECT rect2)
        {
            return !(rect1 == rect2);
        }
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    public class MONITORINFO
    {
        /// <summary>
        /// </summary>            
        public int cbSize = Marshal.SizeOf(typeof(MONITORINFO));

        /// <summary>
        /// </summary>            
        public RECT rcMonitor = new RECT();

        /// <summary>
        /// </summary>            
        public RECT rcWork = new RECT();

        /// <summary>
        /// </summary>            
        public int dwFlags = 0;
    }
}
Share:
21,184
Athari
Author by

Athari

GitHub: Athari CodeProject: Athari (articles) Хабрахабр: Athari Personal website: Snow Lands All original source snippets I post here are dedicated to the public domain, unless stated otherwise.

Updated on April 19, 2020

Comments

  • Athari
    Athari about 4 years

    I'm trying out Ribbon control in combination with RibbonWindow, however they fail even in trivial experiments.

    1. Created new WPF Application
    2. Changed code to example from MSDN
    3. Added reference to System.Windows.Controls.Ribbon and got rid of ribbon: prefix (why are examples outdated?).
    4. Added two icons (16x16 and 32x32).
    5. Executed the application and saw this (Notepad for reference):

    I can already see numerous problems:

    1. Border is tiny. A normal window has a big border, WPF Ribbon app has tiny. The title height is smaller too.
    2. Border is blurry. When a normal window is focused, it's border is black. WPF app's border is greyish (black can be seen in corners; something is drawn over the borders?).
    3. Application icon is misplaced. It's glued to the top-left corner.
    4. Application title is misplaced. It's glued to the top.

    Let's move the toolbar to the bottom. Now we see this:

    Buttons are outside of the toolbar.

    And finally, let's maximize the window:

    Half of the header disappeared outside of the screen (technically window is outside of the screen by 8 pixels on every side, but other apps aren't confused by this).

    I'm using Windows 7, Aero, single monitor, nothing special. I'm afraid to test the application on Windows 8...

    Any chance to fix this?

    • jbe
      jbe over 10 years
    • AgostinoX
      AgostinoX over 4 years
      the link is broken: "You have reached this page because you’ve tried to access the Microsoft Connect portal. After nearly 10 years, Microsoft Connect has been retired in favor of new tools and services."
  • Athari
    Athari over 11 years
    Thank you, but that's too much trouble to use just one control. Using old framework is obviously not an option. Switching to old system will probably cause all kinds of troubles (considering I plan to use features of Vista/7). Guessing system settings will cause troubles with different OS versions. Hoping Microsoft will fix a bug soon is just plain silly, based on my experience (it took years to fix URL parsing...). I'm trying out Fluent Ribbon, and so far, it works much better (it does have bugs, but at least basic functionality JUST WORKS). Arrgghh!
  • Joe Castro
    Joe Castro over 11 years
    This should work fine if you take the WindowChrome instance that the Ribbon is using and change the ResizeBorderThickness to the correct values. It doesn't have to be hardcoded, instead you can just get the appropriate system metrics. I don't know for sure what the default Ribbon template is using for everything, so it may require a couple other properties to be changed. That said, if you're happy with the Fluent version then that works too :)
  • Olly
    Olly about 11 years
    For C# you can set the compiler /subsystemversion switch. (msdn.microsoft.com/en-us/library/vstudio/hh965708.aspx) I add a property in the project file <subsystemversion>5.01</subsystemversion> that falsely indicates that the code can run on Windows XP. The shim is then used, and the ribbon window looks OK.
  • Julien Lebosquain
    Julien Lebosquain about 11 years
    @Olly that's really great! Edited my answer with that, seems perfect.
  • Marcin Wisnicki
    Marcin Wisnicki about 11 years
    Anyone tried this with VS2012 Update2 ? Neither <SubsystemVersion>5.01</SubsystemVersion> nor changing any property on WindowChrome seem to have any effect.
  • Vladimir
    Vladimir almost 9 years
    I've used <subsystemversion>5.01</subsystemversion> solution until I upgraded to Windows 10. In Windows 10 this workaround adds ugly side borders on a window. Yannick KcinnaY's workaround works better if you do not need special Ribbon buttons on the titlebar. Here it is: stackoverflow.com/a/27730148/265196
  • Vladimir
    Vladimir almost 9 years
    This workaround is the best for me, that is if you do not need special Ribbon buttons on the titlebar.
  • dotnetzen
    dotnetzen over 6 years
    Yes, this is a quick and easy solution if you don't need the QuickAccessToolBar.
  • StayOnTarget
    StayOnTarget about 5 years
    The link to the GetSystemMetrics bug is dead :(
  • Xam
    Xam over 4 years
    Sorry, I have a question: where should I put the style?
  • astef
    astef almost 4 years
    Margin should be calculated for different display configs. Try it with 100%, 125%, 150% scales in Windows 10, you'll see that it is different. See my answer below for the code, which is calculating it
  • Yannick Turbang
    Yannick Turbang over 3 years
    @astef - This solution is working with 100%, 125%, 150% it has been tested and works in all cases.
  • astef
    astef over 3 years
    Sorry, I thought you were using margin to overcome the window chrome positioning problem, which is the original problem for this issue. But it seems that you simply use the standard window and hide the part of the ribbon under it. BTW, standard window chrome is not ideal after resizing, if you examine it pixel by pixel
  • Yannick Turbang
    Yannick Turbang over 3 years
    @astef now i understand where is the misunderstanding, take care it is not the margin of the window which is modified but the margin of the ribbon. There is no connection with the resizing or positioning of the window. The ribbon is a control which is not bound.
  • Toni
    Toni about 3 years
    No need for the negative margin when using the Window class. The title is fixed but a binding error occurs at runtime.