WinUI 3 新手踩坑

WinUI 3已经发布很久了,以前也简单尝试了一下,但是因为各种问题,比如没有亚克力背景、文档混乱等没有继续尝试,最近看WindowsApp SDK1.4发布了,刚好很久没折腾了就决定用一下WinUI 3写个应用,结果没想到还是有一堆坑,不过折腾了两天也算是做出来了想要的东西了,所以写一篇文章记录一下各种坑,也简单记录一下作为新手学习使用时的一些困惑和解决方案。

1、亚克力和Mica背景

WindowsApp SDK1.3才支持使用亚克力或Mica作为应用背景,这是我迟迟观望的主要原因。亚克力材质真的很好看,我使用的应用只要支持我都会开启,但是似乎是由于性能原因导致微软一直没能推出背景亚克力和Mica材质。1.3版本发布后我第一时间根据指引尝试了一下,但是发现很复杂,要想在1.3版本中使用亚克力或Mica背景材质,需要你自己手动获取窗口和控制器,如文档所述:

将 SystemBackdropController 与 WinUI 3 XAML 配合使用 – Windows apps | Microsoft Learn

而在1.4版本中就很简单了,只需要在xmal中加上一个标签,然后在对应的cs文件中添加一行代码即可,如文档所述:

将 SystemBackdropController 与 WinUI 3 XAML 配合使用 – Windows apps | Microsoft Learn

但是这样做是有个问题的,就是标题栏没法应用材质,会导致应用不统一,观感不太好,虽然在Win11上看起来似乎还不错,但是如果在Win10上标题栏就是灰色一片。这是没有应用材质的标题栏:

为了解决这一问题,可参考以下文档:

标题栏自定义 – Windows apps | Microsoft Learn

另外,实测还可直接在窗口对应的cs文件中使用如下代码完成:

    public DashboardWindow()
    {
        InitializeComponent();
        SystemBackdrop = new DesktopAcrylicBackdrop();
        ExtendsContentIntoTitleBar = true;
    }

这样做的坏处是没了标题栏,不过也可以自己设定一个标题栏,这样甚至还能对标题栏进行完全自定义:

<?xml version="1.0" encoding="utf-8"?>

<Window
    x:Class="Dashboard.DashboardWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Dashboard"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Window.SystemBackdrop>
        <DesktopAcrylicBackdrop></DesktopAcrylicBackdrop>
    </Window.SystemBackdrop>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition MaxHeight="30"></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <Grid Grid.Row="0" x:Name="AppTitleBar" VerticalAlignment="Top">
            <TextBlock x:Name="AppTitleTextBlock" Text="66666"
                       TextWrapping="NoWrap"
                       VerticalAlignment="Center"
                       Margin="28,6,0,6" />
        </Grid>
        <Grid Grid.Row="1">
            <NavigationView x:Name="NavigationView">
                <NavigationView.MenuItems>
                    <NavigationViewItem Content="A" />
                    <NavigationViewItem Content="B" />
                </NavigationView.MenuItems>

                <Frame x:Name="ContentFrame" />
            </NavigationView>
        </Grid>
    </Grid>

</Window>

对应cs文件中:

public DashboardWindow()
{
    InitializeComponent();
    ExtendsContentIntoTitleBar = true;
    AppTitleTextBlock.Text = Package.Current.DisplayName;
    SetTitleBar(AppTitleBar);
}

这样就完成了标题栏自定义以及亚克力或Mica材质的应用。

2、获取应用信息

继续上文,我们可以在标题栏里添加文本框来显示应用名称,但是怎么样才能自动获取应用名称,而非手动设置呢?在Windows19041以后版本中,可以使用 AppInfo.Current 来获取当前应用的信息,如下所示:

public DashboardWindow()
{
    InitializeComponent();
    CurrentWindow = GetAppWindowForCurrentWindow();
    SystemBackdrop = new DesktopAcrylicBackdrop();
    AppTitleTextBlock.Text = AppInfo.Current.DisplayInfo.DisplayName;
    ExtendsContentIntoTitleBar = true;
}

但是如果要在此版本以前的Windows上运行时,就没法这么做了,此时可以使用如下代码获取信息:

