Skip to content

Instantly share code, notes, and snippets.

@LanceMcCarthy
Last active November 12, 2024 22:36
Show Gist options
  • Save LanceMcCarthy/4954ab92ca44c19eb4316d9d683efd50 to your computer and use it in GitHub Desktop.
Save LanceMcCarthy/4954ab92ca44c19eb4316d9d683efd50 to your computer and use it in GitHub Desktop.
Mica support in .NET MAUI
// ************************************************************************ //
// This implementation is inspired from the official WinUI3 backdrops examples
// https://github.com/microsoft/WinUI-Gallery/blob/winui3/XamlControlsGallery/ControlPagesSampleCode/SystemBackdrops/SystemBackdropsSample2.txt
// *********************************************************************** //
using Microsoft.Maui.LifecycleEvents;
#if WINDOWS10_0_17763_0_OR_GREATER
// The namespace of where the WindowsHelpers class resides
// In older versions of .NET MAUI, this was
//using YOUR_APP.Platforms.Windows
// In newer versions of .NET MAUI, it is now
using YOUR_APP.WinUI;
#endif
namespace YOUR_APP.Maui
{
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");
});
// **** THIS SECTION IS WHAT IS RELEVANT FOR YOU ************ //
builder.ConfigureLifecycleEvents(events =>
{
#if WINDOWS10_0_17763_0_OR_GREATER
events.AddWindows(wndLifeCycleBuilder =>
{
wndLifeCycleBuilder.OnWindowCreated(window =>
{
window.TryMicaOrAcrylic(); // requires 'using YOUR_APP.WinUI;'
});
});
#endif
});
// ************* END OF RELEVANT SECTION *********** //
return builder.Build();
}
}
}
// **************************************************** //
// Place this class file in your Platforms/Windows folder
// **************************************************** //
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml;
using WinRT;
namespace YOUR_APP.Platforms.Windows
{
public static class WindowHelpers
{
public static void TryMicaOrAcrylic(this Microsoft.UI.Xaml.Window window)
{
var dispatcherQueueHelper = new WindowsSystemDispatcherQueueHelper(); // in Platforms.Windows folder
dispatcherQueueHelper.EnsureWindowsSystemDispatcherQueueController();
// Hooking up the policy object
var configurationSource = new SystemBackdropConfiguration();
configurationSource.IsInputActive = true;
switch (((FrameworkElement)window.Content).ActualTheme)
{
case ElementTheme.Dark:
configurationSource.Theme = SystemBackdropTheme.Dark;
break;
case ElementTheme.Light:
configurationSource.Theme = SystemBackdropTheme.Light;
break;
case ElementTheme.Default:
configurationSource.Theme = SystemBackdropTheme.Default;
break;
}
// Let's try Mica first
if (MicaController.IsSupported())
{
var micaController = new MicaController();
micaController.AddSystemBackdropTarget(window.As<ICompositionSupportsSystemBackdrop>());
micaController.SetSystemBackdropConfiguration(configurationSource);
window.Activated += (object sender, WindowActivatedEventArgs args) =>
{
if (args.WindowActivationState is WindowActivationState.CodeActivated or WindowActivationState.PointerActivated)
{
// Handle situation where a window is activated and placed on top of other active windows.
if (micaController == null)
{
micaController = new MicaController();
micaController.AddSystemBackdropTarget(window.As<ICompositionSupportsSystemBackdrop>()); // Requires 'using WinRT;'
micaController.SetSystemBackdropConfiguration(configurationSource);
}
if (configurationSource != null)
configurationSource.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated;
}
};
window.Closed += (object sender, WindowEventArgs args) =>
{
if (micaController != null)
{
micaController.Dispose();
micaController = null;
}
configurationSource = null;
};
}
// If no Mica, maybe we can use Acrylic instead
else if (DesktopAcrylicController.IsSupported())
{
var acrylicController = new DesktopAcrylicController();
acrylicController.AddSystemBackdropTarget(window.As<ICompositionSupportsSystemBackdrop>());
acrylicController.SetSystemBackdropConfiguration(configurationSource);
window.Activated += (object sender, WindowActivatedEventArgs args) =>
{
if (args.WindowActivationState is WindowActivationState.CodeActivated or WindowActivationState.PointerActivated)
{
// Handle situation where a window is activated and placed on top of other active windows.
if (acrylicController == null)
{
acrylicController = new DesktopAcrylicController();
acrylicController.AddSystemBackdropTarget(window.As<ICompositionSupportsSystemBackdrop>()); // Requires 'using WinRT;'
acrylicController.SetSystemBackdropConfiguration(configurationSource);
}
}
if (configurationSource != null)
configurationSource.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated;
};
window.Closed += (object sender, WindowEventArgs args) =>
{
if (acrylicController != null)
{
acrylicController.Dispose();
acrylicController = null;
}
configurationSource = null;
};
}
}
}
}
// **************************************************** //
// Place this class file in your Platforms/Windows folder
// **************************************************** //
using System.Runtime.InteropServices;
using Windows.System; // Required for DllImport attribute and DispatcherQueue
namespace YOUR_APP.Maui.Platforms.Windows;
public class WindowsSystemDispatcherQueueHelper
{
[StructLayout(LayoutKind.Sequential)]
struct DispatcherQueueOptions
{
internal int dwSize;
internal int threadType;
internal int apartmentType;
}
[DllImport("CoreMessaging.dll")]
private static extern int CreateDispatcherQueueController([In] DispatcherQueueOptions options, [In, Out, MarshalAs(UnmanagedType.IUnknown)] ref object dispatcherQueueController);
object m_dispatcherQueueController = null;
public void EnsureWindowsSystemDispatcherQueueController()
{
if (DispatcherQueue.GetForCurrentThread() != null)
{
// one already exists, so we'll just use it.
return;
}
if (m_dispatcherQueueController == null)
{
DispatcherQueueOptions options;
options.dwSize = Marshal.SizeOf(typeof(DispatcherQueueOptions));
options.threadType = 2; // DQTYPE_THREAD_CURRENT
options.apartmentType = 2; // DQTAT_COM_STA
CreateDispatcherQueueController(options, ref m_dispatcherQueueController);
}
}
}
@FHWWC
Copy link

