微软在 Windows 10 Version 1809 上正式发布了新的 UI 框架,命名为 WinUI 3。

这已经是微软发布的第不知道多少个 UI 框架了,但是微软宣称它将支持原生 C++ 和 Win32 应用。这引起了我的注意,因为微软已经很久没有为 Win32 提供新的技术了。

WinUI 3 与 Win32、UWP

按微软的说法,WinUI 3 是同时为 Win32 和 UWP 程序提供支持的,也就是说它应该允许独立运行在 Win32 框架上,不受 UWP 的权限管理限制。

对于 C++ 开发者,WinUI 3 借助 C++/WinRT 有完全的原生 C++ 支持,而不需要 C++/CX 或 C++/CLI 这样剑走偏锋的设计。这无疑对 GCC 或 Clang 上编译 WinUI 3 留下了可能。作为开发者,着实不希望微软带领技术走向分裂。

对于 UI 设计,WinUI 3 继承了 UWP 程序的 XAML 技术,为用户提供了 Fluent 风格的控件和交互体验。也就是说在核心的 UI 开发方式上,还是和 UWP 保持一致的,只是控件风格有所改变。但是 WinUI 3 不受 UWP 复杂的权限约束限制,可以说对 Win32 开发者十分友好了。

创建一个新 WinUI 3 应用

开发环境

开发前按照微软文档配置安装环境。

Install tools for developing apps for Windows 10 and Windows 11

基本上要在最新版本的 Windows 系统和 Visual Studio 上才支持。

创建工程

接下来按照微软文档创建第一个 WinUI 3 项目。

Create your first WinUI 3 project

先安装 WinUI 3 工程模板,使用模板创建工程。

WinUI 3 project templates in Visual Studio

微软文档主要支持 C# 和 C++ 两种开发语言,这里以 C++/WinUI 3 为例。

微软提供了两种空应用程序模板,一种使用 Windows Application Packaging(WAP)打包,另一种使用 MSIX 打包。与常见的 msi、exe 文件的安装包不同,这两种打包方式大多只会在 Windows 应用商店中使用。

其实这里微软也允许创建不被打包的传统程序,这需要用户自行部署依赖环境,这里暂不讨论。

这里先使用微软推荐的新版 MSIX 打包。

核心代码和界面

工程创建后,资源管理器中有不少文件,这里先关注两个重要的 xaml 项。

App.xaml 描述了 Windows 应用的基本属性。

<!-- on App.xaml -->
<Application
    x:Class="WinUIDemo.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUIDemo">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
                <!-- Other merged dictionaries here -->
            </ResourceDictionary.MergedDictionaries>
            <!-- Other app resources here -->
        </ResourceDictionary>
    </Application.Resources>
</Application>

与其关联的 App.xaml.hApp.xaml.cpp 实现了 WinRT 主应用类,这里封装了程序入口函数。

// on App.xaml.h
struct App : AppT<App>
{
    App();

    void OnLaunched(Microsoft::UI::Xaml::LaunchActivatedEventArgs const&);

private:
    winrt::Microsoft::UI::Xaml::Window window{ nullptr };
};
// on App.xaml.cpp
void App::OnLaunched(LaunchActivatedEventArgs const&)
{
    window = make<MainWindow>();
    window.Activate();
}

入口很简单,创建主窗口并激活。

MainWindow.xaml 描述了应用的主窗口界面,MainWindow.xaml.hMainWindow.xaml.cpp 实现了主窗口类,这里封装了窗口事件回调。

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

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
    </StackPanel>
</Window>

主窗口描述了一个居中的堆叠布局,里面有一个按钮 myButton

// on MainWindow.xaml.h
struct MainWindow : MainWindowT<MainWindow>
{
    MainWindow();

    int32_t MyProperty();
    void MyProperty(int32_t value);

    void myButton_Click(
        Windows::Foundation::IInspectable const& sender, 
        Microsoft::UI::Xaml::RoutedEventArgs const& args);
};
// on MainWindow.xaml.cpp
void MainWindow::myButton_Click(IInspectable const&, RoutedEventArgs const&)
{
    myButton().Content(box_value(L"Clicked"));
}

主窗口类也很简单,当按钮 myButton 点击时,替换其内容文本。

事件绑定

和传统的 Win32 UI 技术不同,MainWindow 类并没有消息处理函数,也无需处理消息循环,但是这个 myButton_Click 函数却可以响应 MainWindow.xaml 中描述的 myButton 按钮的点击事件。

追踪 myButton_Click 函数的调用,可以发现调用者在 MainWindow.xaml.g.hpp 文件中。

// on MainWindow.xaml.g.hpp
template <typename D, typename ... I>
void MainWindowT<D, I...>::Connect(int32_t connectionId, IInspectable const& target)
{
    switch (connectionId)
    {
    case 2:
        {
            auto targetElement = target.as<::winrt::Microsoft::UI::Xaml::Controls::Button>();
            this->myButton(targetElement);
            auto weakThis = ::winrt::make_weak<class_type>(*this);
            targetElement.Click([weakThis](
                ::winrt::Windows::Foundation::IInspectable const& p0, 
                ::winrt::Microsoft::UI::Xaml::RoutedEventArgs const& p1){
                    if (auto t = weakThis.get())
                    {
                        ::winrt::get_self<D>(t)->myButton_Click(p0, p1);
                    }
                });
        }
        break;
    }
    _contentLoaded = true;
}

这个文件是 C++/WinRT 对 MainWindow.xaml 描述的对象生成的 C++ 代码。通过控件连接接口 IComponentConnector 实现了对控件对象点击事件回调的绑定。

运行

编译运行模板工程。

屏幕截图_2022-02-08_015501.png

测试了下这个窗口,无论是执行速度、字体渲染还是高 DPI 缩放都处理的很不错。不得不说,微软在技术实力和设计大框架上一直都让人很佩服(笑)。

添加自定义页面

下面我们添加一个自己页面,在工程中插入一个新的 WinUI 3 空白页 RootPage.xaml

界面和代码参考微软提供的 XAML 控件演示工程。

Xaml-Controls-Gallery

<!-- on RootPage.xaml -->
<Page
    x:Class="WinUIDemo.RootPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUIDemo"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <NavigationView x:Name="RootNavigation">
        <NavigationView.MenuItems>
            <NavigationViewItem Icon="Play" Content="Menu Item1" Tag="SamplePage1" />
            <NavigationViewItem Icon="Save" Content="Menu Item2" Tag="SamplePage2" />
            <NavigationViewItem Icon="Refresh" Content="Menu Item3" Tag="SamplePage3" />
            <NavigationViewItem Icon="Download" Content="Menu Item4" Tag="SamplePage4" />
        </NavigationView.MenuItems>
        <Frame x:Name="RootFrame"/>
    </NavigationView>
</Page>

在 Page 中描述一个侧边导航栏,和一个主页面框架,作为应用的基础结构。

// on App.xaml.cpp
void App::OnLaunched(LaunchActivatedEventArgs const&)
{
    window = make<MainWindow>();
    auto rootPage = make<RootPage>();
    window.Content(rootPage);
    window.Activate();
}

在入口函数中,把创建的页面对象塞进窗口中,一个简单的应用就搭好了。

屏幕截图_2022-02-08_025427.png

参考

作者:lyincc

版权:本文采用 「CC BY 4.0」 知识共享许可协议进行许可。

地址:https://lyincc.com/tech/cpp-winui-3-note-01/