Search code examples
.net-corewindows-10.net-6.0mauimaui-windows

MAUI why is my title bar gray and how do I fix it?


I made the MAUI sample project in Visual Studio 2022, but when I launch the program, the window's title bar is gray, a gray that doesn't change color at all when the window loses focus (though the title text I added changes from black to grey). I have "Show accent color on the following surfaces" with both boxes checked.

Why is my MAUI window not using my accent color, and how do I fix it?

Note: I'm on Windows 10, so I can't use that thing that works only on Windows 11.


Solution

  • In the end, I used a code based on this one to hide most of the grey title bar while on Windows, plus a Windows-only dependency on a Windows Forms library I use to childify and enclose the WinUI3 window inside a Windows Form (that resizes its child when resized).

    The WinForms side

    I start by creating a basic form and add P/Invoke code to childify and resize (as well as a simplified form of the FormClosed event).

        public partial class Form1 : Form, IContainerForm
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            static class NativeMethods
            {
                [DllImport("user32.dll", SetLastError = true)]
                public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
    
                [DllImport("user32.dll", SetLastError = true)]
                public static extern IntPtr GetParent(IntPtr hWnd);
    
                public const int GWL_STYLE = -16;
    
                [DllImport("User32.dll", EntryPoint = "GetWindowLong", CharSet = CharSet.Auto)]
                public extern static uint GetWindowLongU(IntPtr hwnd, int nIndex);
                [DllImport("User32.dll", EntryPoint = "SetWindowLong", CharSet = CharSet.Auto)]
                public extern static uint SetWindowLongU(IntPtr hwnd, int nIndex, uint dwNewLong);
    
                [DllImport("user32.dll", SetLastError = true)]
                internal static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
            }
    
            [Flags]
            public enum WindowStyles : uint
            {
                WS_BORDER = 0x800000,
                WS_CAPTION = 0xc00000,
                WS_CHILD = 0x40000000,
                WS_CLIPCHILDREN = 0x2000000,
                WS_CLIPSIBLINGS = 0x4000000,
                WS_DISABLED = 0x8000000,
                WS_DLGFRAME = 0x400000,
                WS_GROUP = 0x20000,
                WS_HSCROLL = 0x100000,
                WS_MAXIMIZE = 0x1000000,
                WS_MAXIMIZEBOX = 0x10000,
                WS_MINIMIZE = 0x20000000,
                WS_MINIMIZEBOX = 0x20000,
                WS_OVERLAPPED = 0x0,
                WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_SIZEFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX,
                WS_POPUP = 0x80000000u,
                WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU,
                WS_SIZEFRAME = 0x40000,
                WS_SYSMENU = 0x80000,
                WS_TABSTOP = 0x10000,
                WS_VISIBLE = 0x10000000,
                WS_VSCROLL = 0x200000
            }
    
            IntPtr hwndContained;
    
            #region IContainerForm members
            public IntPtr EnclosedHandle { get => hwndContained; }
    
            public void Enclose(IntPtr hWndToContain)
            {
                if(Handle==IntPtr.Zero)
                    throw new InvalidOperationException("Cannot enclose window because current object's window is not created.");
                var windowStyles = (WindowStyles)NativeMethods.GetWindowLongU(hWndToContain, NativeMethods.GWL_STYLE);
                windowStyles &= ~WindowStyles.WS_OVERLAPPEDWINDOW;
                windowStyles &= ~WindowStyles.WS_POPUP;
                windowStyles &= ~WindowStyles.WS_CAPTION;
                windowStyles |= WindowStyles.WS_CHILD;
                NativeMethods.SetWindowLongU(hWndToContain, NativeMethods.GWL_STYLE, (uint)windowStyles);
    
                NativeMethods.SetParent(hWndToContain, Handle);
                hwndContained = hWndToContain;
                OnSizeChanged(EventArgs.Empty);
            }
    
            public string TitleText { get => this.Text; set => this.Text=value; }
    
            private event EventHandler? formClosedSimple;
            public event EventHandler FormClosedSimple { add => formClosedSimple+=value; remove => formClosedSimple-=value; }
            private void FireFormClosedSimple()
            {
                if(formClosedSimple != null)
                    formClosedSimple(this, EventArgs.Empty);
            }
            protected override void OnFormClosed(FormClosedEventArgs e)
            {
                base.OnFormClosed(e);
                FireFormClosedSimple();
            }
    
            #endregion
    
            public static bool HasParent(IntPtr hWnd)
            {
                return NativeMethods.GetParent(hWnd)!=IntPtr.Zero;
            }
    
            private void Form1_SizeChanged(object sender, EventArgs e)
            {
                int width = Width;
                int height = Height;
                if(width==0 || height==0 || WindowState == FormWindowState.Minimized)
                    return;
                if(hwndContained!=IntPtr.Zero)
                {
                    NativeMethods.MoveWindow(hwndContained, 0, 0, ClientSize.Width, ClientSize.Height, true);
                }
            }
        }
    

    To avoid the calling code having to manipulate WinForms types directly, I isolate it behind an interface:

        public class Class1
        {
            public static IContainerForm CreateContainerForm()
            {
                var form = new Form1();
                form.Show();
                return form;
            }
            public static IContainerForm CreateContainerForm(int width, int height)
            {
                var form = new Form1();
                form.Width = width;
                form.Height = height;
                form.Show();
                return form;
            }
            public static bool HasParent(IntPtr hWnd) => Form1.HasParent(hWnd);
        }
    
        public interface IContainerForm
        {
            void Enclose(IntPtr hWnd);
            IntPtr EnclosedHandle { get; }
            string TitleText { get; set; }
            event EventHandler FormClosedSimple;
        }
    

    That's it for the Windows Forms Control Library project.

    The MAUI side

    For starters, I add a conditional reference to the Windows Forms Control Library:

        <ItemGroup Condition="$(TargetFramework.Contains('-windows')) != false">
            <ProjectReference Include="..\WinFormsLibrary1\WinFormsLibrary1.csproj" />
        </ItemGroup>
    

    Then it's just a matter of calling it (but only on Windows) from the MAUI code:

    using Microsoft.Extensions.Logging;
    using Microsoft.Maui.LifecycleEvents;
    #if WINDOWS
    using Microsoft.UI;
    using Microsoft.UI.Windowing;
    #endif
    
    namespace MySecondMauiApp
    {
        public static class MauiProgram
        {
            public static MauiApp CreateMauiApp()
            {
                var builder = MauiApp.CreateBuilder();
                builder
                    .UseMauiApp<App>()
                    .ConfigureFonts(fonts =>
                    {
                        fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                        fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                    });
    
    #if DEBUG
                builder.Logging.AddDebug();
    #endif
                AddTitleBarCodeOnWindows(builder, new System.Drawing.Size(640, 640));
    
                return builder.Build();
            }
    
            public static void AddTitleBarCodeOnWindows(MauiAppBuilder builder, System.Drawing.Size size)
            {
    #if WINDOWS
                builder.ConfigureLifecycleEvents(events =>
                {
                    events.AddWindows(wndLifeCycleBuilder =>
                    {
                        wndLifeCycleBuilder.OnWindowCreated(window =>
                        {
                            IntPtr nativeWindowHandle = WinRT.Interop.WindowNative.GetWindowHandle(window);
                            WindowId win32WindowsId = Win32Interop.GetWindowIdFromWindow(nativeWindowHandle);
                            AppWindow winuiAppWindow = AppWindow.GetFromWindowId(win32WindowsId);
                            if(winuiAppWindow.Presenter is OverlappedPresenter p)
                            {
                                window.ExtendsContentIntoTitleBar = false;
                                p.SetBorderAndTitleBar(false, false);
                            }
                            var containerForm = WinFormsLibrary1.Class1.CreateContainerForm(size.Width, size.Height);
                            containerForm.Enclose(nativeWindowHandle);
                            containerForm.TitleText = window.Title;
                            //Apparently necessary since app doesn't close on its own.
                            //I would have thought closing the form, and therefore destroying the child MAUI Window, would do the trick.
                            containerForm.FormClosedSimple += (sender, args) => Application.Current.Quit();
                        });
                    });
                });
    #endif
            }
    
        }//class
    }
    

    And that's it, you now have a shiny new MAUI application whose title bar correctly uses your accent color when focused (and turns white when unfocused): enter image description here

    Of course, none of this would have been necessary had Microsoft not unilaterally decreed that all WinUI3 windows would have a grey title bar that doesn't change color based on focus, instead of following your accent color. But now at least you can fix it.