FHWWC commented Oct 27, 2022

Hello again!
Thanks for help, but the problem wasn't solved. The page background is white, or the application couldn't be running and still show error.

@FHWWC
Copy link

FHWWC commented Oct 27, 2022

SS

@LanceMcCarthy
Copy link
Author

@FHWWC
Copy link

FHWWC commented Oct 27, 2022

Yes I know, but the application background is white ,I have already followed your steps to change the code.
SS

@navi705
Copy link

navi705 commented Dec 28, 2022

Hello, can you help me? I have a problem with color or how I think with contrast. I use this code in winui 3 and maui, but the result is different. I using Windows 11. I tried to follow the link upper Lance's .NET MAUI Mica Demo, but link has expired. Sorry for my bad English. Thank you in advance

//WindowHelpers
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Windows.UI.ViewManagement;
using WinRT;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI;

    public static class WindowHelpers
    {
        public static void TryMicaOrAcrylic(this Microsoft.UI.Xaml.Window window)
        {
            var dispatcherQueueHelper = new WindowsSystemDispatcherQueueHelper(); // in Platforms.Windows folder
            dispatcherQueueHelper.EnsureWindowsSystemDispatcherQueueController();

            // Hooking up the policy object
            var configurationSource = new SystemBackdropConfiguration();
            configurationSource.IsInputActive = true;

            switch (((FrameworkElement)window.Content).ActualTheme)
            {
                case ElementTheme.Dark:
                    configurationSource.Theme = SystemBackdropTheme.Dark;
                    break;
                case ElementTheme.Light:
                    configurationSource.Theme = SystemBackdropTheme.Light;
                    break;
                case ElementTheme.Default:
                    configurationSource.Theme = SystemBackdropTheme.Default;
                    break;
            }



            if (DesktopAcrylicController.IsSupported())
            {
                var acrylicController = new DesktopAcrylicController();
                acrylicController.LuminosityOpacity = 0.3f;
                acrylicController.TintOpacity = 0.7f;
                var uiSettings = new UISettings();
                var color = uiSettings.GetColorValue(UIColorType.AccentDark2);
                acrylicController.TintColor = color;
                acrylicController.FallbackColor = color;
                acrylicController.AddSystemBackdropTarget(window.As<ICompositionSupportsSystemBackdrop>());
                acrylicController.SetSystemBackdropConfiguration(configurationSource);



                window.Activated += (object sender, WindowActivatedEventArgs args) =>
                {
                    if (args.WindowActivationState is WindowActivationState.CodeActivated or WindowActivationState.PointerActivated)
                    {
                        // Handle situation where a window is activated and placed on top of other active windows.
                        if (acrylicController == null)
                        {
                            acrylicController = new DesktopAcrylicController();
                            acrylicController.AddSystemBackdropTarget(window.As<ICompositionSupportsSystemBackdrop>()); // Requires 'using WinRT;'
                            acrylicController.SetSystemBackdropConfiguration(configurationSource);
                        }
                    }

                    if (configurationSource != null)
                        configurationSource.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated;
                };

                window.Closed += (object sender, WindowEventArgs args) =>
                {
                    if (acrylicController != null)
                    {
                        acrylicController.Dispose();
                        acrylicController = null;
                    }

                    configurationSource = null;
                };
            }

        }

    }



