Medinoc
Medinoc

Reputation: 6608

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.

Upvotes: 3

Views: 3982

Answers (3)

Yu-Core
Yu-Core

Reputation: 1

you can change Platforms/Windows/App.xaml

<maui:MauiWinUIApplication
x:Class="MauiApp1.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:maui="using:Microsoft.Maui"
xmlns:local="using:MauiApp1.WinUI">
<Application.Resources>
    <ResourceDictionary>
        <SolidColorBrush x:Key="WindowCaptionBackground">#fff</SolidColorBrush>
        <SolidColorBrush x:Key="WindowCaptionBackgroundDisabled">#fff</SolidColorBrush>
        <SolidColorBrush x:Key="WindowCaptionForeground">#000</SolidColorBrush>
        <SolidColorBrush x:Key="WindowCaptionForegroundDisabled">#000</SolidColorBrush>
    </ResourceDictionary>
</Application.Resources>
</maui:MauiWinUIApplication>

if you want change title bar color at runtime

1.Install this package

 <PackageReference Include="PInvoke.User32" Version="0.7.124" Condition="$(TargetFramework.Contains('-windows')) == true" />

2.Add the following code

using Microsoft.Maui.Platform;
using WinRT.Interop;
using System.Runtime.Versioning;
using PInvoke;
using static PInvoke.User32;

namespace MauiApp1;

[SupportedOSPlatform("Windows")]
public static class TitleBar
{
    static Microsoft.UI.Xaml.Window? NativeWindow =>
        (Microsoft.UI.Xaml.Window?)Application.Current?.Windows.FirstOrDefault()?.Handler?.PlatformView;
    static Microsoft.UI.Xaml.ResourceDictionary Resources =>
        Microsoft.UI.Xaml.Application.Current.Resources;

    public static void SetColor(Color color)
    {
        Resources["WindowCaptionBackground"] = color.ToWindowsColor();
        Resources["WindowCaptionBackgroundDisabled"] = color.ToWindowsColor();
        TriggertTitleBarRepaint();
    }

    public static void SetStyle(TitleBarStyle style)
    {
        var color = style switch
        {
            TitleBarStyle.Default => Colors.Black,
            TitleBarStyle.LightContent => Colors.White,
            TitleBarStyle.DarkContent => Colors.Black,
            _ => throw new NotSupportedException($"{nameof(TitleBarStyle)} {style} is not yet supported on iOS")
        };
        Resources["WindowCaptionForeground"] = color.ToWindowsColor();
        Resources["WindowCaptionForegroundDisabled"] = color.ToWindowsColor();
        TriggertTitleBarRepaint();
    }

    static void TriggertTitleBarRepaint()
    {
        if (NativeWindow is null)
        {
            return;
        }

        var hWnd = WindowNative.GetWindowHandle(NativeWindow);
        var activeWindow = User32.GetActiveWindow();
        if (hWnd == activeWindow)
        {
            User32.PostMessage(hWnd, WindowMessage.WM_ACTIVATE, new IntPtr((int)0x00), IntPtr.Zero);
            User32.PostMessage(hWnd, WindowMessage.WM_ACTIVATE, new IntPtr((int)0x01), IntPtr.Zero);
        }
        else
        {
            User32.PostMessage(hWnd, WindowMessage.WM_ACTIVATE, new IntPtr((int)0x01), IntPtr.Zero);
            User32.PostMessage(hWnd, WindowMessage.WM_ACTIVATE, new IntPtr((int)0x00), IntPtr.Zero);
        }
    }
}
public enum TitleBarStyle
{
    Default = 0,
    LightContent = 1,
    DarkContent = 2
}

3.use

#if Windows
    TitleBar.SetColor(titleBarColor);
    TitleBar.SetStyle(TitleBarStyle.DarkContent);
#endif

Valid on both Win10 and Win11

Upvotes: 0

Medinoc
Medinoc

Reputation: 6608

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.

Upvotes: 1

ToolmakerSteve
ToolmakerSteve

Reputation: 21243

Alexandar May's comment references doc that describes full customization of the title bar:

Per the official docs Title bar customization / Full customization, there are two levels of customization that you can apply to the title bar: apply minor modifications to the default title bar, or extend your app canvas into the title bar area and provide completely custom content.

From that doc / Title bar content and drag regions:

<Grid x:Name="AppTitleBar">
    <Image Source="Images/WindowIcon.png"
           HorizontalAlignment="Left" 
           Width="16" Height="16" 
           Margin="8,0"/>
    <TextBlock x:Name="AppTitleTextBlock" Text="App title"
               TextWrapping="NoWrap"
               Style="{StaticResource CaptionTextBlockStyle}" 
               VerticalAlignment="Center"
               Margin="28,0,0,0"/>
</Grid>
public MainWindow()
{
    this.InitializeComponent();

    ExtendsContentIntoTitleBar = true;
    SetTitleBar(AppTitleBar);

    AppTitleTextBlock.Text = AppInfo.Current.DisplayInfo.DisplayName;
}

ORIGINAL ANSWER

tl;dr: You can't control title bar color on Windows 10. At least not via WinUI-3 APIs.

IMPORTANT: This answer describes the situation with WinUI-3 APIs.
I'll leave it to someone else to figure out how to use P/Invoke: Build a C# .NET app with WinUI 3 and Win32 interop, to get at Win32 APIs that might work on Windows 10.
It might not be possible even that way.
Unclear to me whether the Window created by WinUI-3 on Windows 10 is physically capable of changing its title color.

The existing WinUI-3 APIs don't support this on Windows 10. WinUI-3 is what Maui targets on Windows.

Title bar customization says:

Title bar customization APIs are currently supported on Windows 11 only. We recommend that you check AppWindowTitleBar.IsCustomizationSupported in your code before you call these APIs to ensure your app doesn't crash on other versions of Windows.

Further detail is shown in Windows UI Library in the Windows App SDK (WinUI 3).

There is a table Feature Window AppWindow showing features supported on Windows 10.
In that table, we see that Window is supported on Windows 10, but AppWindow is not.
It also shows that Window allows (only) Title to be set. Need AppWindow to change colors.

Upvotes: 2

Related Questions