Maui学习之路(三)--Winui3深入探讨

学习Maui已经有一段时间,随着不断地深入,对Maui有了一些初步的了解。

我们都知道Maui为了保持平台原生特性,所以在每一个平台都使用了平台自身的原生开发框架,如在Windows系统使用了Winui3作为UI框架,Mac平台使用了UIKit作为UI框架,(我愿称之为“套娃”或者是对不同平台做了一层“抽象”),所以如果直接操作Maui的对象的某个属性或者方法实际他背后去转调了平台相关的属性或者方法。.

有了这样的认识那么我们就会深刻的理解到如果我想改变某些东西,其实也可以自己去直接操作平台相关的方法,而并不一定需要操作Maui对象(如果Maui未提供这样的能力,你只能这么做)。

在做Windows桌面程序开发时,我们常常有这样的需求,你的exe程序同一时间只能运行一份(单例),你很希望程序打开就全屏,别人不能轻易关闭(通常在工业领域这样的需求极大)。

基于以上需求,我们来深入了解学习一下,如何实现:

首先使用Maui模板创建的每一个工程有有一个Platforms的目录,这个本质上是对应多平台的一个工程集合(在Xamarin中如果你需要构建一个真正的跨平台程序,实际是做不到的,你需要通过Xamarin.Form的模板创建不同平台的入口,然后抽出公用的平台无关逻辑来作为跨平台的共享资源),有了这个设计Maui真正实现了一份代码到处编译到处运行,而你在别的平台编译并不需要对工程做任何特别的改动(目前支持MacWindows平台编译)(这里不做深入探讨,如果你想了解MauiXamarin工程上的区别请看这个视频:.NET MAUI 跨平台开发合集_哔哩哔哩_bilibili)

Maui学习之路(三)--Winui3深入探讨

其次对于Maui的可执行工程(非dll)来说,对应平台的程序的真正入口并非是MauiProgram 或MauiApplication,他实际是Platforms下对应的平台的Main或者是App,比如对Window来说,Maui启动程序的真正入口是Platforms/Windows/App这个对象。

Maui学习之路(三)--Winui3深入探讨
Window单例实现

有了上面的认识作为基础,那么实现一个单例非常简单,在Wpf或者Winform上做过相同的设计,如今只需要搬运过来即可(在Wpf或是Winform实现单例的方式很多,最常用的是使用Mutex)实现方式请查阅:零食栏 - .NET MAUI Community Toolkit - .NET Community Toolkit | Microsoft Docs 在Platform/Windows/App.xaml.cs中增加单例检查

public partial class App : MauiWinUIApplication
{
    /// <summary>
    /// Initializes the singleton application object.  This is the first line of authored code
    /// executed, and as such is the logical equivalent of main() or WinMain().
    /// </summary>
    public App()
    {
        this.InitializeComponent();
    }

    static Mutex? __SingleMutex;
    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
    protected override void OnLaunched(LaunchActivatedEventArgs args)
    {
        if (!IsSingleInstance())
        {
            //Process.GetCurrentProcess().Kill();
            Environment.Exit(0);
            return;
        }
        base.OnLaunched(args);
    }
    static bool IsSingleInstance()
    {
        const string applicationId = "813342EB-7796-4B13-98F1-14C99E778C6E";
        __SingleMutex = new Mutex(false, applicationId);
        GC.KeepAlive(__SingleMutex);

        try
        {
            return __SingleMutex.WaitOne(0, false);
        }
        catch (Exception)
        {
            __SingleMutex.ReleaseMutex();
            return __SingleMutex.WaitOne(0, false);
        }
    }
}
实现一个无边框窗体

在这之前我其实已经写过一篇Maui在windows上实现无边框的方式,有兴趣的同学可以去查阅一下(链接:Window窗体设置)。Maui实际提供了一名为Window的类型,不过很可惜这个类型中并没有任何有设置窗口相关的属性(比如:宽,高,背景色等等,另外也没法通过修改Style来修改样式),很明显Maui是一个跨平台的设计,因为在移动端并不存在所谓的窗口大小概念,通常打开都是全屏。如果我们需要对窗体进行修改,那么需要拿到Window对象后面管理的Native对象才行

获取Native对象

  • 方法1:注册Maui提供的生命周期函数

在不同的平台会推送相关的程序生命周期通知(在这里可以获取到平台相关操作对象包括对应的ApplicationWindow),使用这个方式存在一些弊端就是当使用多窗体方案时,你没法定位哪个是主窗体(当然第一个创建的必然就是主窗体了),详情请看:应用生命周期 - .NET MAUI | Microsoft Docs(这里不做过多探讨如果你像知道细节可以看这个视频:.NET MAUI BLAZOR 生命周期_哔哩哔哩_bilibili)

  • 方法2:访问Window下的Handler属性

