3

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.

Medinoc
  • 6,577
  • 20
  • 42

3 Answers3

2

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.

ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196
  • 4
    :-( The big question is, why do they feel the need to override Windows's normal titlebar in the first place to replace it with this gray bar? – Medinoc Nov 29 '22 at 08:29
  • 2
    Per the official docs[How much to customize the title bar](https://learn.microsoft.com/en-us/windows/apps/develop/title-bar?tabs=winui3#how-much-to-customize-the-title-bar), 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. – Alexandar May - MSFT Dec 01 '22 at 09:18
  • 2
    Great, so we have to extend the App Canvas to draw a fake title bar to work around the problem... And from what clues I've been able to gather, this is exactly what MAUI/WinUI3 is *already doing* to cause the problem in the first place! So we have to draw a fake title bar *to replace WinUI3's own fake title bar* to undo the damage! Talk about abstraction inversion... – Medinoc Dec 02 '22 at 08:55
  • Unfortunately the code posted above appears to be for working directly on a WinUI3 window, it does not seem compatible with MAUI's `Shell`-based structure. – Medinoc Jan 03 '23 at 20:50
  • *"does not seem compatible"* - maybe, maybe not. It is indeed code for working directly on the WinUI3 window, which Maui creates and displays in, when running on Windows. The code must be only on WinUI3, not in cross-platform code. If you want help doing that, add a new section to your question. Show the code the tried, where you added that code, and what went wrong. – ToolmakerSteve Jan 05 '23 at 03:37
1

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.

Medinoc
  • 6,577
  • 20
  • 42
0

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

Yu-Core
  • 1
  • 1