// WindowsSystemDispatcherQueueHelper
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using Windows.System; 

    public class WindowsSystemDispatcherQueueHelper
    {
        [StructLayout(LayoutKind.Sequential)]
        struct DispatcherQueueOptions
        {
            internal int dwSize;
            internal int threadType;
            internal int apartmentType;
        }

        [DllImport("CoreMessaging.dll")]
        private static extern int CreateDispatcherQueueController([In] DispatcherQueueOptions options, [In, Out, MarshalAs(UnmanagedType.IUnknown)] ref object dispatcherQueueController);

        object m_dispatcherQueueController = null;

        public void EnsureWindowsSystemDispatcherQueueController()
        {
            if (DispatcherQueue.GetForCurrentThread() != null)
            {
                // one already exists, so we'll just use it.
                return;
            }

            if (m_dispatcherQueueController == null)
            {
                DispatcherQueueOptions options;

                options.dwSize = Marshal.SizeOf(typeof(DispatcherQueueOptions));
                options.threadType = 2;    // DQTYPE_THREAD_CURRENT
                options.apartmentType = 2; // DQTAT_COM_STA

                CreateDispatcherQueueController(options, ref m_dispatcherQueueController);
            }
        }
    }

image

@LanceMcCarthy
Copy link
Author

Hi @navi705 from the way that screenshot looks, the native window is applying the root frame transparency from Mica.

As to why the tint is a different color, I cannot say. There's also a difference int he titlebar color, too. I suspect that .NET MAUI is adding/changing something. I recommend opening a new issue in the MAUI repo https://github.com/dotnet/maui

Tip - When opening the issue, start with the screenshot, then put the code at the end. This will help communicate the message that something different is happening in MAUI than WinUI3.

@navi705
Copy link

navi705 commented Dec 29, 2022

Ok, thank you!

@veikkoeeva
Copy link

May be related to microsoft/WindowsAppSDK#3230.

As an aside, https://dotmorten.github.io/WinUIEx/, maybe have interesting helpersfor this code too (I don't know, if someone finds something, plase do share).

@LanceMcCarthy
Copy link
Author

Yep WinUIEx has some nice window position/size helpers. This gist example is specifically to add the window.TryMicaOrAcrylic() extension method to simplify the MAUI code.

