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.
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):
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.