Maui Window类型中存在这样一个属性Handler(类型是IElementHandler,实现类型是ElementHandler)(所有的Maui控件对象都存在这个个属性),该属性中记录了平台的Native映射对象(如果你希望修改native对象的外观,可以访问他下属的属性PlatformView(将其转换成对应平台的native对象))(注意在刚开始new Window对象时Handler对象并不存在,你可以注册HandlerChanged事件捕获他的变化)(在Winodw平台PlatformView对象是Microsoft.UI.Xaml.Window类型)

学习必要的Winui3相关知识

Winui开始微软带来了窗口的全新设计,如果需要实现诸如窗口大小修改,标题栏修改,全屏实现等等功能你需要学习如下知识:

  • 自定义标题栏

链接:标题栏自定义 - Windows apps | Microsoft Docs

  • AppWindow

这是Win10之后引入的窗口操作对象,学习链接:使用 AppWindow 类显示应用的辅助窗口 - Windows apps | Microsoft Docs以及AppWindow Class (Microsoft.UI.Windowing) - Windows App SDK | Microsoft Docs

注意:官网相关的学习资料在不同的文档中介绍存在偏差,主要是部分设计是老设计,尚未及时更新,请以Windows App SDK 1.1版本为准

通过学习以上知识,我们可以进行部分功能定制

  1. 更改窗口尺寸:
        var winuiWindow = Window.Handler?.PlatformView as MicrosoftuiXaml.Window;
        if (winuiWindow is null)
            return;
        var appWindow = winuiWindow.GetAppWindow();
        if (appWindow is null)
            return;

        var displyArea = MicrosoftuiWindowing.DisplayArea.Primary;
        double scalingFactor = winuiWindow.GetDisplayDensity();
        var width = 800 * scalingFactor;
        var height = 600 * scalingFactor;
        double startX = (displyArea.WorkArea.Width - width) / 2.0;
        double startY = (displyArea.WorkArea.Height - height) / 2.0;

        appWindow.MoveAndResize(new((int)startX, (int)startY, (int)width, (int)height), displyArea);
  1. 最大化(使用Win32消息):
        var winuiWindow = Window.Handler?.PlatformView as MicrosoftuiXaml.Window;
        if (winuiWindow is null)
            return;

        var windowHanlde = winuiWindow.GetWindowHandle();
        User32.PostMessage(windowHanlde, WindowMessage.WM_SYSCOMMAND, new IntPtr((int)SysCommands.SC_MINIMIZE), IntPtr.Zero);
  1. 最小化(使用Win32消息):
        var winuiWindow = Window.Handler?.PlatformView as MicrosoftuiXaml.Window;
        if (winuiWindow is null)
            return;

        var windowHanlde = winuiWindow.GetWindowHandle();
        User32.PostMessage(windowHanlde, WindowMessage.WM_SYSCOMMAND, new IntPtr((int)SysCommands.SC_MINIMIZE), IntPtr.Zero);

  1. 全屏:
        var winuiWindow = Window.Handler?.PlatformView as MicrosoftuiXaml.Window;
        if (winuiWindow is null)
            return;
        var appWindow = winuiWindow.GetAppWindow();
        if (appWindow is null)
            return;

        //注意由于Maui默认开启了扩展TitleBar(标题栏融合模式?)所以先要去掉 否则全屏仍然会出现 关闭等按钮
        //虽然关闭了标题栏融合模式,但是全屏时仍然会存在一个类似标题栏的东西,如果需要处理需要进行深度定制(可以查看我的github项目)
        winuiWindow.ExtendsContentIntoTitleBar = false;
        appWindow.SetPresenter(MicrosoftuiWindowing.AppWindowPresenterKind.FullScreen);
  1. 修改Maui默认标题栏颜色:
        var winuiWindow = Window.Handler?.PlatformView as MicrosoftuiXaml.Window;
        if (winuiWindow is null)
            return;

        var application = MicrosoftuiXaml.Application.Current;
        var res = application.Resources;

        //看到这里你一定会疑惑为什么是这样,如果你有兴趣,可以查阅Winui3的源码
        res["WindowCaptionBackground"] = new MicrosoftuixmlMedia.SolidColorBrush(Microsoftui.Colors.Red);

        //修改标题栏后需要主动刷新才会生效(否则需要你人为进行一次最小化处理)
        TriggertTitleBarRepaint();

以上Demo都已经上传Github 地址:WPFDevelopersOrg/Demo,请查阅MauiApp1这个demo

最后放一个目前我已经实现的Maui Win11的演示效果。

该项目已上传github地址:WPFDevelopersOrg/MauiToolkit