For example, here I center and size the window => https://github.com/LanceMcCarthy/DevOpsExamples/blob/efbd26e7abe39738e518fb45d159a75bd84989ea/src/MAUI/MauiDemo/MauiProgram.cs#L44

I'm not sure if Morten has added the Acrylic/Mica stuff yet, but IIRC he mentioned to me it was in his plans.

Tip using WinUIEx, you can also use the persistence manager and remember the user's last window position and size:

#if WINDOWS10_0_17763_0_OR_GREATER
                
                events.AddWindows(wndLifeCycleBuilder =>
                {
                    wndLifeCycleBuilder.OnWindowCreated(window =>
                    {
                        // This remembers the user's last window position and size
                        var manager = WinUIEx.WindowManager.Get(window);
                        manager.PersistenceId = "MainWindowPersistanceId";
                        manager.MinWidth = 640;
                        manager.MinHeight = 480;

                        // Check for Mica or Acrylic support
                        window.TryMicaOrAcrylic();
                    });
                });

@kolkonos
Copy link

Lance,
Any chance you got this working with Shell Apps?
When using a single page, everything works fine. However, in a shell app, the following elements also get the Mica background resulting in a stacking of the effect:

  • Shell.TitleView
  • ShellContent

This results in the background for the page's content looking like it's not fully transparent.

If you have any insight as to how we could possible tap into the lifecycle of those components and set them to transparent, I would be very grateful.

You can see this especially well when you add the title bar to a flyout page.
image

@LanceMcCarthy
Copy link
Author

LanceMcCarthy commented Dec 19, 2023

Hi @kolkonos I'm sorry, I didn't see this until now.

What you want to do is not technically possible. That is because WinUI 2 and 3 does not support 100% transparent background Windows. You can get semi-transparent real-time blur (what is called Acrylic).

Update
I just realized you might be talking about the Shell page's background so that you can get the acrylic all the way through. I believe you can achieve that with Styles targeting ContentPage, Shell and possibly NavigationPage, then setting the BackgroundColor to Transparent. Though, I would ask the MAUI team for assistance with that if you're still stuck, you can get help here => https://learn.microsoft.com/en-us/answers/tags/247/dotnet-maui

@kolkonos
Copy link

Yeah, I wasn't talking about the shell, but as you pointed out, the background of the flyout header and content.
Setting those to have a background of transparent has no effect. Matter of fact, the screen shot I attached already has those properties set to transparent. I believe the internals of how those components are rendered (much like the label on a check box in Windows) is overriding the explicit background property. When inspecting the rendered UI, there are many pieces that are added by the platform, and which I cannot seem to be able to target with regular styling.

I will see if the MAUI team has any guidance for me.

@LanceMcCarthy
Copy link
Author

LanceMcCarthy commented Dec 29, 2023

NEW APPROACH

Hi folks, we no longer need a bunch of custom code. Microsoft now has an easy API to set the backdrop right on the Window class itself:

window.SystemBackdrop = new MicaBackdrop { Kind = MicaKind.BaseAlt };

Here's what it looks like in MauiProgram.cs

using Microsoft.Maui.LifecycleEvents;

#if WINDOWS10_0_17763_0_OR_GREATER
using Hacked.Maui.Platforms.Windows;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml.Media;

#endif

namespace MyMauiApp;

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");
            });

        builder.ConfigureLifecycleEvents(events =>
        {
#if WINDOWS10_0_17763_0_OR_GREATER
            events.AddWindows(wndLifeCycleBuilder =>
            {
                wndLifeCycleBuilder.OnWindowCreated(window =>
                {
                    window.SystemBackdrop = new MicaBackdrop { Kind = MicaKind.BaseAlt };
                });
            });
#endif
         }

        return builder.Build();
    }

Here is the result at runtime (note MicaKind.BaseAlt is more tinted than default Mica):

image

@FHWWC
Copy link

FHWWC commented Dec 29, 2023