public DashboardWindow()
{
    InitializeComponent();
    SystemBackdrop = new DesktopAcrylicBackdrop();
    ExtendsContentIntoTitleBar = true;
    AppTitleTextBlock.Text = Package.Current.DisplayName;
    SetTitleBar(AppTitleBar);
}

3、使用控件库

实际开发时,基本不会自己纯手搓全部组件,微软有提供一些组件库以供使用。一个是WinUI3控件库:

microsoft/microsoft-ui-xaml: Windows UI Library: the latest Windows 10 native controls and Fluent styles for your applications (github.com)

对应的示例应用为:

WinUI 3 Gallery

另一个是Windows Community Toolkit:

CommunityToolkit/Windows: Collection of controls for WinUI 2, WinUI 3, and Uno Platform developers. Simplifies and demonstrates common developer tasks building experiences for Windows with .NET. (github.com)

对应的示例应用为:

Windows Community Toolkit Gallery

两个都可以在WinUI 3中使用,使用起来基本上都有示例代码和文档。不过在使用Windows Community Toolkit时要注意,此库已经更新至8.x版本,此版本有大量的改动,最大的改动就是,不能再直接使用Nuget中的 CommunityToolkit.WinUI 这个包了,这个包还是7.x版本的,文档里很多内容都是针对8.x版本的。

例如,阴影概述 这篇文档里描述的AttachedCardShadow,就没法在7.x版本的CommunityToolkit.WinUI中找到并使用。但是当你想更新这个包时,你会发现完全没有8.x版本可以进行安装。

这是因为8.x版本开始,这个包就被拆分了,如果想使用AttachedCardShadow,就要使用CommunityToolkit.WinUI.Media 这个包。对应的使用文档在:

附加的投影 – Community Toolkits for .NET | Microsoft Learn

如果你在微软文档里搜索AttachedCardShadow,你会发现大部分都是UWP的内容,少数描述为WinUI的文档实际内容也大部分都是UWP,非常混乱。最后发现,在使用CommunityToolkit.WinUI时,应该参考的是下面这个文档集合:

Windows 社区工具包文档 – Community Toolkits for .NET | Microsoft Learn

这里真的很想吐槽一下,虽然说微软的文档确实算很全面的了,但是不知道是不是因为太多东西了,人手又没那么多,东西变动可能也太快了,看文档的时候非常难找到想要的内容,而且很多文档看起来几乎一模一样,但是细节又大相径庭,看得真是糟心。而且很多东西都没有指引,只有一个API说明,具体的使用框架很多都没说,逼得人只能一遍又一遍看API接口,自己猜应该怎么用。

好的文档应该至少要说明某些功能的使用框架和方式,更好的文档会说明代码的设计思路,当然,写文档确实是一件很繁琐的事情,甚至写博客也是这样(比如我现在就想赶紧写完这篇然后打无限火力😂)

2024-03-27 更新:
最近又发现一个看起来很不错的WinUI 3的控件库WinUICommunity,这个作者自己还维护了一个叫HandyControls的HandyControl分支库,添加了一些其他控件在里边。WinUICommunity 这个控件库包含了很多东西,还提供有自定义程度更高的亚克力、Mica背景,除了微软自己的库之外可以试试看这个。

WinUICommunity: WinUICommunity/WinUICommunity: WinUICommunity is a collection of useful classes, controls, styles, and codes for WinUI 3. Create a WinUI 3 app in less than a minute with the built-in project templates and scaffolding tools. (github.com)

4、自定义鼠标样式

如果你想对某个控件自定义鼠标样式,需要重新创建一个类,继承你想自定义的控件,然后写一个公开的方法,允许修改鼠标样式,就像下面这样:

using System.Numerics;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;

namespace Example.CustomUIElement;

public class CursorStackPanel : StackPanel
{
    public void ChangeCursor(InputCursor cursor)
    {
        ProtectedCursor = cursor;
    }

    // 以下其余代码是为了在点击时让此控件向右下移动一些以对点击行为做出反馈
    public CursorStackPanel() : base()
    {
        PointerPressed += OnPointDownMove;
        PointerReleased += OnPointReleasedReset;
    }

    // 点击时让此控件向右下移动一些
    private void OnPointDownMove(object sender, PointerRoutedEventArgs pointerRoutedEventArgs)
    {
        ((UIElement)sender).Translation = new Vector3(2, 2, 0);
    }
    
    // 松开点击后恢复原位
    private void OnPointReleasedReset(object sender, PointerRoutedEventArgs e)
    {
        ((UIElement)sender).Translation = Vector3.Zero;
    }
}

注意并不需要你再写XAML了,只需要简单地继承一下就可以了,为什么要这样做是因为鼠标样式属性ProtectedCursor是一个Protected属性,没有办法从外部直接调用,当然如果你要使用反射获取然后设置的话也可以,只不过赶紧更麻烦了一些。

在你要使用的地方,引入你自定义的控件:

<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"
            Spacing="32" Background="Transparent"
            Orientation="Horizontal">
    <customUiElement:CursorStackPanel
        x:Name="Sing" Height="360" Width="271.5"
        Background="White" ui:Effects.Shadow="{StaticResource CommonShadow}"
        VerticalAlignment="Center" HorizontalAlignment="Center"
        PointerReleased="OnClickToSing">
        <Image Source="/Assets/sing.svg" Stretch="Fill" Height="200" Margin="0 30 0 0" />
        <TextBlock HorizontalAlignment="Center"
                   Margin="0 40 0 0" FontSize="30">
            开始欢唱
        </TextBlock>
    </customUiElement:CursorStackPanel>
</StackPanel>

对应的cs文件中:

public MainWindow()
{
    InitializeComponent();
    Sing.ChangeCursor(InputSystemCursor.Create(InputSystemCursorShape.Hand));
}

5、页面与窗口

如果你跟我一样对前端开发比较熟悉,那你可能会和我一样在使用WinUI 3或WPF时感到痛苦,HTML中异常简单的各种样式效果,比如阴影、边框、定义鼠标指针,到了XAML中就麻烦得要死,这是因为设计的理念不同(虽然我觉得就应该像HTML这样简简单单的最好了)。

一个很大的差异就是窗口的使用。在HTML中,如果你使用过Vue或者React之类的框架,那你一定对路由、页面之类的概念很熟悉。在桌面开发中,传统的WinForm一般都是使用各种窗口完成开发。在WPF和UWP中,应用越来越趋向于仅使用一个窗口完成工作了。我曾经开发过一些WinForm和WPF的小工具,比较简单,都是一个个窗口组合完成各种工作。但是如果看现在的很多UWP或者WinUI 3应用,你会发现也出现了各种导航栏。如果你有看过一些应用的代码,你更是会发现都是用的各种Frame和Page,并不是一个个窗口了。

实际上,这就和前边提到的Vue这些框架中的路由和页面很像了,你可以把Frame当成Vue中的<router-view>,把Page当成Vue中的组件,这样类比起来就会很简单。

比如下面这个示例就创建了一个经典的侧边导航布局:

<?xml version="1.0" encoding="utf-8"?>

<Window
    x:Class="Example.Dashboard.DashboardWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Example.Dashboard"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Window.SystemBackdrop>
        <DesktopAcrylicBackdrop></DesktopAcrylicBackdrop>
    </Window.SystemBackdrop>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition MaxHeight="30"></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <Grid Grid.Row="0" x:Name="AppTitleBar" VerticalAlignment="Top">
            <TextBlock x:Name="AppTitleTextBlock" Text="66666"
                       TextWrapping="NoWrap"
                       VerticalAlignment="Center"
                       Margin="28,6,0,6" />
        </Grid>
        <Grid Grid.Row="1">
            <NavigationView x:Name="NavigationView" ItemInvoked="OnClickRouteItem">
                <NavigationView.MenuItems>
                    <NavigationViewItem Content="歌手" />
                    <NavigationViewItem Content="歌曲" />
                </NavigationView.MenuItems>

                <Frame x:Name="ContentFrame" />
            </NavigationView>
        </Grid>
    </Grid>

</Window>

其中的Frame标签就是用来放路由对应的页面的,如果你想在点击时导航到对应的页面去(也就是改变Frame的内容),你可以使用如下代码完成:

private void OnClickRouteItem(NavigationView sender, NavigationViewItemInvokedEventArgs args)
{
    ContentFrame.Navigate(typeof(Singers));
}

对Frame的各种导航操作就不细说了,微软的API文档里已经将所有可用操作列出来了。

留下评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Captcha Code