That's good! I'm trying now...

@LanceMcCarthy
Copy link
Author

I haven't been able to test it in most situations, so I'd be happy to hear how it goes.

Here are the docs => https://learn.microsoft.com/en-us/windows/apps/design/style/mica#use-mica-with-the-windows-app-sdk

@FHWWC
Copy link

FHWWC commented Dec 30, 2023

It seems cannot put wasdk api on maui...

@LanceMcCarthy
Copy link
Author

LanceMcCarthy commented Dec 30, 2023 via email

@FHWWC
Copy link

FHWWC commented Dec 30, 2023

Oh sorry, I'm forgot to update the nuget package, now works fine.

@ajsuydam
Copy link

ajsuydam commented Jun 6, 2024

@LanceMcCarthy Question for you,

Does this work in shell apps? Ive tried your exact code (and a bunch of other things, i even tried going into the inheritance hierarchy) but nothing seems to work. It feels like .net Maui keeps overriding the default platform behavior.

@LanceMcCarthy
Copy link
Author

@ajsuydam There is a ShellPage background color that is overlaying the window background. I forget the exact trick to change it, but I want to say just make sure the Shell page's background color is Transparent.

@ajsuydam
Copy link

Yeah, I wasn't talking about the shell, but as you pointed out, the background of the flyout header and content. Setting those to have a background of transparent has no effect. Matter of fact, the screen shot I attached already has those properties set to transparent. I believe the internals of how those components are rendered (much like the label on a check box in Windows) is overriding the explicit background property. When inspecting the rendered UI, there are many pieces that are added by the platform, and which I cannot seem to be able to target with regular styling.

I will see if the MAUI team has any guidance for me.

Hey @kolkonos with the new approach that @LanceMcCarthy listed (just doing it in MauiProgram.cs), I'm now running into the same issue that you are. Did you ever find a solution? @LanceMcCarthy I did try setting those properties, no dice sadly.

@LanceMcCarthy
Copy link
Author

@ajsuydam In case you haven't tried it yet, there is an official Discord server for .NET and there's a dedicated MAUI channel. Here's the invite link http://aka.ms/dotnet-discord

@kolkonos
Copy link

@ajsuydam While I was still working on that project, I found some references to creating custom renderes for components. In those articles, which I don’t have handy, they mentioned that you can override the rendering of a built in component by inheriting from it. However I have since had to move off that project, and never tested the suggested approach.
perhaps you can dive into that rabbit hole and keep us posted.

@kolkonos
Copy link

kolkonos commented Jul 18, 2024

Just watched this vid, and it seems to point in the right direction for overriding or defining custom behavior on default controls or custom ones.
https://www.youtube.com/live/tOCh0d4PpOw?si=H4LClSKD37epxtzb

@kolkonos
Copy link

kolkonos commented Nov 12, 2024

This has been a long time coming. To all who are still trying to solve this issue... it's done.

full sample: https://github.com/kolkonos/MicaBackdropMAUI

As Lance mentioned, add the mica effect during the lifecycle event. I prefer the MikaKind.BaseAlt as it is more pronounced (more vivid color).

THEN in the AppShell.xaml.cs in the class constructor:

public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();

        this.Loaded += (_, _) => {
#if WINDOWS        
          var shellView = `Shell.Current?.Handler?.PlatformView` as ShellView;
          var navigationView = shellView?.Content as MauiNavigationView;

          var contentGrid = navigationView?.GetType()
              .GetProperty("ContentGrid", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?
              .GetValue(navigationView) as Microsoft.UI.Xaml.Controls.Grid;

          contentGrid!.Background.Opacity = 0;
#endif
       };
}

It's crucial that you set the opacity to 0 rather than setting the Background to null, as this would cause the framework to fallback to the default.
@ajsuydam
Screenshot 2024-11-12 102719
Screenshot 2024-11-12 102704

@LanceMcCarthy
Copy link
Author

@kolkonos BOOM! Looks great.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment