🤖 作者:包瑞清(richie bao): lastmod: 2025-01-07T09:39:35+08:00

10.1 框架选择, VS 安装项

10.1.1 框架选择

Windows 提供了广泛的编程语言、框架(frameworks)和工具来构建应用程序,包括WinUI、UWP(Universal Windows Platform)、桌面版 React Native、WPF(Windows Presentation Foundation)、Windows Forms,以及多种跨平台跨平台框架。支持的编程语言包括 C++、C#,JavaScript等。针对 Windows 开发应用程序的多种选择,其最佳选项取决于用户对应用程序的需求、编程语言的选择和对相关技术的熟悉程度。表列出了当前 Windows 最流行的应用开发框架(App Development Frameworks)及其支持的功能。

表:应用开发框架功能比较(表引自,Overview of framework options

Feature(功能)\框架 .NET MAUI Blazor Hybrid React Native for Desktop UWP XAML (Windows.UI.Xaml) Win32 (MFC or ATL) Windows Forms WinUI 3 WPF
Language(支持的语言) C# C# JavaScript, TypeScript C#, C++, Visual Basic C++, Rust C#, Visual Basic C#, C++ C#, Visual Basic
UI language(用户界面语言) XAML/Code Razor JSX XAML Code Code XAML XAML
UI designer(用户界面设计器)
UI debugging(用户界面调试) Hot Reload Hot Reload Fast Refresh Hot Reload - Hot Reload Hot Reload Hot Reload
Fluent Design ✅ (via WinUI 2)
.NET .NET .NET N/A .NET Core & .NET Native N/A .NET & .NET Framework .NET .NET & .NET Framework
Windows App SDK(Software Development Kit) ✅ (more info) via MAUI ✅ (more info) ✅ (more info) ✅ (more info)
Great for touch(适合触摸操作)
Cross-platform(跨平台)
Xbox/HoloLens apps
Sandboxing (AppContainer)
Currently supported(当前支持)
Receiving updates(接收更新) ✅ (security & bugfix)
Roadmap(开发路径) GitHub GitHub GitHub n/a n/a GitHub GitHub GitHub

XAML(Extensible Application Markup Language),是微软开发的标记语言,主要用于构建用户界面(UI),允许开发者通过简单的 XML 结构定义控件、布局、样式和事件,类似于 HTML 和 XML,可以清晰的分离界面的结构(标记部分)与逻辑(代码部分)。 XAML 是一种声明式语言(Declarative Language),可以使用一种语言结构初始化对象并设置对象的属性,这种结构展示了多个对象之间的层级关系,并通过支持扩展类型的类型约定来扩展类型,这意味着开发者通过 XAML 语言定义界面元素的外观和布局,而不是通过编程方式逐步构建界面。声明式的方式使得界面的结构清晰易懂,且易于维护。XAML 语言支持在开发过程中不同工具和角色之间交换源代码,例如在设计工具和互动开发环境(Interactive Development Environment,IDE)之间交换 XAML 源代码,或在开发人员之间交换源代码。通过 XAML 作为交换格式,可以保持设计师角色和开发者角色的独立性或将其结合起来,并在开发过程中不断迭代代码。XAML 语言文件扩展名通常为 .xaml,可以通过可视化设计工具(用户界面设计,UI designer)进行编辑和预览,并支持数据绑定,允许控件的属性与后台数据模型直接连接,使得 UI 更新与数据源保持同步,便于实现响应式界面,是现代 Windows 应用程序开发不可或缺的组成部分。

Fluent Design (System) ,是微软提出的一种设计语言,旨在提供更加现代化、灵活且互动性强的体验,其首次在 Windows 10 中提出,并随着 Windows11 及其他微软产品的开发不断演进。Fluent Design 强调视觉、声音、动画和触感的统一,旨在为开发者提供更直观和丰富的互动界面,其设计理解基于的主要核心要素有光纤(Light)、深度(Depth)、运动(Motion)、材质(Material)和缩放(Scale)等。Fluent2 是 Fluent Design(Fluent1)的迭代版本,提供了更多的视觉效果和更强大的互动支持,着重提升了现代操作系统和应用的用户体验,更具沉浸感和动态感的交换界面。WinUI3 是 Windows App SDK 的一部分,包含了 Fluent2 的最新设计控件和效果。开发者可以通过 WinUI 控件轻松实现 Fluent2 风格的 UI,为开发现代桌面和跨平台应用的一个重要设计语言,已帮助开发者创建更美观、直观且一致的用户界面。

Windows App SDK,是微软提供的一组新的开发者组件和工具,代表了 Windows 应用开发平台的下一次演进,其提供了一套任何 Window11 上的桌面应用都可以一致使用的统一的应用程序接口(Application Programming Interface ,APIs) 和组件,并且向下兼容 Windows10,版本1809及以上。Windows APP SDK 并不替代 Windows SDK 或现有的桌面 Windows 应用类型,例如 .NET(包括 Windows Forms 和 WPF)及使用 C++ 的桌面 Win32 应用。相反,Windows App SDK补充了这些现有工具和应用类型,提供了一套开发者可以在这些平台上依赖的公共 API,包括 WinUI3。

通过参考应用开发框架功能比较表,选择的框架应该能够同时支持 C++ 和 C#,以便比较演示两种语言的应用开发;并支持 XAML 标记语言,方便构建 UI;最好支持 Fluent Design,用于建立现代感十足的界面设计; 且为微软当前和未来持续维护更新,主流的框架。因此选择 WinUI3 框架构建应用开发程序。

WinUI3 是微软推出的一个现代化 UI 框架,专为 Windows 应用开发而设计,是 Windows App SDK 的一部分。WinUI3 是在 UWP(Universial Windows Platform)和 WPF(Windows Presentation Foundation)的基础上发展而来,旨在提供更加一致和灵活的跨平台开发体验,支持 Windows10 和 Windows11 应用的开发。WinUI3 的核心特性和优势包括:

  1. 现代化的 UI 控件和样式。WinUI3 包含了 Fluent2 的最新设计控件和效果,可以帮助开发者创建更美观、现代、和响应式的应用界面;并支持更高效的自定义样式。
  2. 跨平台支持。WinUI3 支持在 Windows10 和Windows11 上运行,且在未来支持更多的 Windows 设备(如 PC、平板和可穿戴设备)和平台。
  3. 支持桌面应用。WinUI3 允许开发者创建传统的桌面应用程序,而不仅仅是 UWP 应用,并兼容现有的 Windows 桌面技术,如 WPF 和WinForms(Wndows Forms)。对于传统桌面开发,WinUI3 提供了同一的 UI 和控件,以便开发者更加方便的开发现代化的桌面应用。
  4. 高性能和可扩展性。WinUI3 提供了高性能的 UI 渲染,可以在不同的设备上运行时提供流畅的动画、图形和交互;并支持 MVVM (Model-View-ViewModel)架构,帮助分离数据层、界面层和逻辑层,有助于提高应用的可维护性和扩展性。
  5. 与 .NET 和 C++ 兼容。 WinUI3 支持 C# 和 XAML 开发,可以通过 .NET 和 C# 编写应用程序的逻辑;并支持 C++/WinRT,使得开发者可以使用 C++ 语言来构建 Windows 应用程序。
  6. 支持多平台和开发工具。通过 Windows App SDK,WinUI3 可以与其他现代 Windows 开发工具和库集成;并支持 Visual Studio、Visual Studio Code 等开发环境。

10.1.2 技术/组件关系梳理

前文中涉及到框架 WinUI,标记语言 XAML, C++/WinRT,Windows App SDK 和 Fluent Design System 等技术,其之间的关系如表,

技术/组件 作用/功能 关系 说明
WinUI3 现代化 UI 框架,用于构建 Windows 应用用户界面 与 XAML 配合使用,构建 UI;与 Fluent Design System 配合,提供现代化的设计风格 提供一套现代 UI 控件和设计规范,支持响应式和动态 UI 元素
XAML 用于声明式界面设计的标记语言 与 WinUI3 配合, 定义界面元素和布局 声明界面结构、支持数据绑定和与后台(代码)逻辑的交互
C++/WinRT C++ 与 WinRT API 的互操作库 使 C++ 开发者能够调用 WinRT API,支持 Windows 应用开发 作为后台代码(backing code),处理应用的业务逻辑,操作系统服务,及与 UI 的交互
WinRT Windows Runtime API, 提供底层操作系统服务 被 C++/WinRT 或 C# 访问,提供文件管理,UI 控件等服务 提供应用开发所需的操作系统 API,可通过 C++/WinRT 调用
Window App SDK 开发框架,整合多种工具和库 整合了 WinUI3、C++/WinRT、WinRT,为开发者提供跨版本的 Windows 应用开发工具 简化 Windows 应用开发,支持最新功能和跨版本的兼容性
Fluent Design System 设计语言,提供现代的视觉风格 在 WinUI3 中实现,用于增强 UI 的外观和交互性 提供光影、透明度、动画等视觉效果,通过 WinUI3 实现控件的外观设计

WinUI3 是开发 Windows 应用界面的框架,结合了标记语言 XAML 和 Fluent Design System 实现现代化的用户界面设计;C++/WinRT 和 WinRT 提供底层 API 支持,供开发者用高性能代码实现应用逻辑;Windows App SDK 将所有开发工具(如 WinUI、WinRT 等)打包,成为现代 Windows 应用开发的基础。

10.1.3 VS 安装项

在安装 Visual Studio[2022](VS)时,需要安装用于开发 WinUI 和 Windows App SDK 所需的工作负载(workloads)和组件(components)。如果已完成安装,但没有勾选相关负载和组件时,则可以打开 Visual Studio Installer应用,选择修改以添加所需负载和组件。在 VS 安装程序的 workloads 选项卡中,使用 Windows App SDK,开发 C# 应用程序,需要选择负载Windows application development。如果开发 C++ 应用程序,则还需要在Installation Details(安装详情)面板中勾选C++ WinUI app development tools,如图。

PYC icon

图 10-1 VS 安装勾选模块

Windows 为开发者提供了一种特殊模式,可以调整安全设置以运行正在开发的应用程序。在使用 VS 构建、部署和测试应用程序前需要启用Developer Mode,其位于Windows 设置->System->For developers页面下。

VS 项目模板包括快速创建应用程序所需的所有文件,在从 WinUI3 应用程序模板VS->Create a new project->Blank App, Packaged (WinUI 3 in Desktop)(如图)创建项目后(项目名称起名为WinUI3Cpp),就已经内置了一个可以运行的应用程序(如图),可以在Soluution Explorer中查看文件结构,并打开文件查看代码。执行Start Debugging[Local Machine](快捷键 F5)后,结果如图,可以用Live Visual TreeXAML Live Preview.xaml代码行交互定位来辅助设计 UI 界面,诊断可能出现的问题,调试代码。同时,编译该项目,部署到本地机器上,并在调试模式下运行,结果如图。

PYC icon

图 10-2 选择 Blank App, Packaged (WinUI 3 in Desktop)

PYC icon

图 10-3 打开应用程序,查看文件结构

PYC icon

图 10-4 运行后的界面,查看 Live Visual Tree,XAML Live Preview 及 Diagnostic Tools

PYC icon

图 10-5 应用程序运行结果,点击按钮前后变化

Windows App SDK(含 WinUI)作为 NuGet 包发布,意味着更新可能会与 Windows 和 VS 的更新不同步。因此,创建项目的 VS 模板可能没有引用最新的 Windows App SDK NutGet 包。为了确保获得最新的功能和修复,建议每次在 VS 中创建新项目时更新 NutGet 包,其位于 VS->Tools->NuGet Package Manager->Manage NuGet Packages for Solution...下。

10.2 默认应用(C++)代码解读和 Hello world!

10.2.1 默认应用(C++)代码解读

Blank App, Packaged (WinUI 3 in Desktop)模板创建了一个带有交互式按钮的窗口(图10-5),当点击按钮后,显示在按钮上的文字Click Me会变为Clicked

10.2.1.1 项目文件结构

首先查看Solution Explorer(解决方案的资源管理器),即项目的文件结构,如图10-3,各文件的具体解释如下表,

表,文件说明,参考调整于 Build a Hello World app using C# and WinUI 3 / Windows App SDK(https://learn.microsoft.com/en-us/windows/apps/how-tos/hello-world-winui3)

项(Item) 描述(Description)
Solution 'WinUI3Cpp' 为一个解决方案(一个解决方案可以包含多个项目(Project)),是项目的逻辑容器(Logical container)。项目通常是应用程序(Apps),但也支持类库(class libraries) 。此处的解决方案名称为WinUI3Cpp
WinUI3Cpp(Desktop) 为一个项目,是应用程序文件的逻辑容器,用于构建基于 WinUI3 的桌面应用程序。名称与创建项目时,同为该项目起的名称
External DependenciesReferences 为应用依赖的框架(如,.NETWindows SDK 等)和包(如,Windows App SDK);也包含开发者在应用中引入的其他功能和第三方库
Assets 资产文件夹,包含用于应用程序的图标、图像和其它媒体资产,为保存用户界面需要的静态文件
App.manifest 这个应用程序清单文件包含与应用安装在用户设备上时 Windows 显示应用程序方式相关的配置
App.xaml 该标记文件指定应用所依赖的,共享并全局可访问的资源,相关联的代码文件有App.xaml.cppApp.xaml.h
App.xaml.cppApp.xaml.h .cpp代码后置(code-behind)文件表示应用程序业务逻辑(business logic)的入口点,负责创建和激活MainWindow实例。.h为声明.cpp的头文件,定义应用程序类App的接口
MainWindow.xaml 该标记文件包含了应用程序主窗口展示的相关内容
MainWindow.xaml.cppMainWindow.xaml.h .cpp代码后置(code-behind)文件包含了与应用程序主窗口相关的业务逻辑。.h声明与MainWindow.xaml界面逻辑相关的类和成员函数
MainWindow.xaml.idl IDL(Interface Definition Language)文件用于定义应用程序与WinRT接口的交互,提供了MainWindow类的 WinRT投影接口
module.g.cpp 模块生成文件,通常由工具生成,负责与WinRTCOM模块的交互,包含项目模块的初始化和其他低级别设置
Package.appxmanifest 这个包清单文件为开发者提供发布信息、图标、处理器架构和其他细节的配置信息,及应用程序在 Windows Store 中显示方式等内容,通常包括的版块有ApplicationVisual AssetsCapabilitiesDeclarationsContent URIsPackaging
packages.config 为包管理配置文件,记录了项目所依赖的包,如Microsoft.WindowsAppSDKMicrosoft.Windows.SDK.BuildToolsMicrosoft.Windows.ImplementationLibraryMicrosoft.Windows.CppWinRTMicrosoft.Web.WebView2
pch.cpppch.h 预编译头文件,.h包含项目中常用的头文件,以减少重复编译,提供编译速度;.cpp编译.h中的内容,生成预编译头文件
readme.txt 说明文档,通常包含关于项目的基本信息,如项目的功能描述、编译和运行步骤及作者信息等
wil.natvis 该文件是一个用于自定义如何在调试器的LocalsWatch窗口中显示来自 Windows 实现库(Windows Implementation Library,WIL)的本地 C++ 对象的文件,其允许在调试 C++ 应用程序时,更友好地查看复杂的数据结构。本质上,是通过Nativs框架为 WIL类型提供了自定义可视化,使开发者能够在 VS 调试器中更容易理解这些类型的内容

上述WinUI3 C++桌面项目,其文件结构可以主要概况为:

  1. 项目基础文件(app.manifestPackage.appxmanifest):配置应用元数据和权限。
  2. 用户界面文件/标记文件(App.xamlMainWindow.xaml):配置应用的外观和主窗口。
  3. 代码逻辑文件/代码文件(*.cpp*.h):实现和声明应用程序功能。
  4. 资源文件(Assets):存储应用所需的静态资源。
  5. 工具生成文件(module.g.cppwil.natvis):辅助 WinRT和调试器工作。

10.2.1.2 主窗口实现

主窗口实现主要包括一个标记文件MainWindow.xaml和一个代码文件MainWindow.xaml.cpp,及其对应的头文件MainWindow.xaml.h,及MainWindow.idl IDL 文件。

文件 MainWindow.xaml MainWindow.xaml.h MainWindow.xaml.cpp MainWindow.idl
代码
  <?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="WinUI3Cpp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3Cpp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="WinUI3Cpp">

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

</Window>
  
  #pragma once

#include "MainWindow.g.h"

namespace winrt::WinUI3Cpp::implementation
{
    struct MainWindow : MainWindowT<MainWindow>
    {
        MainWindow()
        {
            // Xaml objects should not call InitializeComponent during construction.
            // See https://github.com/microsoft/cppwinrt/tree/master/nuget#initializecomponent
        }

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

        void myButton_Click(IInspectable const& sender, Microsoft::UI::Xaml::RoutedEventArgs const& args);
    };
}

namespace winrt::WinUI3Cpp::factory_implementation
{
    struct MainWindow : MainWindowT<MainWindow, implementation::MainWindow>
    {
    };
}
  
  #include "pch.h"
#include "MainWindow.xaml.h"
#if __has_include("MainWindow.g.cpp")
#include "MainWindow.g.cpp"
#endif

using namespace winrt;
using namespace Microsoft::UI::Xaml;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace winrt::WinUI3Cpp::implementation
{
    int32_t MainWindow::MyProperty()
    {
        throw hresult_not_implemented();
    }

    void MainWindow::MyProperty(int32_t /* value */)
    {
        throw hresult_not_implemented();
    }

    void MainWindow::myButton_Click(IInspectable const&, RoutedEventArgs const&)
    {
        myButton().Content(box_value(L"Clicked"));
    }
}
  
  namespace WinUI3Cpp
{
    [default_interface]
    runtimeclass MainWindow : Microsoft.UI.Xaml.Window
    {
        MainWindow();
        Int32 MyProperty;
    }
}
  
🤖 代码解读
  • 配置
  1. <?xml version="1.0" encoding="utf-8"?>:声明了XML文件的版本为 1.0,并指定了文件的字符编码为 UTF-8,为一个标准的 XML 声明,用于确保正确处理文件中的字符编码。
  2. x:Class="WinUI3Cpp.MainWindow":绑定到WinUI3Cpp.MainWindow类,表明该 XAML 文件将与此 C++/C# 类关联。
  3. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation":定义WinUI 3使用的命名空间,用于包含 UI 相关的控件和元素。
  4. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml":定义 XAML 标记语言本身使用的命名空间,用于支持 XAML 特性,如 x:Name等。
  5. xmlns:local="using:WinUI3Cpp":引用本地应用程序命名空间(即WinUI3Cpp),用于将本地类型引入 XAML 文件。
  6. xmlns:d="http://schemas.microsoft.com/expression/blend/2008":用于设计时支持的命名空间,通常用于设计器工具,例如 Blend for Visual Studio (Visual Studio Installer->.NET desktop development[workload]-> Blend for Visual Studio[component])。
  7. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006":支持标记兼容性的命名空间,允许在多个平台或版本之间兼容使用。
  8. mc:Ignorable="d":这个属性告诉 XAML 解析器忽略设计时命名空间d,因其只用于设计器工具。
  9. Title="WinUI3Cpp":设置窗口的标题为WinUI3Cpp
  • 窗口 UI
  1. <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">:定义了一个StackPanel布局容器,将子元素垂直或水平方向堆叠,其中,Orientation="Horizontal"设置StackPanel内部元素按水平方向排列;HorizontalAlignment="Center"VerticalAlignment="Center"为水平/垂直居中对齐StackPanel内的所有元素。
  2. <Button x:Name="myButton" Click="myButton_Click">Click Me</Button>:定义了一个Button按钮控件,其中x:Name="myButton"为按钮指定了一个名称myButton,可以在C++/C#代码中引用;Click="myButton_Click"为该按钮的Click事件绑定到myButton_Click方法,当按钮被点击时会调用该方法;Click Me为按钮上显示的文本。
  3. </StackPanel>:关闭StackPanel标签,结束这个布局容器的定义。
  4. </Window>:结束Window标签,结束整个窗口的定义。

这段 XAML 标记代码定义了一个简单的 WinUI3 应用界面,包含一个水平排列、居中对齐的StackPanel,该面板内有一个按钮控件,其上显示Click Me文本,并在点击时触发myButton_Click事件。

  1. #pragma once:是一种编译指令,确保该头文件在单个编译单元中只被包含一次,防止重复包含头文件,通常用于防止多重包含问题。
  2. #include "MainWindow.g.h":引入由 XAML 文件自动生成的代码(编译时由 XAML 编译器生成的代码),通常这个文件包含了与 XAML 文件对应的控件、事件等的定义。
  3. namespace winrt::WinUI3Cpp::implementation:该命名空间表示MainWindow类的实现部分,属于WinUI3Cpp项目中实现的命名空间。
  4. struct MainWindow : MainWindowT<MainWindow>:是MainWidow类的定义。MainWindowT是模板类,通常由 WinUI 3 自动生成,用于绑定MainWindow类和 XAML 之间的关联。继承MainWindowT<MainWindow>使得MainWindow类能够继承与 XAML 文件相关联的行为和功能。
          MainWindow()
        {
            // Xaml objects should not call InitializeComponent during construction.
            // See https://github.com/microsoft/cppwinrt/tree/master/nuget#initializecomponent
        }
  

MainWindow()MainWindow类的构造函数,其中的注释说明了:XAML 对象不应在构造函数中调用InitializeComponent,这是因为InitializeComponent通常是在 XAML 文件加载并与 C++ 类关联后才调用的。调用InitializeComponent会初始化 XAML 定义的控件,而此时如果在构造函数中调用,可能会遇到尚未完全初始化的资源或控件,因此推荐将其推迟到正确的时间点。InitializeComponent是由 XAML 自动生成的代码来初始化 UI 元素。具体内容可以在MainWindow.g.cpp中找到。

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

这两个方法定义了一个属性的 gettersetter

  1. int32_t MyProperty();:是MyProperty属性的 getter方法,返回一个int32_t类型的值。
  2. void MyProperty(int32_t value);:是MyProperty属性的setter方法,接受一个int32_t类型的参数,设置属性的值。

位于头文件(.h)的这些方法目前没有实现,通常会在对应的实现文件(.cpp)中提供具体的实现。

void myButton_Click(IInspectable const& sender, Microsoft::UI::Xaml::RoutedEventArgs const& args);:是按钮点击事件的处理函数声明:

  1. sender:事件的发送者,通常是触发事件的控件(此处为myButton)。

  2. args:包含有关事件信息的对象,通常是RoutedEventArgs,包括事件的源、附加数据等。

  3. namespace winrt::WinUI3Cpp::factory_implementation:该命名空间定义了工厂实现,用于创建MainWindow实例,与winrt::WinUI3Cpp::implementation命名空间中的实现类相对应。

  4. struct MainWindow : MainWindowT<MainWindow, implementation::MainWindow>:定义MainWindow的工厂类,用于实例化MainWindow类的对象,其继承了MainWindowT模板。模板参数包括MainWindowimplementation::MainWindow,表示工厂类和实现类之间的关联。

这段代码是C++/WinRT项目的头文件,定义了MainWindow类的接口部分和构造函数,具体实现的细节通常在.cpp文件(本例为MainWindow.xaml.cpp)中进行定义和实现。

  • 头文件和命名空间
  1. #include "pch.h":是预编译头文件,通常用于提高编译效率。预编译头文件包含了在多个源文件中都会使用的常用头文件。
  2. #include "MainWindow.xaml.h":引用MainWindow.xaml.h头文件,包含了由 XAML 文件自动生成的代码和说明,连接了 XAML 与 C++ 实现部分。
  #if __has_include("MainWindow.g.cpp")
#include "MainWindow.g.cpp"
#endif
  

为条件编译,检查MainWindow.g.cpp文件是否存在,如果存在则包含该文件。MainWindow.g.cpp是由 XAML 生成的代码,通常包含 XAML 文件的控件、事件等的实现部分。通过这种方式,项目的构建可以更加灵活。

  1. using namespace winrt;:引入winrt命名空间,winrt是 Microsoft 提供的C++/WinRT库,提供了与 Windows Runtime API 的交互功能。
  2. using namespace Microsoft::UI::Xaml;:引入 WinUI 的 XAML 命名空间,提供了与 WinUI3 中 XAML 相关的功能,如 UI 控件、事件、属性等。
  • 项目实现
  namespace winrt::WinUI3Cpp::implementation
{
  
  1. 定义了winrt::WinUI3Cpp::implementation命名空间,表示该类是WinUI3Cpp项目的实现部分。winrt::是 Windows Runtime 命名空间的前缀。
      int32_t MainWindow::MyProperty()
    {
        throw hresult_not_implemented();
    }
  
  1. int32_t MainWindow::MyProperty():是MainWindow类中的MyProperty方法的定义,其返回类型是int32_t(即 32 位整数)。
  2. throw hresult_not_implemented();:因上述方法还没实现,因此之间抛出hresult_not_implemented异常,表示该方法尚未实现。
      void MainWindow::MyProperty(int32_t /* value */)
    {
        throw hresult_not_implemented();
    }
  
  1. 这是MainWindow类中另一个MyProperty方法,负责设置属性的值。该方法的参数是一个整数value,但参数用/* value */方式注释掉,表明该参数在方法内还未实现。由于方法尚未实现,同样抛出hresult_not_implemented异常。
      void MainWindow::myButton_Click(IInspectable const&, RoutedEventArgs const&)
    {
        myButton().Content(box_value(L"Clicked"));
    }
  
  1. void MainWindow::myButton_Click(IInspectable const&, RoutedEventArgs const&):是MainWindow类中按钮点击事件的处理函数。此方法在myButton按钮被点击时被调用。其中IInspectable const&是事件的发送者(通常是触发事件的控件或对象);RoutedEventArgs const&包含有关事件的信息(如,事件源、事件处理等)。
  2. myButton().Content(box_value(L"Clicked"));:将按钮的Content设置为Clicked。其中myButton()是访问 XAML 中x:Name="myButton"按钮控件的方法;box_value是一个用于将字符串(L"Clicked")转换为 Windows Runtime 类型的函数,Content属性设置按钮的显示内容。

这段代码是MainWindow类的实现文件,连接了 XAML 中定义的 UI 控件(如按钮)和 C++ 实现部分。

  1. namespace WinUI3Cpp:声明了一个WinUI3Cpp的命名空间。命名空间用于将代码逻辑组织成一个隔离区域,以避免名称冲突。在这个命名空间下定义的所有类型(如类、接口等)都将以WinUI3Cpp为前缀进行访问。
  2. [default_interface]:为一个特性(attribute),指定了类的默认接口(用于强制生成默认接口,否则不会生成默认接口)。接口是一组函数或方法的声明,这些函数或方法在实现时需要提供具体的行为。在 WinUI3 中,runtimeclass本质上是一个具有默认接口的类,默认接口通常是该类与外部交互的主要接口。这里MainWindow类有一个默认接口,并通常指向自身,因为这个类的本质就是窗口类,用于显示和管理应用的主窗口。
  3. runtimeclass MainWindow : Microsoft.UI.Xaml.Windowruntimeclass关键字用于声明一个 WinRT 类,是 Windows Runtime(WinRT)API 中的一个概念,表示能够在应用程序中动态加载和实例化的类。runtimeclass通常对应一个类,而这个类可以通过 WinRT 接口与其它组件进行交互。MainWindow是一个 WinRT 类(runtimeclass),意味着其是一个可以被实例化并与系统交互的类。MainWindow类继承自Microsoft.UI.Xaml.Window,其是 WinUI3 中的窗口类,提供了窗口管理行为和 UI 呈现的功能。通过继承Window类,MainWindow类可以作为应用程序的主窗口。
  4. {...}:包含MainWindow类的成员。类内包含了构造函数和属性。
  5. MainWindow();:这是MainWindow类的构造函数。构造函数是类的一部分,在类实例化时自动调用,通常用于初始化类的成员。由于没有给构造函数提供参数,是一个无参构造函数。构造函数的实现可以在.cpp文件中,或在实现类时提供。
  6. Int32 MyProperty;:这是一个名为MyProperty的属性,类型是Int32,即一个 32 位整数。Int32类型在 C++/WinRT 中通常对应int类型。通过MyProperty属性,可以读取或设置与MainWindowe类相关的整数值。通常,MyProperty会有相应的gettersetter方法来进行访问和修改。

这个 IDL 文件定义了一个MainWindow类,继承自Microsoft.UI.Xaml.Window,表示一个应用程序窗口。MainWindow类有一个无参构造函数和一个Int32类型的属性MyProperty,用于存储一个整数值。MainWindow是一个 WinRT 类,可以与外部的 WinRT API 进行交互。

10.2.2 Hello world!

基于既有默认应用,按钮点击后除了改变按钮的显示内容从Click Me,修改为Clicked外;增加TextBlock控件显示文本,点击按钮前显示文本内容为Hi, there.,点击按钮后显示文本内容为Hello World!。另外,通过检索WinUI 3 Gallery(需从给出的链接下载安装程序,安装后运行查看)现代 UI 框架提供的示例应用,重新配置了按钮的样式,及配置文本显示的字体样式。并将应用的 UI 主题色由暗色模式Dark调整为了亮色模式Light,结果如图10-6。

PYC icon

图 10-6 Hello World! 点击按钮前后

首先在MainWindow.xaml UI 界面标记文件中调整按钮Button的样式;并增加文本块TextBlock控件。调整StackPanel中的样式为Orientation="Vertical"以垂直向布局按钮和文本块。并修改了窗口的标题为HelloWorld!,修改和增加部分的代码如下,

      ...
    Title="HelloWorld!">

    <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button x:Name="myButton" Click="myButton_Click" Content="Click Me" Style="{StaticResource AccentButtonStyle}" HorizontalAlignment="Center"/>
        <TextBlock x:Name="TB_SayHi" FontFamily="Arial" FontSize="24" FontStyle="Italic" TextWrapping="WrapWholeWords" CharacterSpacing="200" Foreground="CornflowerBlue" Text="Hi, there." Margin="0,10,0,0" />       
    </StackPanel>
  

不需要修改MainWindow.xaml.h头文件,只在MainWindow.xaml.cpp代码文件下myButton_Click按钮点击事件的处理函数中增加修改名为x:Name="TB_SayHi"文本块TextBlock的文本内容,修改的部分代码如下,

      void MainWindow::myButton_Click(IInspectable const&, RoutedEventArgs const&)
    {
        myButton().Content(box_value(L"Clicked"));
        TB_SayHi().Text(L"Hello World!");
    }
  

TB_SayHi().Text(L"Hello World!");TB_SayHi()是对MainWindow类中名为TB_SayHi控件(TextBlock)的访问方法。.TextTextBLock控件的属性,表示文本框中显示的文本内容。TB_SayHi().Text表示访问该文本框的Text属性。L"Hello World!"是一个宽字符字符串,将文本框的内容设置为"Hello World!"。因此,当按钮被点击时,TB_SayHi文本块的内容被修改为"Hello World!"

应用 UI 主题色的修改位于App.xaml标记文件下,增加代码RequestedTheme="Light"实现,其代码如下,

  <?xml version="1.0" encoding="utf-8"?>
<Application
    x:Class="WinUI3Cpp.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3Cpp"
    RequestedTheme="Light">
    <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>
  

10.3 C++/WinRT + WinUI3

10.3.1 数据绑定

{x:Bind} 标记扩展

图10.7为{x:Bind}标记扩展(markup extension)数据绑定的一个小示例。同前文默认应用,用程序模板Blank App, Packaged (WinUI 3 in Desktop)(C++)创建项目,但项目名为WinUI3X。每次点击按钮Click Me,都会更新上方的数字,增加1。即,属性propertyValue的初始值为 0,通过数据绑定显示在 UI 的TextBlock中,为初始状态;每次点击按钮,propertyValue增加1,并通过Bindings->Update()更新 UI。

PYC icon

图 10-7 {x:Bind} 标记扩展试验 App

文件 MainWindow.xaml MainWindow.xaml.h MainWindow.xaml.cpp

代码

  <?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="WinUI3X.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3X"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="x:Bind">

    <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
        <TextBlock Text="{x:Bind MyProperty}" HorizontalAlignment="Center" FontSize="64"/>
        <Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
    </StackPanel>
</Window>
  
  #pragma once

#include "MainWindow.g.h"

namespace winrt::WinUI3X::implementation
{
    struct MainWindow : MainWindowT<MainWindow>
    {
    private:
        int32_t propertyValue = 0;

    public:
        MainWindow(){}

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

        void myButton_Click(IInspectable const& sender, Microsoft::UI::Xaml::RoutedEventArgs const& args);
    };
}

namespace winrt::WinUI3X::factory_implementation
{
    struct MainWindow : MainWindowT<MainWindow, implementation::MainWindow>
    {
    };
}
  
  #include "pch.h"
#include "MainWindow.xaml.h"
#if __has_include("MainWindow.g.cpp")
#include "MainWindow.g.cpp"
#endif

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::WinUI3X::implementation
{
    int32_t MainWindow::MyProperty()
    {
        return propertyValue;
    }

    void MainWindow::MyProperty(int32_t value)
    {
       propertyValue = value;
       this->Bindings->Update();
       OutputDebugString(L"Property value updated to: ");
       OutputDebugString(winrt::to_hstring(propertyValue).c_str());
       OutputDebugString(L"\r\n");
    }

    void MainWindow::myButton_Click(IInspectable const&, RoutedEventArgs const&)
    {
        MyProperty(propertyValue + 1);
    }
}
  
🤖 代码解读

这段 XAML 定义了一个简单的 WinUI3 窗口布局,其中包含一个TextBlock和一个Button,并使用{x:Bind}数据绑定实现动态更新。

  1. <TextBlock Text="{x:Bind MyProperty}" HorizontalAlignment="Center" FontSize="64"/>:定义一个文本块,用于显示绑定的文本。Text="{x:Bind MyProperty}"使用{x:Bind}绑定到MyProperty属性。数据绑定会动态更新TextBlockTextBlock属性。HorizontalAlignment="Center"为文本块水平居中对齐。FontSize="64"为设置文本的字体大小为 64。
  2. <Button x:Name="myButton" Click="myButton_Click">Click Me</Button>:定义一个按钮,用户点击时会触发事件。x:Name="myButton"为按钮定义一个名称myButton,可以在代码后端(backing code,后端代码)通过这个名字访问按钮。Click="myButton_Click"绑定按钮的点击事件到后端代码的myButton_Click方法。Click Me按钮显示的内容是"Click Me"

窗口功能:一个简单的窗口,包含一个绑定到属性MyPropertyTextBlock和一个按钮。点击按钮后触发myButton_Click,可以在后端(代码)更新MyProperty,从而动态更新TextBlock的文本内容。

数据绑定:使用{x:Bind}提供高性能绑定,将后端数据直接绑定到 UI 元素属性。

这段代码定义了一个MainWindow类的声明,使用 C++/WinRT 风格和 WinUI3 框架,包括属性、事件处理方法的声明及工厂实现。

  1. int32_t propertyValue = 0;:声明一个私有成员变量propertyValue,类型为int32_t(32 位整数),并被初始化位值0,用于存储绑定到 UI 的属性值。
  2. int32_t MyProperty();:声明了一个名为MyProperty的公共成员函数,用于获取propertyValue的值(getter 方法)。
  3. void MyProperty(int32_t value);:声明另一个名为MyProperty的公共成员函数,用于设置propertyValue的值(setter 方法),接受一个参数value,类型为int32_t
  4. void myButton_Click(IInspectable const& sender, Microsoft::UI::Xaml::RoutedEventArgs const& args);:声明一个事件处理函数myButton_Click,用于处理按钮点击事件。参数IInspectable const& sender为事件的源对象;Microsoft::UI::Xaml::RoutedEventArgs const& args为事件的附加信息(如路由事件的相关数据)。

成员变量和方法/函数:propertyValue是用于存储一个整数值的私有成员变量。MyProperty()MyProperty(int32_t value)为属性的 getter 和 setter,用于与 UI 数据绑定。myButton_Click(...)为按钮点击事件的处理函数。

用途:该代码是一个典型的 WinUI3 应用程序的后端框架,支持与前端 XAML 的数据绑定和交互。

这段代码是对MainWindow类的实现,展示了如何定义属性和事件处理函数,及如何在 C++/WinRT 中使用数据绑定与更新逻辑。

  int32_t MainWindow::MyProperty()
{
    return propertyValue;
}
  

int32_t MainWindow::MyProperty():定义MyProperty的 getter 方法,返回私有成员变量propertyValue的值。

  void MainWindow::MyProperty(int32_t value)
{
    propertyValue = value;                  // 更新属性值。
    this->Bindings->Update();              // 通知绑定系统属性值已更新。
    OutputDebugString(L"Property value updated to: ");  // 输出调试信息。
    OutputDebugString(winrt::to_hstring(propertyValue).c_str()); // 转换值为字符串并输出。
    OutputDebugString(L"\r\n");            // 添加换行符。
}
  

void MainWindow::MyProperty(int32_t value):定义MyProperty的 setter 方法。功能包括更新propertyValue;调用this->Bindings->Update() 更新 UI,通知数据绑定系统绑定的属性值已更新;使用OutputDebugString输出调式信息,便于开发者跟踪值的变化。

  void MainWindow::myButton_Click(IInspectable const&, RoutedEventArgs const&)
{
    MyProperty(propertyValue + 1);
}
  

定义按钮点击事件的处理函数myButton_Click。通过调用MyProperty方法,将propertyValue的值加1。更新后的值自动通过数据绑定反映到绑定的 UI 元素(如 TextBlockText属性)。

属性机制: MyProperty是一个读写属性,通过数据绑定链接到 UI。使用Bindings->Update()保证数据绑定的同步更新。

按钮事件: myButton_Click用于处理按钮的点击事件,每次点击将propertyValue加1。数据绑定会自动更新 UI。

调试支持:通过OutputDebugString打印调试信息到输出窗口,方便开发调试。

10.3.2 控件

• 控件(Controls)-事件处理

图10.8 用程序模板Blank App, Packaged (WinUI 3 in Desktop)(C++)创建名为WinUI3X的项目,移除了默认应用程序中按钮和属性部分的代码,从而在一个完全空的项目上构建应用。该示例应用包括两个部分,左部分是选择不同的单选按钮,变换圆形边框的颜色;右部分是在输入的文本框中输入名字等字符,点击Sy Hi!按钮后会弹出对话框,显示消息。该部分展示了前端 UI 控件(XAML)和后端事件处理函数(C++/WinRT),表示与逻辑分离和交互实现的方式;也展示了winrt::fire_and_forgetco_await配合使用执行异步任务(操作)。

PYC icon

图 10-8 控件——事件处理

由 XAML 标记语言前端 UI 生成对应事件处理函数的后端代码(如示例中的 yellowButton_Checked 等在 .cpp 和 .h中的代码),可以将光标置于 XAML 的对应代码段内,按 F12 键自动生成。

更改 XAML 代码后,重构(Build Solution)应用程序,以便在.cpp代码中识别 XAML 元素。

文件 MainWindow.xaml MainWindow.xaml.h MainWindow.xaml.cpp MainWindow.idl
代码
  <?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="WinUI3X.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3X"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="x:Control">

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Border x:Name="colorPanel" Width="64" Height="64" CornerRadius="32" Background="Black" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="0,0,10,0"/>
        <StackPanel Orientation="Vertical">            
            <RadioButton x:Name="yellowButton" Content="Yellow" Checked="yellowButton_Checked"/>
            <RadioButton x:Name="blueButton" Content="Blue" Checked="blueButton_Checked"/>
            <RadioButton x:Name="redButton" Content="Red" Checked="redButton_Checked"/>
        </StackPanel>
        <StackPanel Orientation="Vertical" Margin="20,0,0,0">
            <TextBox x:Name="nameBox" Header="Enter your name:" Width="200"/>
            <Button x:Name="sayHiButton" Content="Say Hi!" Click="sayHiButton_Click" Margin="0,10,0,0"/>
        </StackPanel>
    </StackPanel>
</Window>
  
  #pragma once

#include "MainWindow.g.h"

namespace winrt::WinUI3X::implementation
{
    struct MainWindow : MainWindowT<MainWindow>
    {
    private:
        winrt::fire_and_forget showMessage(hstring message);
    public:
        MainWindow(){}

        void yellowButton_Checked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
        void blueButton_Checked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
        void redButton_Checked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
        void sayHiButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
    };
}

namespace winrt::WinUI3X::factory_implementation
{
    struct MainWindow : MainWindowT<MainWindow, implementation::MainWindow>
    {
    };
}
  
  #include "pch.h"
#include "MainWindow.xaml.h"
#if __has_include("MainWindow.g.cpp")
#include "MainWindow.g.cpp"
#endif

using namespace winrt;
using namespace Microsoft::UI::Xaml;
using namespace Microsoft::UI::Xaml::Media;

namespace winrt::WinUI3X::implementation
{
	winrt::fire_and_forget MainWindow::showMessage(hstring message)
	{
		Microsoft::UI::Xaml::Controls::ContentDialog dialog{};
		dialog.XamlRoot(this->Content().XamlRoot());
		dialog.Title(box_value(L"Greetings"));
		dialog.Content(box_value(message));
		dialog.CloseButtonText(L"Close");
		co_await dialog.ShowAsync();
	}
	void MainWindow::yellowButton_Checked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		colorPanel().Background(SolidColorBrush{ Microsoft::UI::Colors::Yellow() });
	}

	void MainWindow::blueButton_Checked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		colorPanel().Background(SolidColorBrush{ Microsoft::UI::Colors::Blue() });
	}

	void MainWindow::redButton_Checked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		colorPanel().Background(SolidColorBrush{ Microsoft::UI::Colors::Red() });
	}

	void MainWindow::sayHiButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		if (nameBox().Text().empty()) {
			showMessage(L"Enter your name!");
			return;
		}
		showMessage(hstring{ L"Hi, " + nameBox().Text() });
	}

}
  
  namespace WinUI3X
{
    [default_interface]
    runtimeclass MainWindow : Microsoft.UI.Xaml.Window
    {
        MainWindow();
    }
}
  
🤖 代码解读

这段 XAML 代码定义了一个用户界面,包含一个圆形边框作为颜色展示面板,及单选按钮组和文本框按钮组合,用于与用户交互。

  1. <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">:外层<StackPanel>,其中Orientation="Horizontal"属性配置内部子元素按水平布局。HorizontalAlignment="Center"VerticalAlignment="Center"StackPanel水平和垂直居中于窗口。
  2. <Border x:Name="colorPanel" Width="64" Height="64" CornerRadius="32" Background="Black" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="0,0,10,0"/>:为圆形颜色边框。x:Name="colorPanel"为边框赋予一个名称,便于后端代码引用。Width="64" Height="64"设置边框高宽为 64 像素。CornerRadius="32"将角半径设置为 32,使边框呈圆形。Background="Black"设背景色为黑色。Margin="0,0,10,0"为右边添加 10 像素间距。
  • 第一组垂直布局:颜色选项按钮
  <StackPanel Orientation="Vertical">
    <RadioButton x:Name="yellowButton" Content="Yellow" Checked="yellowButton_Checked"/>
    <RadioButton x:Name="blueButton" Content="Blue" Checked="blueButton_Checked"/>
    <RadioButton x:Name="redButton" Content="Red" Checked="redButton_Checked"/>
</StackPanel>
  
  1. Orientation="Vertical"为左内层<StackPanel>,配置其子元素垂直排列。
  2. 包含三个RadioButton(单选按钮),分别代表"Yellow"、“Blue"和"Red”。x:Name="..."为每个按钮指定名称,用于后端逻辑。Checked="..."当按钮被选中时,触发相应的事件处理函数(如yellowButton_Checked)。
  • 第二组垂直布局:输入框与按钮
  <StackPanel Orientation="Vertical" Margin="20,0,0,0">
    <TextBox x:Name="nameBox" Header="Enter your name:" Width="200"/>
    <Button x:Name="sayHiButton" Content="Say Hi!" Click="sayHiButton_CLick" Margin="0,10,0,0"/>
</StackPanel>
  
  1. Orientation="Vertical"为右内层<StackPanel>,配置其子元素垂直排列。Margin="20,0,0,0"为左边添加 20 像素间距,使其与颜色选项按钮组分开。
  2. TextBox文本框中,x:Name="nameBox"为文本框指定名称。Header="Enter your name:"在文本框上方显示提示文本。Width="200"宽度设置为 200 像素。
  3. Button按钮中,x:Name="sayHiButton"为按钮指定名称。Content="Say Hi!"为按钮上显示"Say Hi!"文本。Click="sayHiButton_CLick"点击按钮时触发后端的sayHiButton_CLick事件处理函数。Margin="0,10,0,0"为在顶部添加 10 像素间距。

前端用户界面有三个部分,一个显示当前颜色的圆形面板;一个垂直排列的颜色选项按钮组;和一个输入框和按钮的组合,用于输入和触发事件。功能是,当选中不同颜色按钮时,触发Checked事件,改变圆形面板的颜色;在输出框中输入文本,点击按钮时触发弹出窗口dialog对话框,显示提示信息。后端绑定的事件包括yellowButton_CheckedblueButton_CheckedredButton_Checked,用于设置圆形面板背景颜色为黄色、蓝色和红色;sayHiButton_CLick获取文本框中的值并显示用户输入的问候信息。

这段代码定义了MainWindow类的接口和实现结构(头文件),旨在支持用户界面中边框、颜色按钮切换和文本框、按钮点击事件的处理逻辑。

  1. winrt::fire_and_forget showMessage(hstring message);:定义了一个私有异步函数showMessage,参数message类型为hstring。返回类型为winrt::fire_and_forget,是 C++/WinRT 中用于异步编程的一个返回类型,表示一种无需等待结果的异步操作,主要用于事件处理、后台任务或其它不需要返回值或等待完成的场景,从而避免不必要的等待,提高性能,特别是当任务的结果对应用程序其余部分无关紧要时。功能是通过对话框显示一条消息。
  2. MainWindow(){}:定有MainWindow的默认构造函数,用于初始化MainWindow类的实例。

事件处理函数

  void yellowButton_Checked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
void blueButton_Checked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
void redButton_Checked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
void sayHiButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
  
  1. 定义了4个事件处理函数,分别对应 UI 元素的交互。yellowButton_CheckedblueButton_CheckedredButton_Checked分别处理黄色、蓝色和红色单选按钮被选中的事件。sayHiButton_CLick处理点击Say Hi!按钮的事件,通过对话框显示基于用户输入的消息。sender为触发事件的对象;e为事件参数对象。

事件处理:头文件定义了四个事件处理函数,分别响应单选按钮的选中事件和按钮的点击事件。

异步支持:提供了showMessage函数,用于以异步方式显示消息。

  1. using namespace Microsoft::UI::Xaml::Media;:是一个命名空间,包含在 WinUI3 框架中,用于处理用户界面(UI)中媒体相关功能,例如颜色、画刷、几何图形、转换等,提供了支持创建视觉效果的重要类和接口,帮助开发者自定义控件的外观和行为。
  2. namespace winrt::WinUI3X::implementation:这是为WinUI3项目自动生成的实现命名空间,包含MainWindow类的实现。

定义异步方法showMessage,是一个典型的 WinUI 异步对话框展示逻辑,适合用于弹出消息或提示用户的场景。

  	winrt::fire_and_forget MainWindow::showMessage(hstring message)
	{
		Microsoft::UI::Xaml::Controls::ContentDialog dialog{};
		dialog.XamlRoot(this->Content().XamlRoot());
		dialog.Title(box_value(L"Greetings"));
		dialog.Content(box_value(message));
		dialog.CloseButtonText(L"Close");
		co_await dialog.ShowAsync();
	}
  
  1. winrt::fire_and_forget MainWindow::showMessage(hstring message):其中winrt::fire_and_forget表示这是一个异步方法,但不会返回任何值,也不允许调用者等待它完成,而是方法执行完成后,系统自动“忘记”它,适用于无需处理结果或异常的异步任务。MainWindow::showMessage表示方法定义在MainWindow类中,负责显示一条消息对话框。hstring message参数message是一个hstring类型(WinRT 的字符串类型),用于传递要显示的消息内容。
  2. Microsoft::UI::Xaml::Controls::ContentDialog dialog{};:创建ContentDialog,是由 WinUI 提供的对话框控件,用于显示模态消息框(Modal Message Box)。模态消息框为一种用户界面元素,通常用于显示消息或提示用户做出决策时,要求用户与其交互并在关闭消息框之前不能与应用程序中的其它部分进行交互,常用于显示警告、确认操作、错误信息等场景,确保用户注意到某个重要事项或做出特定选择。
  3. dialog.XamlRoot(this->Content().XamlRoot());为设置对话框的XamlRoot属性,指向当前窗口的XamlRootthis->Content()返回窗口的内容(UIElement),调用XamlRoot()方法获取XamlRoot)。XamlRoot代表了窗口中根级 XAML 元素,在 WinUI 中作为视觉树的根节点,有助于管理和更新用户界面的布局及处理界面元素的渲染,可以实现模态对话框等窗口的显示与管理,确保它们的呈现与根窗口一致,否则对话框无法正确显示在当前窗口中。
  4. dialog.Title(box_value(L"Greetings"));为设置对话框的标题。box_value(L"Greetings")是将普通的宽字符串(L"Greetings")L表示字符串中的字符使用的是宽字符类型wchar_t,通常为 16 位或 32 位字符,以表示更多字符集中的字符,如 Unicode 字符等)转换为 WinRT 的IInspectable类型。IInspectable 是一个 Windows Runtime(WinRT)接口,提供对象的类型信息和接口查询功能,支持反射(Reflection)、类型查询、接口查询等操作,使得开发者可以在动态环境中操作对象,通常用于访问或操作 WinRT 对象的内部数据和方法。
  5. dialog.Content(box_value(message));:设置对话框的主要内容为传入的message字符串。同样用box_value()message转换为IInspectable类型,从而将普通的值类型包装为能够在 Windows Runtime API 中进行处理的对象。
  6. dialog.CloseButtonText(L"Close");:设置对话框的关闭按钮文本为"Close"。用户点击此按钮后对话框会关闭。
  7. co_await dialog.ShowAsync();:用于显示对话框。co_await暂停当前方法,等待ShowAsync异步完成。co_await属于协程coroutine机制的一部分,用于在协程中暂停当前的执行,等待一个异步操作完成,并在操作完成时恢复执行。dialog.ShowAsync()显示对话框并等待用户与对话框交互完成。用户可能点击对话框或其它地方关闭对话框。

yellowButton_CheckedyellowButton选项按钮触发Checked事件处理函数。(blueButtonredButton同)

  	void MainWindow::yellowButton_Checked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		colorPanel().Background(SolidColorBrush{ Microsoft::UI::Colors::Yellow() });
	}
  

事件处理函数yellowButton_Checked是当黄色按钮被选中(Checked)时触发。colorPanel().Background(...)设置colorPanel控件的背景颜色为黄色。SolidColorBrush{ Microsoft::UI::Colors::Yellow() }用于创建一个表示黄色的画刷对象。

sayHiButton_ClicksayHiButton按钮单击触发Click事件处理函数。

  	void MainWindow::sayHiButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		if (nameBox().Text().empty()) {
			showMessage(L"Enter your name!");
			return;
		}
		showMessage(hstring{ L"Hi, " + nameBox().Text() });
	}
  
  1. void MainWindow::sayHiButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)方法签名中,void表示该方法没有返回值。MainWindow::sayHiButton_Click属于MainWindow类,并且命名为sayHiButton_CLick,是与 UI 按钮点击事件关联的事件处理方法。
  2. if (nameBox().Text().empty())代码检查nameBox()控件(TextBox)中的文本是否为空。Text()用于获取文本框中的文本。.empty()方法检查文本是否为空,如果文本为空,则条件为true
  3. showMessage(L"Enter your name!"); :如果文本框文本为空,则调用showMessage方法,向用户显示提示消息"Enter your name!",提醒用户输入名字。这个方法是异步的,因此会弹出对话框并等待用户操作。
  4. return;:如果文本框为空,则跳出方法,不再执行下面的代码。
  5. showMessage(hstring{ L"Hi, " + nameBox().Text() });:如果文本框不为空,则构建一个包含"Hi, "和文本框文本nameBox().Text()+拼接在一起的字符串。拼接结果转换为hstring类型,符合showMessage输入参数的类型。

在 IDL 文件中移除了默认应用中的Int32 MyProperty;行代码。

• 控件(Controls)-数据绑定

该部分应用示例展示了通过按钮点击更新下拉框数据集的三种数据关联方式。当点击各按钮Add Item时,上方各自对应的下拉框会追加数据Item 1Item 2等。虽然这三部分实现的功能一致,但是后端代码更新前端用户界面下拉框的数据方式不同,分别为手动列表管理(后端代码直接操作前端 UI 控件)、数据源绑定(后端代码绑定控件和数组变量)和数据绑定(前端 UI 绑定控件和数组变量(以获取器 getter 方式))的不同实现。

PYC icon

图 10-9 控件——数据绑定

文件 MainWindow.xaml MainWindow.xaml.h MainWindow.xaml.cpp MainWindow.idl
代码
  <?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="WinUI3X.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3X"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="x:Control">

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <StackPanel Orientation="Vertical" Padding="12" BorderBrush="Gray" BorderThickness="0,0,1,0">
            <ComboBox x:Name="manualList" Header="Manual List" Width="200"/>
            <Button x:Name="addManualItemButton" Content="Add Item" Click="addManualItemButton_Click" Margin="0,10,0,0"/>
        </StackPanel>
        <StackPanel Orientation="Vertical" Padding="12" BorderBrush="Gray" BorderThickness="0,0,1,0">
            <ComboBox x:Name="sourceList" Header="ItemsSource List" Width="200"/>
            <Button x:Name="addSourceItemButton" Content="Add Item" Click="addSourceItemButton_Click" Margin="0,10,0,0"/>
        </StackPanel>
        <StackPanel Orientation="Vertical" Padding="12" BorderBrush="Gray" BorderThickness="0,0,1,0">
            <ComboBox x:Name="boundList" Header="Bound List" Width="200" ItemsSource="{x:Bind collection}"/>
            <Button x:Name="addBoundItemButton" Content="Add Item" Click="addBoundItemButton_Click" Margin="0,10,0,0"/>
        </StackPanel>
    </StackPanel>
</Window>
  
  #pragma once

#include "MainWindow.g.h"

namespace winrt::WinUI3X::implementation
{
    struct MainWindow : MainWindowT<MainWindow>
    {
    private:
        int32_t manualIndex = 0;
        int32_t sourceIndex = 0;
        int32_t boundIndex = 0;
        winrt::Windows::Foundation::Collections::IObservableVector<hstring> sourceArray{ winrt::single_threaded_observable_vector<hstring>() };
        winrt::Windows::Foundation::Collections::IObservableVector<hstring> boundArray{ winrt::single_threaded_observable_vector<hstring>() };
    public:
        MainWindow();

        winrt::Windows::Foundation::Collections::IObservableVector<hstring> collection();

        void addManualItemButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
        void addSourceItemButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
        void addBoundItemButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
    };
}

namespace winrt::WinUI3X::factory_implementation
{
    struct MainWindow : MainWindowT<MainWindow, implementation::MainWindow>
    {
    };
}
  
  #include "pch.h"
#include "MainWindow.xaml.h"
#if __has_include("MainWindow.g.cpp")
#include "MainWindow.g.cpp"
#endif

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::WinUI3X::implementation
{
	MainWindow::MainWindow() {
		InitializeComponent();
		sourceList().ItemsSource(sourceArray);
	}

	void MainWindow::addManualItemButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		manualIndex++;
		manualList().Items().Append(box_value(hstring{ L"Item " + to_hstring(manualIndex) }));
		if (manualList().SelectedItem() == nullptr) manualList().SelectedIndex(0);
	}

	void MainWindow::addSourceItemButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		sourceIndex++;
		sourceArray.Append(hstring{ L"Item " + to_hstring(sourceIndex) });
		if (sourceList().SelectedItem() == nullptr) sourceList().SelectedIndex(0);
	}

	winrt::Windows::Foundation::Collections::IObservableVector<hstring> MainWindow::collection() {
		return boundArray;
	}

	void MainWindow::addBoundItemButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		boundIndex++;
		boundArray.Append(hstring{ L"Item " + to_hstring(boundIndex) });
		if (boundList().SelectedItem() == nullptr) boundList().SelectedIndex(0);
	}
}
  
  namespace WinUI3X
{
    [default_interface]
    runtimeclass MainWindow : Microsoft.UI.Xaml.Window
    {
        MainWindow();
        Windows.Foundation.Collections.IObservableVector<String> collection{ get; };
    }
}
  
🤖 代码解读

这段 XAML 代码定义了一个窗口,其布局由一个外层横向排列的StackPanel,及其内部由StackPanel配置的三个垂直排列的功能区块组成。每个区块有一个下拉框和一个按钮,分别用于手动列表管理、数据源绑定和数据绑定的实现示例。

  1. 每个区块中的下拉列表框(ComboBox)均包括属性x:Name",用于在代码中访问此控件;Header,设置下拉框的标题;Width,指定下拉框的宽度。在数据绑定示例的第三个区块,ItemsSource="{x:Bind collection}"boundList(ComboBox)绑定到collection对象集合。
  2. 每个区块的按钮(Button)均包括属性x:Name,用于在代码中访问此控件;Content,设置按钮显示的文本;Click,将按钮的点击事件关联到后端代码的方法上;Margin,用于设置按钮的边距。

私有成员变量:

  1. manualIndexsourceIndexboundIndex,定义了三个整型变量,分别初始化为0,用作下拉框中手动项、数据源项和绑定项的索引,用于跟踪当前添加的项目数量。
  2. sourceArrayboundArray,定义了两个可观察的向量(数组),类型为IObservableVector<hstring>,使用winrt::single_threaded_observable_vector初始化,支持数据变更通知,用于存储绑定到下拉框(ComBox)的动态数据集合。

winrt::Windows::Foundation::Collections::IObservableVector<hstring> boundArray{ winrt::single_threaded_observable_vector<hstring>() };代码行的详细解读:

  1. winrt::Windows::Foundation::Collections::IObservableVector<hstring>IObservableVector是一个接口,表示一个动态数组(或向量),支持集合更改通知,即动态数据集合,当集合发生更改时(例如添加、删除、替换元素等),可以通知绑定到它的 UI 控件自动更新,适用于数据绑定场景,如绑定到ComboBoxListView控件上。模板参数<hstring>表示该向量中存储的元素类型是hstring,即 WinRT 的字符串类型,是一个高效,不可变的 Unicode 字符串类型,在 WinRT 中用于字符串处理。
  2. winrt::single_threaded_observable_vector<hstring>()winrt::single_threaded_observable_vector是一个函数,用于创建一个线程安全、支持集合更改通知的IObservableVector实现,即返回一个实现了IObservableVector<T>接口的对象;并假定所有操作都发生在同一线程上,适用于 UI 线程上的操作场景。因为没有跨线程同步的开销,性能较高。
  3. 初始化语法{ ... },是使用大括号{}初始化boundArray,表示通过构造函数初始化,即winrt::single_threaded_observable_vector<hstring>()的返回值被直接赋值给boundArray
  4. boundArray是一个IObservableVector<hstring>类型的变量,底层实现由single_threaded_observable_vector<hstring>提供,可存储字符串(hstring)元素,并在发生更改时触发通知。将其绑定到 UI 控件上,对应的 XAML 语句如<ComboBox x:Name="boundList" Header="Bound List" Width="200" ItemsSource="{x:Bind collection}"/>

公有成员函数:

  1. MainWindow();:默认构造函数,用于初始化窗口对象。
  2. collection():返回类型为IObservableVector<hstring>,表示一个可观察的字符串集合,示例中返回的是boundArray,用于与 XAML 中的数据绑定关联(ItemsSource="{x:Bind collection})。
  3. addManualItemButton_Click(...)addSourceItemButton_Click(...)addBoundItemButton_Click(...)分别为按钮点击事件处理函数,对应手动列表、数据源列表和绑定列表示例。
  • 文件顶部指令和命名空间同前文解释。

  • MainWindow构造函数

  1. MainWindow::MainWindow()MainWindow的构造函数,在窗口实例化时调用。
  2. InitializeComponent():初始化窗口组件,加载 XAML 文件中定义的 UI。
  3. sourceList().ItemsSource(sourceArray);:设置sourceList(XAML 中定义的下拉框)的ItemsSource属性,绑定到sourceArray数据集合。这使得sourceArray的变化会自动更新到sourceList
  • addManualItemButton_Click函数
  1. manualIndex++;:每次触发按钮的Click事件,增加manualIndex的值,用于生成新项的编号。
  2. manualList().Items().Append(...):往manualList下拉框的项集合中添加新项。hstring{ L"Item " + to_hstring(manualIndex) },将manualIndex转换为字符串并拼接成Item X的形式格式。box_value(...),将hstring包装为IInspectable类型,这是 WinRT 控件的数据项类型。
  3. if (manualList().SelectedItem() == nullptr):检查manualList当前是否没有选中项。如果未选中任何项,执行manualList().SelectedIndex(0);,将第一项(索引为0)设为选中项。
  • addSourceItemButton_Click 函数
  1. sourceIndex++;,每次触发按钮的Click事件,增加sourceIndex的值,用于生成新项的编号。
  2. sourceArray.Append(...),因为sourceArrayMainWindow构造函数中,通过sourceList().ItemsSource(sourceArray);绑定到了sourceList(ComboBox)上,因此向sourceArray添加一项,sourceList会自动更新。
  • collection函数
  1. 返回boundArray,用于与绑定的下拉框boundList关联。 XAML 中的ItemsSource="{x:Bind collection}"会调用此函数。
  • addBoundItemButton_Click函数
  1. boundIndex++;,每次触发按钮的Click事件,增加boundIndex的值,用于生成新项的编号。
  2. boundArray.Append(...),向boundArray添加新项,boundList会自动更新,在 XAML 前端代码中,其已绑定到boundArray

该部分代码主要定义了三个按钮的点击事件处理函数,通过直接(手动)操作、数据源绑定和{x:Bind}数据绑定的不同方式来更新 UI 中各自对应的下拉框集合。其中manualList,是直接操作Items集合,用manualList().Items().Append(...)方式更新;sourceList,是通过sourceList().ItemsSource(sourceArray);sourceArray绑定到sourceList,从而通过操作数据sourceArray更新;boundList,是在 XAML 前端代码中用ItemsSource="{x:Bind collection}绑定collection函数,而collection函数返回boundArray,从而实现boundList(ComboBox)和boundArray数组的绑定。sourceArrayboundArray向量(数组)需要为IObservableVector类型的变量,以在向量发生更改时触发通知,更新绑定的 UI 控件对象。

Windows.Foundation.Collections.IObservableVector<String> collection{ get; };:中Windows.Foundation.Collections.IObservableVector<String>,定义了一个公开属性collection,类型为IObservableVector<String>,是一个动态数组接口,支持观察者模式(通知绑定的控件数组变化),适用于绑定到 XAML 控件的ItemsSource上。collection{ get; };表明collection属性是只读的(get表示有获取器 getter,但没有设置器 setter)。当其它代码访问collection时,会调用实现类中的collection()函数。

10.3.3 页面和导航

使用 WinUI3 创建一个包含多页面导航界面的应用程序。应用界面功能包括导航视图,为左侧垂直导航栏,提供主页面的导航入口,包括 Home(主页)、Other Page(其他页面)和 Settings(设置页面)。导航栏使用图标和文本直观展示页面导航选项,可折叠/展开;使用NavigationView控件,具有现代化的响应式设计,支持多种窗口大小调整。右侧内容区域根据导航选项动态加载相应的页面内容,示例性的显示各自对应的文本。每个页面独立实现,通过Frame控件加载,便于扩展和维护。位于左上角的后退按钮提供导航历史的回退功能,允许用户返回到前一个页面。如果用户在导航历史中没有可返回的页面则按钮禁用。

PYC icon

图 10-10 页面和导航

MainPageHomePageOtherPageSettingsPage四个页面的创建方式为Solution Explorer->WinUI3X(Desktop)->右键弹出菜单->Add->New Item->Blank Page(WinUI3)。其中页面的结构层次关系如图10.11,MainWindow为应用程序的主窗口,作为程序启动后加载的初始窗口,主要任务是承载MainPageMainPage作为应用程序的主页面,其包含一个Frame(导航框架),用于显示和切换页面内容;NavigationView控件用于导航到其它功能页面(如HomePageOtherPageSettingsPage)。MainPage是通过MainWindow加载的。HomePageOtherPageSettingsPage分别为MainPage导航到的页面,为“主页”,“其他”功能页面和“设置“选项页面。项目结构中页面关系图的层级可以描述为MainWindow包含MainPage作为子页面;MainPage通过NavigationView和导航框架Frame加载HomePageOtherPageSettingsPage

PYC icon

图 10-11 文件结构

1-MainWindow

MainWindow.xaml中,<local:MainPage/>是一个自定义页面(或控件),其名字为MainPage。并使用了自闭和标签/,表示这是一个没有子元素的单独控件。local是在根节点<Window>中定义的一个命名空间前缀xmlns:local="using:WinUI3X",指向当前项目的命名空间WinUI3X。作用是,所有前缀为local:的控件或元素,均被解析为WinUI3X命名空间内的类型。此处,local:MainPage指向WinUI3X.MainPage类。MainPage自定义页面同样包括与之关联的 XAML 文件(MainPage.xaml),描述其 UI 结构;一个后端代码文件(MainPage.xaml.cpp,及其头文件MainPage.xaml.h),描述行为逻辑;和 IDL 文件MainPage.idl

MainWindow页面中,MainWindow.xaml.cppMainWindow.xaml.hMainWindow.idl中均移除了自动生成的按钮myButton和属性变量MyProperty相关部分的内容,保持为一个空白的页面。

在 WinUI 或类似框架中,将MainPage嵌套到MainWindow中是一种常见的设计模式,其目的是为了分离关注点和提升代码的可维护性,其优势有:

  1. 模块化设计。MainWindow的职责是作为应用的主要窗口,是整个应用的容器,主要负责窗口级别的设置,例如窗口标题;窗口尺寸和布局;应用程序的生命周期事件(如启动、最小化和关闭等)等。MainWindow通常不直接承载复杂的 UI 内容,而是用来嵌套其他页面或控件。MainPage的职责专注于应用程序的主要 UI 和交互逻辑。作为窗口的内容部分,定义了具体的用户界面(如按钮、输入框和列表等)。这种设计让界面逻辑和窗口设置分离,便于管理。
  2. 可扩展性。如果后续应用需要多页面导航(如从MainPage导航到SettingsPage或其他页面),可以包含一个Frame控件,用于托管不同的页面。这种设计使得应用具有更高的扩展性,而不是将所有内容直接嵌套到MainWindow中。
  3. 简化代码结构。如果所有的 UI 和逻辑都写在MainWindow中,代码可能变得难以维护。将主要的 UI 放在MainPage中,可以使每个类保持简洁,单一负责。
  4. 多窗口支持。可以在应用冲创建多个窗口,每个窗口可能有不同的内容,是独立的页面。
  5. 设计和开发分离。可以在设计阶段单独对页面进行布局和调整。在开发阶段,MainWindowMainPage以及其他页面的开发可以并行进行,提高效率。
文件 MainWindow.xaml MainWindow.xaml.h MainWindow.xaml.cpp MainWindow.idl
代码
  <?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="WinUI3X.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3X"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" 
    Title="Pages and Navigation">

    <local:MainPage/>

</Window>
  
  #pragma once

#include "MainWindow.g.h"

namespace winrt::WinUI3X::implementation
{
    struct MainWindow : MainWindowT<MainWindow>
    {
        MainWindow(){}
    };
}

namespace winrt::WinUI3X::factory_implementation
{
    struct MainWindow : MainWindowT<MainWindow, implementation::MainWindow>
    {
    };
}
  
  #include "pch.h"
#include "MainWindow.xaml.h"
#if __has_include("MainWindow.g.cpp")
#include "MainWindow.g.cpp"
#endif

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::WinUI3X::implementation
{
}
  
  namespace WinUI3X
{
    [default_interface]
    runtimeclass MainWindow : Microsoft.UI.Xaml.Window
    {
        MainWindow();
    }
}
  

2-MainPage

MainPage通过MainWindow加载,加载的代码为MainWindow.xaml文件下的<local:MainPage/>。而MainPage通过NavigationView控件导航到其他功能页面(HomePageOtherPageSettingsPage)。

文件 MainPage.xaml MainPage.xaml.h MainPage.xaml.cpp MainPage.idl
代码
  <?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="WinUI3X.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3X"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" 
    Loaded="Page_Loaded">
    
    <NavigationView x:Name="nav" IsSettingsVisible="True" ItemInvoked="NavigationView_ItemInvoked" BackRequested="nav_BackRequested">
        <NavigationView.MenuItems>
            <NavigationViewItem Content="Home" Icon="Home" Tag="home"/>
            <NavigationViewItem Content="Other Page" Icon="Page" Tag="other"/>
        </NavigationView.MenuItems>
        <NavigationView.Content>            
            <Frame x:Name="mainFrame" Navigated="mainFrame_Navigated"/>
        </NavigationView.Content>
    </NavigationView>

</Page>
  
  #pragma once

#include "MainPage.g.h"

namespace winrt::WinUI3X::implementation
{
    struct MainPage : MainPageT<MainPage>
    {
    private:
        void openHomePage();
        void openOtherPage();
        void openSettingPage();

    public:
        MainPage(){}

        void Page_Loaded(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
        void NavigationView_ItemInvoked(winrt::Microsoft::UI::Xaml::Controls::NavigationView const& sender, winrt::Microsoft::UI::Xaml::Controls::NavigationViewItemInvokedEventArgs const& args);

        void mainFrame_Navigated(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e);
        void nav_BackRequested(winrt::Microsoft::UI::Xaml::Controls::NavigationView const& sender, winrt::Microsoft::UI::Xaml::Controls::NavigationViewBackRequestedEventArgs const& args);
    };
}

namespace winrt::WinUI3X::factory_implementation
{
    struct MainPage : MainPageT<MainPage, implementation::MainPage>
    {
    };
}
  
  #include "pch.h"
#include "MainPage.xaml.h"
#if __has_include("MainPage.g.cpp")
#include "MainPage.g.cpp"
#endif
#include "winrt/Windows.UI.Xaml.Interop.h"

using namespace winrt;
using namespace Microsoft::UI::Xaml;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace winrt::WinUI3X::implementation
{
    void MainPage::Page_Loaded(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
    {
        // Initialize your stuff here!
    }

    void MainPage::NavigationView_ItemInvoked(winrt::Microsoft::UI::Xaml::Controls::NavigationView const& sender, winrt::Microsoft::UI::Xaml::Controls::NavigationViewItemInvokedEventArgs const& args)
    {
        if (args.IsSettingsInvoked()) {
            openSettingPage();
        }
        else {
            hstring tag = unbox_value<hstring>(args.InvokedItemContainer().Tag());
            if (tag == L"home")
                openHomePage();
            else if (tag == L"other") 
                openOtherPage();
        }
    }

    void MainPage::openHomePage()
    {
        mainFrame().Navigate(xaml_typename<HomePage>());
    }

    void MainPage::openOtherPage()
    {
        mainFrame().Navigate(xaml_typename<OtherPage>());
    }

    void MainPage::openSettingPage()
    {
        mainFrame().Navigate(xaml_typename<SettingsPage>());
    }

    void MainPage::mainFrame_Navigated(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e)
    {
        nav().IsBackEnabled(mainFrame().CanGoBack());
    }


    void MainPage::nav_BackRequested(winrt::Microsoft::UI::Xaml::Controls::NavigationView const& sender, winrt::Microsoft::UI::Xaml::Controls::NavigationViewBackRequestedEventArgs const& args)
    {
        mainFrame().GoBack();
    }
}
  
  namespace WinUI3X
{
    [default_interface]
    runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
    {
        MainPage();
    }
}
  
🤖 代码解读
  • 根元素
  1. <Page>定义了一个页面(Page),通常表示一个应用程序的独立视图。PageMicrosoft.UI.Xaml.Controls.Page的实例。
  2. x:Class="WinUI3X.MainPage":指定此页面的后端代码为WinUI3X.MainPage,对应的 C++ 文件为MainPage.xaml.hMainPage.xaml.cpp
  3. MainPage.xaml.cpp:指向项目命名空间WinUI3X,用于引用项目中的类或控件。
  4. Loaded="Page_Loaded":指定页面加载完成后触发的事件,关联后端代码中的Page_Loaded方法。
  • 页面内容——<NavigationView>
  <NavigationView x:Name="nav" IsSettingsVisible="True" ItemInvoked="NavigationView_ItemInvoked" BackRequested="nav_BackRequested">
  
  1. <NavigationView>:用于实现现代应用中的导航菜单,提供导航视图结构,包括菜单项、导航内容区域等。
  2. x:Name="nav":为NavigationView控件命名为nav,便于在后台代码中引用。
  3. IsSettingsVisible="True":指定是否显示设置菜单项(通常以齿轮图标表示)。这里设置为True,默认显示。
  4. ItemInvoked="NavigationView_ItemInvoked":定义当用户点击导航菜单中的某项时触发的事件,关联后端代码NavigationView_ItemInvoked方法。
  5. BackRequested="nav_BackRequested":定义当用户点击返回按钮时触发的事件,关联后台代码的nav_BackRequested方法。
  • 子元素——<NavigationView.MenuItems>
  <NavigationView.MenuItems>
    <NavigationViewItem Content="Home" Icon="Home" Tag="home"/>
    <NavigationViewItem Content="Other Page" Icon="Page" Tag="other"/>
</NavigationView.MenuItems>
  
  1. <NavigationView.MenuItems>:定义导航菜单的项(MenuItems),每个菜单项表示一个页面或功能。
  2. <NavigationViewItem>:定义导航菜单中的具体项。Content="Home"为菜单项显示的文本为"Home"Icon="Home"为菜单项使用内置的Home图标;Tag="home"为此项指定标识符home,便于事件处理中识别。
  3. 第二个菜单项other,基本同第一个菜单项home的解释。
  • 子元素——<NavigationView.Content>
  <NavigationView.Content>
    <Frame x:Name="mainFrame" Navigated="mainFrame_Navigated"/>
</NavigationView.Content>
  
  1. <NavigationView.Content>:定义NavigationView的主要内容区域,显示导航到的页面内容。
  2. <Frame>:是一个容器控件,支持导航功能,可以加载其他页面。属性x:Name="mainFrame"命名为mainFrame,用于在后端代码中操作此控件。Navigated="mainFrame_Navigated"指定页面导航完成后触发的事件,关联后端代码的mainFrame_Navigated方法。

该页面(MainPage)通过NavigationView构建了一个导航菜单,包含HomeOther Page两个菜单项,将Frame作为主要内容区域,动态加载导航目标页面。后端代码中的事件(如NavigationView_ItemInvokedPage_Loaded)控制具体行为。

  • 私有方法
  private:
    void openHomePage();
    void openOtherPage();
    void openSettingPage();
  
  1. openHomePageopenOtherPageopenSettingPage定义了三个私有方法,用于打开不同页面或设置,在对应的.cpp文件中定义具体实现。
  • 公有方法和构造函数
  1. 默认构造函数,MainPage()初始化MainPage对象。
  2. 页面加载事件处理程序,void Page_Loaded(...);,处理页面加载完成的事件,在页面被初始化后触发,通常用于进行数据绑定、控件初始化等操作。
  3. 导航视图点击事件处理程序,void NavigationView_ItemInvoked(...),在用户点击导航视图中的某个菜单时触发。参数sender触发此事件的导航视图控件;args包含被点击项的信息,如TagContent等。
  4. 主框架导航完成事件处理程序,void mainFrame_Navigated(...), 当主内容框架(Frame)完成导航到新页面时触发,可用于更新导航状态、标题或执行其它操作。
  5. 返回按钮事件处理程序,void nav_BackRequested(...),当用户点击返回按钮时触发,通常用于实现导航堆栈的后退功能。

此文件定义了一个MainPage类,负责实现页面逻辑,包含有多个事件处理程序,用于处理导航、页面加载和返回等交互;其私有方法(如openHomePage)用于封装页面逻辑。

  • 文件头部
  1. winrt/Windows.UI.Xaml.Interop.h:提供了辅助函数,如xaml_typename,用于在运行时动态加载页面。
  • 使用命名空间
  1. using namespace winrt;:包含所有 C++/WinRT 的核心功能。
  2. using namespace Microsoft::UI::Xaml;:用于访问 WinUI 控件和相关功能。
  • 页面加载事件处理程序:void Page_Loaded(...);
  void MainPage::Page_Loaded(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
{
    // Initialize your stuff here!
    //openHomePage();
}
  

该部分空出。openHomePage为初始化时加载主页(被注释掉,可以按需启动),用于功能测试。

  • 导航视图点击事件处理程序:void NavigationView_ItemInvoked(...)
  if (args.IsSettingsInvoked()) {
    openSettingPage();
}
  
  1. 判断是否点击设置菜单。args.IsSettingsInvoked()用于检查用户是否点击了设置菜单,返回一个布尔值,如果点击了则调用函数openSettingPage打开设置页面。
  hstring tag = unbox_value<hstring>(args.InvokedItemContainer().Tag());
if (tag == L"home")
    openHomePage();
else if (tag == L"other") 
    openOtherPage();
  
  1. hstring tag = unbox_value<hstring>(args.InvokedItemContainer().Tag());为获取用户点击导航项的Tag属性值,并将其转换为hstring类型。其中args.InvokedItemContainer()返回用户点击的具体导航容器项,Tag()为返回导航项的Tag属性值,通常是用于标识的字符串。unbox_value<hstring>则将通用类型(如IInspectable)取消装箱为hstring类型。C++/WinRT 提供了winrt::box_value函数,采用标量或数组值,将装箱的值返回到IInspectable中;对于取消IInspectable装箱并返回到标量或数组值,则为winrt::unbox_value函数。
  2. if (tag == L"home")检查tag的值是否为L"home",如果是则执行openHomePage();导航到其链接的页面HomePage上。
  3. else if (tag == L"other")检查tag是否为L"other",如果是则执行openOtherPage();导航到其链接的页面OtherPage上。
  • 页面导航函数:void MainPage::openHomePage()void MainPage::openOtherPage()void MainPage::openSettingPage()
  void MainPage::openHomePage()
{
    mainFrame().Navigate(xaml_typename<HomePage>());
}
  

void MainPage::openHomePage()为例,定义MainPage类的一个成员函数openHomePage,用于导航到主页面HomePagemainFrame()(控件Frame)是页面中管理导航历史的容器,负责加载和显示页面。.Navigate(xaml_typename<HomePage>())调用FrameNavigate方法,用于加载指定类型的页面,导航到指定页面类型HomePagexaml_typename<HomePage>()是一个模板函数,返回HomePage类型的TypeName对象,告诉Frame需要加载的目标页面类型。xaml_typename<>提供类型信息,支持 WinUI 运行时使用反射加载页面。HomePage通常在项目中定义,是目标页面的类名,由 XAML 文件和对应的 C++/WinRT 代码生成。

该函数的功能就是使用主框架(mainFrame)作为导航容器;调用Navigate方法将导航目标设定为HomePage类型;加载HomePage并在主框架中显示其内容。

  • 主框架导航完成事件处理程序:void mainFrame_Navigated(...)
  1. nav().IsBackEnabled(mainFrame().CanGoBack());根据导航框架的返回状态动态更新导航视图的返回按钮状态。其中mainFrame().CanGoBack()返回一个布尔值,表示当前是否可以返回到前一个页面。如果导航历史中存在前一个页面,则返回true;否则返回falsenav().IsBackEnabled(bool)设置导航视图中返回按钮的启用状态,true为启用返回按钮;false为禁用返回按钮。
  • 返回按钮事件处理程序:void nav_BackRequested(...)
  1. mainFrame().GoBack();为调用主内容框架的返回功能,将当前页面导航到前一个页面。如果导航历史中没有前一个页面,此操作无效。

mainFrame_Navigated用于更新返回按钮的状态;nav_BackRequested处理返回按钮的点击操作,执行页面返回导航。结合了FrameNavigationView的功能,实现了基本的导航控制。

3-HomePageOtherPageSettingsPage

这三个页面均为演示性页面,页面内容除了在各自的 .xaml 文件中通过TextBlock输出一段描述当前页的文本外,无其他功能,如<TextBlock Text="This is the HOME page." FontFamily="Arial" FontSize="24"/>

文件 .xaml .xaml.h .xaml.cpp .idl

HomePage

  <?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="WinUI3X.HomePage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3X"
    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">
        <TextBlock Text="This is the HOME page." FontFamily="Arial" FontSize="24"/>
    </StackPanel>
</Page>
  
  #pragma once

#include "HomePage.g.h"

namespace winrt::WinUI3X::implementation
{
    struct HomePage : HomePageT<HomePage>
    {
        HomePage(){}
    };
}

namespace winrt::WinUI3X::factory_implementation
{
    struct HomePage : HomePageT<HomePage, implementation::HomePage>
    {
    };
}
  
  #include "pch.h"
#include "HomePage.xaml.h"
#if __has_include("HomePage.g.cpp")
#include "HomePage.g.cpp"
#endif

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::WinUI3X::implementation
{

}
  
  namespace WinUI3X
{
    [default_interface]
    runtimeclass HomePage : Microsoft.UI.Xaml.Controls.Page
    {
        HomePage();
    }
}
  

OtherPage

  <?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="WinUI3X.OtherPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3X"
    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">
        <TextBlock Text="This is the OTHER page." FontFamily="Arial" FontSize="24"/>
    </StackPanel>
</Page>
  
  #pragma once

#include "OtherPage.g.h"

namespace winrt::WinUI3X::implementation
{
    struct OtherPage : OtherPageT<OtherPage>
    {
        OtherPage(){}
    };
}

namespace winrt::WinUI3X::factory_implementation
{
    struct OtherPage : OtherPageT<OtherPage, implementation::OtherPage>
    {
    };
}
  
  #include "pch.h"
#include "OtherPage.xaml.h"
#if __has_include("OtherPage.g.cpp")
#include "OtherPage.g.cpp"
#endif

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::WinUI3X::implementation
{

}
  
  namespace WinUI3X
{
    [default_interface]
    runtimeclass OtherPage : Microsoft.UI.Xaml.Controls.Page
    {
        OtherPage();
    }
}
  

SettingsPage

  <?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="WinUI3X.SettingsPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3X"
    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">
        <TextBlock Text="This is the SETTING page." FontFamily="Arial" FontSize="24"/>
    </StackPanel>
</Page>
  
  #pragma once

#include "SettingsPage.g.h"

namespace winrt::WinUI3X::implementation
{
    struct SettingsPage : SettingsPageT<SettingsPage>
    {
        SettingsPage(){}
    };
}

namespace winrt::WinUI3X::factory_implementation
{
    struct SettingsPage : SettingsPageT<SettingsPage, implementation::SettingsPage>
    {
    };
}
  
  #include "pch.h"
#include "SettingsPage.xaml.h"
#if __has_include("SettingsPage.g.cpp")
#include "SettingsPage.g.cpp"
#endif

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::WinUI3X::implementation
{

}
  
  namespace WinUI3X
{
    [default_interface]
    runtimeclass SettingsPage : Microsoft.UI.Xaml.Controls.Page
    {
        SettingsPage();
    }
}
  

10.3.4 文件选择器

该示例实现了通过点击Open file按钮,打开文件浏览器,选择图片后,在窗口右侧显示该图片内容的功能。

PYC icon

图 10-12 文件选择器(打开的内部图片由 AI 生成器生成,工具 shakker,https://www.shakker.ai/home

1-App

App.xaml主要用于设置应用程序的全局资源和主题,定义了 UI 控件的资源字典(包括系统默认样式、控制资源等),并指定了应用程序的外观主题。在App.xaml中,还可以设置应用启动时的一些行为,如启动窗口、全局样式等。这里增加了getMainWindow方法,提供了一个方式来获取主窗口的原生句柄,便于与传统的 Win32 API 进行交互。

文件 App.xaml App.xaml.h App.xaml.cpp
代码
  <?xml version="1.0" encoding="utf-8"?>
<Application
    x:Class="WinUI3APP.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3APP"
    RequestedTheme="Light">
    <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>
  
  #pragma once

#include "App.xaml.g.h"

namespace winrt::WinUI3APP::implementation
{
    struct App : AppT<App>
    {
        App();

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

    private:
        static winrt::Microsoft::UI::Xaml::Window window;
    };
}
  
  #include "pch.h"
#include "App.xaml.h"
#include "MainWindow.xaml.h"
#include <microsoft.ui.xaml.window.h>

using namespace winrt;
using namespace Microsoft::UI::Xaml;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace winrt::WinUI3APP::implementation
{
    winrt::Microsoft::UI::Xaml::Window App::window{ nullptr };
    /// <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>
    App::App()
    {
        // Xaml objects should not call InitializeComponent during construction.
        // See https://github.com/microsoft/cppwinrt/tree/master/nuget#initializecomponent

#if defined _DEBUG && !defined DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION
        UnhandledException([](IInspectable const&, UnhandledExceptionEventArgs const& e)
        {
            if (IsDebuggerPresent())
            {
                auto errorMessage = e.Message();
                __debugbreak();
            }
        });
#endif
    }

    /// <summary>
    /// Invoked when the application is launched.
    /// </summary>
    /// <param name="e">Details about the launch request and process.</param>
    void App::OnLaunched([[maybe_unused]] LaunchActivatedEventArgs const& e)
    {
        window = make<MainWindow>();
        window.Activate();
    }
    HWND App::getMainWindow()
    {
        auto windowNative{ App::window.try_as<::IWindowNative>() };
        HWND hwnd{ 0 };
        windowNative->get_WindowHandle(&hwnd);
        return hwnd;
    }
}
  
🤖 代码解读
  • 声明 XML 文档头
  <?xml version="1.0" encoding="utf-8"?>
  

指定文档是 XML 格式,版本为 1.0,并使用 UTF-8 编码。

  • Application 元素
  <Application
    x:Class="WinUI3APP.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3APP"
    RequestedTheme="Light">
  
  1. <Application>:表示整个应用程序的根级别对象,管理应用程序生命周期。
  2. x:Class="WinUI3APP.App":绑定此 XAML 文件到应用程序的主类WinUI3APP.App,该类通常由 C++/WinRT 或 C# 编写,用于定义应用逻辑。
  3. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation":定义 XAML 命名空间,用于引用核心控件和功能。
  4. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml":引入x:命名空间,提供 XAML 中的附加功能(如类引用和类型转换)。
  5. xmlns:local="using:WinUI3APP"::本地命名空间,引用当前项目中的代码(WinUI3APP命名空间)。
  6. RequestedTheme="Light":设置应用程序的默认主题为浅色主题。
  • 声明全局样式和控件资源
  1. <Application.Resources>:定义应用程序级别的资源(如样式、模板和资源字典),可以在整个应用程序中共享。
  2. ResourceDictionary:封装应用程序资源,便于组织和管理。资源可以包括样式(Style)、数据模板(DataTemplate)和其他定义在 XAML 中的共享对象。
  3. ResourceDictionary.MergedDictionaries:定义多个外部资源字典的合并,使得他们可以集中管理并应用于当前字典。
  4. <XamlControlsResources>:命名空间using:Microsoft.UI.Xaml.Controls引入 WinUI 提供的默认控件样式和资源,确保应用程序控件在不同主题(浅色、深色、高对比度)下正常显示。
  5. <!-- Other app resources here --> 提示可以在此添加其他应用程序级别的资源,如自定义样式或模板。

这段 XAML 是一个典型的 WinUI3 应用程序入口文件的模板,其定义了应用程序的根元素Application;指定应用的主题为Light;合并了默认的控件资源样式;为整个应用预留了资源定义的空间。如果需要进一步扩展,可以在ResourceDictionary中添加自定义资源,如全局样式定义,字体或配色方案,及自定义控件模板等。

  1. #pragma once:确保头文件只会被编译一次,防止头文件被多次包含,避免重复定义问题。
  2. #include "App.xaml.g.h":包含由App.xaml生成的头文件App.xaml.g.h。该文件由编译器自动生成,包含了App.xaml定义的类和成员,实现 XAML 和后端代码的桥接。
  3. namespace winrt::WinUI3APP::implementation:命名空间声明。winrt是 C++/WinRT 的顶级命名空间。WinUI3APP是当前项目的命名空间。implementation 是 C++/WinRT 的惯例,表示具体的实现部分,对应的接口类通常位于winrt::WinUI3APP命名空间中。
  4. struct App : AppT<App>App类声明。AppT<App>是一个模板类,App是模板参数。AppT是由 C++/WinRT 自动生成的模板基类,提供App类的基础功能。
  5. App();:声明一个构造函数,用于初始化App类实例。
  6. void OnLaunched(Microsoft::UI::Xaml::LaunchActivatedEventArgs const&);:处理应用程序的启动事件。参数Microsoft::UI::Xaml::LaunchActivatedEventArgs const&包含应用启动时的上下文信息(如启动参数)。用于定义应用程序启动后要执行的逻辑,通常包括:创建主窗口,加载页面和显示 UI 等。
  7. static HWND getMainWindow();:静态方法getMainWindow返回主窗口的句柄(HWWND),用于与 Win32 API 或其他原生窗口操作的交互,提供对主窗口的访问。
  8. static winrt::Microsoft::UI::Xaml::Window window;:私有成员变量windowMicrosoft::UI::Xaml::Window类型的静态变量,表示应用的主窗口实例。静态变量属于类而非某个对象,确保应用中只有一个主窗口实例。

这段代码是 WinUI3 应用程序的主要入口类 App的声明部分,包含构造函数,用于初始化应用;OnLaunched方法,处理应用启动事件;静态方法getMainWindow,提供主窗口句柄;静态成员变量window,存储主窗口实例。在实现中,App类将结合 App.xaml文件提供的 XAML 定义,实现 WinUI3 应用程序的核心功能,包括窗口管理和事件处理。

  • 包含头文件
  1. #include "pch.h":引入预编译头文件,通常包含一些常用的标准库或项目头文件,以提高编译效率。
  2. #include "App.xaml.h":包含App.xaml自动生成的头文件,定义了应用程序的App类。
  3. #include "MainWindow.xaml.h":包含MainWindow.xaml自动生成的头文件,定义了应用程序的主窗口MainWindow
  4. #include <microsoft.ui.xaml.window.h>:包含用于与 XAML 窗口相关的 WinUI 头文件。
  • 使用命名空间
  1. using namespace winrt;:引入 C++/WinRT 库的命名空间,是代码更简洁。
  2. using namespace Microsoft::UI::Xaml;引入 WinUI 相关的 XAML 命名空间。
  • App类的静态成员变量初始化

winrt::Microsoft::UI::Xaml::Window App::window{ nullptr };:静态成员变量window,用于存储应用程序的主窗口实例。初始化为nullptr,表示没有窗口实例。winrt::Microsoft::UI::Xaml::Window是 WinUI 中表示窗口的类。

  • App构造函数
  1. 注释部分提醒开发者不应在构造函数中调用InitializeComponentInitializeComponent通常用于加载 XAML 定义的 UI 资源,在构造函数中被掉用可能会导致未定义行为。
  2. 调式代码块#if defined _DEBUG && !defined DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION条件编译中_DEBUG是仅在调试模式下启用;并如果未定义DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION宏,则执行调式异常处理逻辑。
  3. 未处理异常处理UnhandledException注册一个回调函数,用于处理应用程序中的未处理异常。异常发生时,该函数会被调用。回调函数的逻辑是使用IsDebuggerPresent()检查当前是否运行在调试模式下。如果是,调用e.Message()获取异常的详细信息(errorMessage)。errorMessage可能会用于日志记录或调试输出。并调用__debugbreak(),通知调试器中断程序运行,触发调试中断,便于检查问题。
  4. #endif:结束调试模式代码块,仅在_DEBUG定义且未禁用异常中断时生效。
  • OnLaunched方法。
  1. void App::OnLaunched([[maybe_unused]] LaunchActivatedEventArgs const& e)void指定函数返回类型为void,表示这个函数没有返回值。App::OnLaunched是一个类App的成员函数,名为OnLaunched。在 Windows 应用中,OnLaunched是应用程序启动时被调用的事件处理函数,用于初始化和显示主窗口。[[maybe_unused]]是一个标准的 C++ 属性(attribute),提示编译器参数e可能未被使用,从而避免编译器发出未使用变量的警告。LaunchActivatedEventArgs是 WinUI 提供的一个类,表示与启动应用相关的信息,如启动参数。const&指定参数为常数引用类型,防止其在函数内被修改,同时避免拷贝对象以提升性能。
  2. window = make<MainWindow>();window是一个成员变量,通常类型是winrt::Windows::UI::Xaml::Window或类似类型,表示应用的主窗口。make<MainWindow>()make是 WinRT 中的一个模板函数,用于创建由MainWindow定义的对象实例。MainWindow表示用户定义的主窗口类,通常继承自winrt::Windows::UI::Xaml::Window。尖括号<>用于为模板函数提供类型参数。这行代码实例化了一个MainWindow对象,并将其分配给window变量。
  3. window.Activate();Activate()调用窗口的Activate方法,将窗口设置为活动状态,使其在屏幕上可见。如果没有调用此方法,窗口可能不会显示。该函数通常还确保将窗口激活为用户的当前焦点窗口。

OnLaunched是应用启动时的入口点,主要用来初始化并激活主窗口。这段代码的作用是创建一个MainWindow实例并显示它,使得用户可以与之交互。其中,[[maybe_unused]]属性可以让编译器忽略未使用LaunchActivatedEventArgs参数的警告。

  • getMainWindow方法
  1. HWND App::getMainWindow()HWND是一个 Windows API 中的句柄类型,表示窗口的句柄(Window Handle),是用来标识窗口的唯一标识符。App::getMainWindow()App类的成员函数,名为getMainWindow,作用是获取应用的主窗口句柄,并将其返回。
  2. auto windowNative{ App::window.try_as<::IWindowNative>() };App::windowApp类中的一个(静态)成员变量,表示应用的主窗口对象,通常是winrt::Windows::UI::Xaml::Window对象。try_as是 WinRT 提供的一个方法,用于将对象转换为另一个接口类型。::IWindowNative是一个 COM 接口,提供与原生 Windows API 的桥接。IWindowNative的主要功能是获取窗口句柄(HWND)。这行代码将App::window转换为IWindowNative接口,如果转换成功,windowNative就是该窗口的一个实例。auto自动类型推断,windowNative的类型会根据try_as的返回值推断为winrt::com_ptr<IWindowNative>
  3. HWND hwnd{ 0 };:定义一个变量hwnd,用于存储窗口句柄。初始值设置为0,表示未初始化的句柄。句柄通过指针传递给IWindowNative::get_WindowHandle函数。
  4. windowNative->get_WindowHandle(&hwnd);get_WindowHandleIWindowNative接口的一个方法,作用是获取与当前窗口相关联的原生窗口句柄。&hwnd通过指针将句柄传递给该函数,用于接收窗口句柄的值。调用后,hwnd将包含与窗口相关联的实际句柄值。如果windowNative有效(即转换成功),这行代码会将主窗口的句柄值存储到hwnd中。
  5. return hwnd;:将存储在hwnd中的窗口句柄值返回给调用者。调用者可以使用这个句柄与Windows API进行交互,例如设置窗口属性、调整大小、发送消息等。

这段代码实现了一个简单的 WinUI3 应用程序,其中App类是应用程序的核心,负责初始化和启动;在构造函数中,设置了未处理异常的调试断点;在OnLaunched方法中,创建并激活了主窗口MainWindowgetMainWindow方法提供了一个方式来获取主窗口的原生句柄,便于与传统的 Win32 API 进行交互。

2-MainWindow

MainWindow页面中,MainWindow.xamlMainWindow.xaml.cppMainWindow.xaml.hMainWindow.idl中均移除了自动生成的按钮myButton和属性变量MyProperty相关部分的内容,保持为一个空白的页面。同页面和导航部分的MainWindow,仅在MainWindow.xaml中增加了<local:MainPage/>一行代码,指向MainPage页面。

3-MainPage

MainPage通过MainWindow加载,加载的代码为MainWindow.xaml文件下的<local:MainPage/>MainPage实现了用户点击按钮触发事件,加载图片文件到图片控件中显示的功能。

文件 MainPage.xaml MainPage.xaml.h MainPage.xaml.cpp MainPage.idl
代码
  <?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="WinUI3APP.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3APP"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Button x:Name="selectImageFileButton" Content="Open file" Click="selectImageFileButton_Click"/>
        <Image x:Name="image" Grid.Column="1" Margin="20">
            <Image.Source>
                <BitmapImage x:Name="bmp"/>
            </Image.Source>
        </Image>
    </Grid>
    
</Page>
  
  #pragma once

#include "MainPage.g.h"

namespace winrt::WinUI3APP::implementation
{
    struct MainPage : MainPageT<MainPage>
    {
    private:
        winrt::fire_and_forget openImageFile();
    public:
        MainPage(){}
        void selectImageFileButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
    };
}

namespace winrt::WinUI3APP::factory_implementation
{
    struct MainPage : MainPageT<MainPage, implementation::MainPage>
    {
    };
}
  
  #include "pch.h"
#include "MainPage.xaml.h"
#if __has_include("MainPage.g.cpp")
#include "MainPage.g.cpp"
#endif
#include "winrt/Windows.Storage.Pickers.h"
#include "App.xaml.h"
#include <shobjidl.h>

using namespace winrt;
using namespace Microsoft::UI::Xaml;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace winrt::WinUI3APP::implementation
{
	void MainPage::selectImageFileButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		openImageFile();
	}

	winrt::fire_and_forget MainPage::openImageFile()
	{
		Windows::Storage::Pickers::FileOpenPicker picker{};
		HWND hwnd = App::getMainWindow();
		picker.as<IInitializeWithWindow>()->Initialize(hwnd);
		picker.SuggestedStartLocation(Windows::Storage::Pickers::PickerLocationId::PicturesLibrary);
		picker.FileTypeFilter().Append(L".png");
		Windows::Storage::StorageFile file = co_await picker.PickSingleFileAsync();
		if (file != nullptr) {
			bmp().SetSource(co_await file.OpenAsync(Windows::Storage::FileAccessMode::Read));
		}
	}
}
  
  namespace WinUI3APP
{
    [default_interface]
    runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
    {
        MainPage();
    }
}
  
🤖 代码解读
  1. <Grid>:定义一个网格布局容器,用于组织页面中的子元素。该示例中将控件分布在两个列中,第一列放置按钮,第二列放置图片。
  • 定义列
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="Auto"/>
    <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
  

<Grid.ColumnDefinitions>定义网格的列。第一列为<ColumnDefinition Width="Auto"/>,宽度根据内容自适应,适合放置固定宽度的控件,如按钮;第二列<ColumnDefinition Width="*"/>,宽度占剩余空间,用于显示主要内容,如图片。

  • 按钮控件

<Button x:Name="selectImageFileButton" Content="Open file" Click="selectImageFileButton_Click"/>:定义一个按钮,用户点击后触发文件选择事件。x:Name="selectImageFileButton"定义按钮的标识符,供代码中引用。Content="Open file"按钮上显示的文本为 “Open file”Click="selectImageFileButton_Click"指定按钮的点击事件处理程序selectImageFileButton_Click,在MainPage的 C++ 代码中定义。

  • 图片控件
  <Image x:Name="image" Grid.Column="1" Margin="20">
    <Image.Source>
        <BitmapImage x:Name="bmp"/>
    </Image.Source>
</Image>
  

定义用于显示图像的控件。 x:Name="image"定义图片控件的标识符。Grid.Column="1"是将图片放置在网格的第2列(列索引从0开始)。Margin="20"设置控件的外边距(上下左右各20像素)。子元素<Image.Source>定义图片的来源。<BitmapImage x:Name="bmp"/>定义一个BitmapImage对象,表示图片的来源,通过bmp标识符可以在代码中操作图片源。

该部分 XAML 代码使用了网格布局将页面分为两列,一列置按钮;另一列置图片控件,显示用户选择的图像。当用户点击按钮触发事件,加载图片文件到图片控件中显示。

  1. winrt::fire_and_forget openImageFile();winrt::fire_and_forget表示该函数是一个协程,支持异步操作。函数执行完成后不返回任何值,也不会等待协程完成。openImageFile()声明一个私有的异步方法,用于打开图片文件。实际实现可能调用文件选择对话框,并将用户选择的图片加载到 UI 控件中。
  2. MainPage(){}:定义一个默认的构造函数,当前没有执行任何初始化操作。
  3. void selectImageFileButton_Click(...);:定义了按钮的点击事件处理程序。sender为事件的发起者;e为事件参数,包含触发事件的信息。
  • 头文件的引入
  1. winrt/Windows.Storage.Pickers.h:包含文件选择器的支持(如FileOpenPicker)。
  2. App.xaml.h:包含App类的头文件,用于调用全局方法(如getMainWindow())。
  3. <shobjidl.h>:包含 Window COM 接口的声明(如IInitializeWithWindow),用于与文件选择器交互。
  • 按钮点击事件处理程序
  void MainPage::selectImageFileButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
{
    openImageFile();
}
  

当按钮被点击时,调用openImageFile()方法,启动文件选择器。

  • 文件选择功能实现
  winrt::fire_and_forget MainPage::openImageFile()
{
    Windows::Storage::Pickers::FileOpenPicker picker{};
    HWND hwnd = App::getMainWindow();
    picker.as<IInitializeWithWindow>()->Initialize(hwnd);
    picker.SuggestedStartLocation(Windows::Storage::Pickers::PickerLocationId::PicturesLibrary);
    picker.FileTypeFilter().Append(L".png");
    Windows::Storage::StorageFile file = co_await picker.PickSingleFileAsync();
    if (file != nullptr) {
        bmp().SetSource(co_await file.OpenAsync(Windows::Storage::FileAccessMode::Read));
    }
}
  
  1. 方法声明与返回类型。winrt::fire_and_forget MainPage::openImageFile()winrt::fire_and_forget是有个返回类型,表示该方法是一个异步操作,但不会返回任何值。fire_and_forget是 WinRT 中的一种特性,表示该函数将执行异步任务,但调用者不需要等待任务完成。MainPage::openImageFile()表示该方法属于MainPage类。
  2. 创建文件选择器。Windows::Storage::Pickers::FileOpenPicker picker{};FileOpenPicker是一个文件选择器,用于让用户选择一个文件。这里,被用来选择图片文件。picker{}使用初始化列表创建一个FileOpenPicker对象。
  3. 获取窗口句柄并初始化选择器。HWND hwnd = App::getMainWindow();App::getMainWindow()调用App类的静态方法获取当前主窗口的句柄(HWND),将用来初始化文件选择器,使其能够与窗口进行交互。picker.as<IInitializeWithWindow>()->Initialize(hwnd);picker转换为IInitializeWithWindow接口,之后调用Initialize(hwnd)方法,将窗口句柄与文件选择器绑定。
  4. 设置文件选择器的起始位置与文件类型过滤器。picker.SuggestedStartLocation(Windows::Storage::Pickers::PickerLocationId::PicturesLibrary);SuggestedStartLocation设置文件选择器的起始目录为用户的图片库。这可以让用户从常用的图片目录开始选择文件。picker.FileTypeFilter().Append(L".png");FileTypeFilter设置文件过滤器,限制用户只能选择.png格式的文件。通过Append方法将文件扩展名.png添加到过滤器中。
  5. 异步选择文件。Windows::Storage::StorageFile file = co_await picker.PickSingleFileAsync();PickSingleFileAsync()是一个异步方法,打开文件选择器并等待用户选择一个文件。通过co_await关键字等待PickSingleFileAsync()操作完成。这意味着程序会暂停在这一行,直到用户选择了文件或者取消操作。如果用户选择了文件,则返回一个StorageFile对象;若取消选择,则返回nullptr
  6. 处理文件并显示图片。if (file != nullptr)检查是否文件被选择。如果文件对象为空,表示用户取消了选择,代码会跳过后续操作。bmp().SetSource(co_await file.OpenAsync(Windows::Storage::FileAccessMode::Read));file.OpenAsync(Windows::Storage::FileAccessMode::Read)为异步打开文件,并指定以读取模式(FileAccessMode::Read)打开。通过co_await等待文件打开完成。bmp().SetSource()将打开的文件内容设置为图片的来源。bmp()是 XAML 中绑定的BitmapImage控件。将文件内容通过SetSource方法设置为BitmapImage的源,从而显示在界面上。

如果需要支持多种图片格式,可以修改文件类型过滤器,允许选择多种文件类型,如,

  picker.FileTypeFilter().Append(L".png");
picker.FileTypeFilter().Append(L".jpg");
picker.FileTypeFilter().Append(L".bmp");
  

10.3.5 Sqlite 数据库和 ViewModel + UserControl

该部分示例包含用户信息数据录入、查看、编辑等功能,综合应用了 SQLite 数据库、ViewModel(视图模型)和 UserControl(用户控件)等技术。

PYC icon

*图 10-13 SQLite 读写数据库)

该项目文件结构主要包括:

  1. 应用程序核心部分(App):包含主应用入口文件(App.xaml)及其逻辑处理文件(后端代码)(App.xaml.cpp 和 App.xaml.h),负责应用程序的初始化和 UI 资源管理。
  2. 用户界面(UI):MainPage.xaml 和 MainWindow.xaml 分别定义了主页面和主窗口的 UI 布局。与之对应的 C++ 逻辑处理文件(如 MainPage.xaml.cpp、MainWindow.xaml.cpp)实现了页面的事件处理和功能逻辑。
  3. 用户控件(UserControl):UpdateUserControl.xaml 用于特定功能(如更新用户信息)的 UI 组件。相关的 C++ 文件(UpdateUserControl.xaml.cpp 和 UpdateUserControl.xaml.h)处理控件的事件和逻辑。
  4. 数据库支持(SQLite):sqlite3.c 和 sqlite3.h 包含 SQLite 数据库库文件,用于数据库的操作和管理。
  5. 视图模型(ViewModel):UserViewModel.idl 定义用户数据和视图逻辑的接口。相关的 C++ 实现文件(UserViewModel.cpp 和 UserViewModel.h)将 UI 和数据模型连接起来。
  6. 其他:Package.appxmanifest 配置应用程序的元数据,如权限、功能声明等。pch.cpp 和 pch.h 为预编译头文件,用于提高编译速度等。

该项目的结构清晰的分离了应用程序的各个模块,确保了 UI、业务逻辑、数据操作和视图模型之间的独立性和可维护性。核心逻辑与 UI 布局分开,使得项目可以方便地扩展和管理。

PYC icon

*图 10-13 SQLite 项目文件结构)

1-SQLite[sqlite3]数据库

SQLite,是一个 C 语言实现的小型、快速、自包含(self-contained)、高可靠性、功能齐全的 SQL 数据库引擎。SQLite 被广泛使用,内置于电话和计算机中,并捆绑到无数人们日常使用的应用程序中。SQLite 文件格式稳定,跨平台且向后兼容,开发者承若在 2050 年之前都会保持这种状态。从 SQLite 官网下载sqlite-amalgamation-3480000.zip文件,解压后,复制sqlite3.csqlite3.h两个文件置于项目WinUI3App(Desktop)新建的文件夹db中(db 文件夹上右键->Add->Existing Item...->选择 sqlite3.c和sqlite3.h)。

需要修改sqlite3.c文件,删除/************** Begin file sqlite3.h *****************************************/(第319行)到/************** End of sqlite3.h *********************************************/(第 13941行)之间的内容;并通过搜索sqlite3_version定位到下述代码,

  #ifndef SQLITE_AMALGAMATION
/* IMPLEMENTATION-OF: R-46656-45156 The sqlite3_version[] string constant
** contains the text of SQLITE_VERSION macro.
*/
SQLITE_API const char sqlite3_version[] = SQLITE_VERSION;
#endif
  

将其中#ifndef SQLITE_AMALGAMATION修改为#ifdef SQLITE_AMALGAMATION

同时修改该文件的两个属性,在sqlite3.c 文件上右键->Properties->C/C++->General->Warning Level属性值修改为Level3(/W3)C/C++->Precompiled Headers->Precompiled Header属性值修改为Not Using Precompiled HeadersWarning Level是 Microsoft C/C++ 编译器提供的一个设置,用于指定编译器在发生潜在问题时报告警告的详细级别。警告级别越高,编译器输出的警告信息越详细。Level3过滤掉了不常见或影响较小的警告信息,避免开发者因过多的警告而忽略真正需要关心的问题,兼顾开发效率和代码质量。预编译头(Precompiled Header)是 C/C++ 编译优化的一种技术,用于加快项目的编译速度,是通过将一些不经常变化的头文件(如标准库、第三方库)预先编译成二进制文件,以便在后续的编译中直接使用,而无需每次都重新处理这些头文件(预编译头文件通常被命名为stdafx.hpch.h)。而sqlite3.c是单一源文件,并且是一个自包含的实现,不依赖外部头文件,因此使用预编译没有意义,反而可能引入编译错误(如缺少pch.h)。而禁用预编译头可以简化编译过程,确保文件能够独立编译,并避免额外的配置和兼容性问题。

2-ViewModel[UserViewModel]

通过WinUI3App(Desktop)上右键->Add->New Item...->View Model(C++/WinRT)建立ViewModel(视图模型),命名为UserViewModel,会自动生成三个文件UserViewModel.idlUserViewModel.hUserViewModel.cpp

ViewModel 是 MVVM (Model-View-ViewModle)设计模式中的一个重要概念,在软件开发中广泛应用,尤其开发带有图形用户界面的应用程序。ViewModel 充当了视图(View)与数据模型(Model)之间的桥梁,并且负责处理展示逻辑。MVVM 软件架构模式将应用程序分为三个主要部分:

  1. Model(模型):代表应用程序的核心数据和业务逻辑,通常独立于用户界面(UI),并负责数据存储、数据处理等操作。
  2. View(视图):代表用户界面部分,用于展示数据,通常是应用程序中的界面元素(如按钮、文本框、标签等)。
  3. ViewModel(视图模型):充当 Model 和 View 之间的中介,负责给管理显示数据并将其绑定到 View,同时处理用户交互,将数据从 Model 转换为适合 View 显示的格式。

ViewModel 使得视图和模型之间的依赖关系降到最低。视图无需直接操作数据模型,也不需要知道模型的具体实现。ViewModel负责将数据传递到视图,并根据需要更新模型。ViewModel 与 View 之间的绑定,

  1. 数据绑定: 视图(View)通过数据绑定将 ViewModel 的属性连接到 UI 控件(如文本框、标签、按钮等)。
  2. 命令绑定:视图通过命令(如按钮点击)来触发 ViewModel 中的方法。

MVVM 的优点,

  1. 清晰的分离职责:MVVM 模式将 UI 和应用程序逻辑分开,帮助实现代码的清晰分离,便于维护和扩展。
  2. 可测试性:由于 ViewModel 是纯粹的逻辑层,与 UI 无关,因此容易进行单元测试。
  3. 灵活的 UI 更新:ViewModel 可以独立于视图进行更新,且支持响应式数据绑定,UI 可以根据数据的变化自动更新。
文件 UserViewModel.idl UserViewModel.h UserViewModel.cpp
代码
  namespace WinUI3App
{
    [bindable]
    [default_interface]
    runtimeclass UserViewModel 
    {
        UserViewModel();
        void SetUserData(Int32 id, String firstName, String lastName, String email);
        String FirstName{ get; };
        String LastName{ get; };
        String Email{ get; };
        Int32 Id{ get; };
    }
}
  
  #pragma once

#include "UserViewModel.g.h"

namespace winrt::WinUI3App::implementation
{
    struct UserViewModel : UserViewModelT<UserViewModel>
    {
    private:
        int32_t id;
        hstring firstName;
        hstring lastName;
        hstring email;
    public:
        UserViewModel() = default;
        void SetUserData(int32_t _id, hstring _firstName, hstring _lastName, hstring _email);
        hstring FirstName();
        hstring LastName();
        hstring Email();
        int32_t Id();
    };
}

namespace winrt::WinUI3App::factory_implementation
{
    struct UserViewModel : UserViewModelT<UserViewModel, implementation::UserViewModel>
    {
    };
}
  
  #include "pch.h"
#include "UserViewModel.h"
#if __has_include("UserViewModel.g.cpp")
#include "UserViewModel.g.cpp"
#endif

namespace winrt::WinUI3App::implementation
{
    void UserViewModel::SetUserData(int32_t _id, hstring _firstName, hstring _lastName, hstring _email)
    {
        id = _id;
        firstName = _firstName;
        lastName = _lastName;
        email = _email;
    }
    hstring UserViewModel::FirstName()
    {
        return firstName;
    }
    hstring UserViewModel::LastName()
    {
        return lastName;
    }
    hstring UserViewModel::Email()
    {
        return email;
    }
    int32_t UserViewModel::Id() {
        return id;
    }
}
  
🤖 代码解读
  • 命名空间声明

namespace WinUI3App:声明UserViewModel类属于WinUI3App命名空间,以防止命名冲突,同时便于逻辑模块化。

  • 属性特性
  [bindable]
[default_interface]
  
  1. [bindable]:表示此类可用于数据绑定,允许在 XAML 中绑定类的属性,例如绑定到TextBoxTextBlock等控件。数据绑定是 MVVM(Model-View-ViewModel)设计模式的重要组成部分。
  2. [default_interface]:指定UserViewModel的默认接口是类自身定义的接口。在 C++/WinRT 中,这可以简化类实例的使用,是因为在大多数情况下,不需要显示转换为接口类型。
  • 类定义

runtimeclass UserViewModelruntimeclass是 C++/WinRT 特有的关键字,表示一个可供 WinRT 环境使用的类,通常用于与 XAML 或其他 WinRT API 交互,自动生成必要的 COM 接口和元数据,以便在 WinRT 环境中正常工作。

  • 构造函数

UserViewModel();:是一个默认构造函数,用于初始化UserViewModel的实例;不接受参数,但可以在其实现中初始化成员变量或执行其他操作。

  • 方法(成员函数)

void SetUserData(Int32 id, String firstName, String lastName, String email);:定义了一个方法,用于批量设置用户数据;接收的参数,idInt32类型的用户 ID,firstName为用户的名字,lastName为用户的姓氏,email为用户的电子邮件地址。实现中通常会将这些参数值存储到私有字段或成员变量中,并可能触发属性的变更通知(用于数据绑定刷新)。

  • 只读属性
  String FirstName{ get; };
String LastName{ get; };
String Email{ get; };
Int32 Id{ get; };
  

定义了四个只读属性:FirstNameLastNameEmailId。每个属性都有get访问器,但没有set访问器,因此是只读的。这些属性通过SetUserData方法或构造函数进行初始化。

UserViewModel是一个 MVVM 模式中典型的 ViewModel 类,负责管理用户数据:通过SetUserData方法接收用户数据并存储;提供数据绑定支持:允许 XAML 用户界面通过数据绑定直接访问FirstNameLastName等属性;封装业务逻辑:在实际项目中,可能会包含额外的验证逻辑或通知机制(如INotifyPropertyChanged支持),以实现动态数据更新。

  1. #include "UserViewModel.g.h"引入由编译器自动生成的头文件,用于支持 XAML 的绑定功能,包含了与runtimeclass定义相关的元数据和模板代码。
  2. namespace winrt::WinUI3App::implementation定义在implementation命名空间中,表示这是UserViewModel的具体实现部分。
  3. struct UserViewModel : UserViewModelT<UserViewModel>struct UserViewModel表示定义了一个名为UserViewModel的结构体(在 C++ 中与类无实质性区别)。UserViewModelT<UserViewModel>是模板基类,提供与 WinRT 和 XAML 绑定相关的基础功能。继承自UserViewModelT的模板,可以自动实现大部分 WinRT 的接口。
  4. 对应 IDL 文件SetUserData()函数的输入参数和UserViewModel类属性,定义为私有成员变量idfirstNamelastNameemail
  5. UserViewModel() = default;声明一个默认构造函数,不执行任何初始化的操作。
  6. void SetUserData(int32_t _id, hstring _firstName, hstring _lastName, hstring _email);设置用户数据,将外部传入的值赋值给私有成员变量。
  7. 定义属性访问器hstring FirstName();返回用户的名字;hstring LastName();返回用户的姓氏;hstring Email();返回用户的电子邮件地址;int32_t Id();返回用户的ID。这些方法的实现部分会直接返回私有成员变量的值,用于外部读取。

通过私有成员变量存储用户数据为数据封装;通过公共方法提供对数据的访问和修改为接口暴露。

  1. SetUserData函数负责设置用户数据,将传入的参数值赋值给私有成员变量。即将参数_id, _firstName, _lastName, _email的值赋值给对应的成员变量id, firstName, lastName, email
  2. 属性访问器,FirstNameLastNameEmailId方法分别直接返回私有成员变量firstNamelastNameemailid的值。

3-Database[DbHelper]

用于封装 SQLite 数据库针对用户数据处理的常用操作,如打开、关闭数据库,检索、插入、更新和删除数据等。

文件 DbHelper.h DbHelper.cpp
代码
  #pragma once
#include "db/sqlite3.h"
#include "UserViewModel.h"

namespace winrt::WinUI3App::implementation {
	class DbHelper
	{
	private:
		sqlite3* db;
		bool doesTableExist(std::string tableName);
		bool doesRecordExist(const char* sql);
		bool executeStatement(const char* sql);
		int getNextId();
		hstring convertToHString(const unsigned char*);

	public:
		void openDataBase(const char* path);
		void closeDatabase();

		void getAllUsers(Windows::Foundation::Collections::IObservableVector<WinUI3App::UserViewModel>& array);
		bool updateUser(int id, hstring firstName, hstring lastName, hstring email);
		bool deleteUser(int id);
	};
}
  
  #include "pch.h"
#include "DbHelper.h"

namespace winrt::WinUI3App::implementation {
	void DbHelper::openDataBase(const char* path)
	{
		int result = sqlite3_open(path, &db);
		if (result == SQLITE_OK) {
			OutputDebugString(L"Amazing!. It actually worked!\r\n");
			if (!doesTableExist("users")) {
				executeStatement(std::string{ "create table users (id int primary key, first_name text, last_name text, email text);" }.c_str());
				OutputDebugString(L"Tabel 'users' created!\r\n");
			}
		}
		else {
			OutputDebugString(L"ERROR: Cannot open db~");
		}
	}

	bool DbHelper::doesTableExist(std::string tableName)
	{
		return doesRecordExist(std::string{ "select 1 from sqlite_master where type='table' and name='" + tableName + "'" }.c_str());
	}

	bool DbHelper::doesRecordExist(const char* sql)
	{
		bool exists = false;
		sqlite3_stmt* st;
		if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK) {
			exists = sqlite3_step(st) == SQLITE_ROW;
		}
		sqlite3_finalize(st);

		return exists;
	}

	bool DbHelper::executeStatement(const char* sql)
	{
		return sqlite3_exec(db, sql, NULL, NULL, NULL) == SQLITE_OK;
	}

	void DbHelper::closeDatabase()
	{
		sqlite3_close(db);
	}

	int DbHelper::getNextId()
	{
		int nextId = 0;
		sqlite3_stmt* st;
		if (sqlite3_prepare_v2(db, "select max(id) from users", -1, &st, NULL) == SQLITE_OK) {
			if (sqlite3_step(st) == SQLITE_ROW)
				nextId = sqlite3_column_int(st, 0);
		}
		sqlite3_finalize(st);
		return ++nextId;
	}

	void DbHelper::getAllUsers(Windows::Foundation::Collections::IObservableVector<WinUI3App::UserViewModel>& array)
	{
		sqlite3_stmt* st;
		if (sqlite3_prepare_v2(db, "select id, first_name, last_name, email from users order by first_name", -1, &st, NULL) == SQLITE_OK) {
			while (sqlite3_step(st) == SQLITE_ROW) {
				WinUI3App::UserViewModel u = winrt::make<WinUI3App::implementation::UserViewModel>();
				u.SetUserData(sqlite3_column_int(st, 0), convertToHString(sqlite3_column_text(st, 1)), convertToHString(sqlite3_column_text(st, 2)), convertToHString(sqlite3_column_text(st, 3)));
				array.Append(u);
			}

		}
		sqlite3_finalize(st);
	}

	bool DbHelper::updateUser(int id, hstring firstName, hstring lastName, hstring email)
	{
		bool updated = false;
		int finalId = id == 0 ? getNextId() : id;
		const char* sql;
		/* Notice we left the field "id" at the end, so both INSERT and UPDATE have the same "bind" field order*/
		if (id == 0) {
			sql = "insert into users (first_name, last_name, email, id) values (?, ?, ?, ?);";
		}
		else {
			sql = "update users set first_name=?, last_name=?, email=? where id=?;";
		}
		sqlite3_stmt* st;
		if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK) {
			sqlite3_bind_text(st, 1, winrt::to_string(firstName).c_str(), -1, SQLITE_TRANSIENT);
			sqlite3_bind_text(st, 2, winrt::to_string(lastName).c_str(), -1, SQLITE_TRANSIENT);
			sqlite3_bind_text(st, 3, winrt::to_string(email).c_str(), -1, SQLITE_TRANSIENT);
			sqlite3_bind_int(st, 4, finalId);
			updated = sqlite3_step(st) == SQLITE_DONE;
		}
		sqlite3_finalize(st);

		return updated;
	}

	bool DbHelper::deleteUser(int id)
	{
		bool deleted = false;
		sqlite3_stmt* st;
		if (sqlite3_prepare_v2(db, "delete from users where id=?", -1, &st, NULL) == SQLITE_OK) {
			sqlite3_bind_int(st, 1, id);
			deleted = sqlite3_step(st) == SQLITE_DONE;
		}
		sqlite3_finalize(st);
		return deleted;
	}

	hstring DbHelper::convertToHString(const unsigned char* value)
	{
		return winrt::to_hstring(std::string{ reinterpret_cast<char const*>(value) });
	}
}
  
🤖 代码解读

这段代码定义了DbHelper类,封装了对 SQLite 数据库常见操作,特别是用于管理用户数据的功能。

  • 头文件的引入
  1. #include "db/sqlite3.h":用于访问 SQLite 数据库的 C 库头文件(该文件的配置见前文)。
  2. #include "UserViewModel.h":包含用户数据模型的定义,UserViewModel类封装了用户的相关信息。
  • DbHelper 类的定义
  1. 私有成员中sqlite3* db为 SQLite 数据库的指针,用于执行数据库操作。私有方法有doesTableExist(std::string tableName),检查指定名称的表是否存在;doesRecordExist(const char* sql)根据给定的 SQL 查询检查是否有记录符合条件;executeStatement(const char* sql)执行给定的 SQL 语句(例如INSERTUPDATEDELETE等);getNextId()获取下一个可用的 ID (可能用于生成自增的 ID);convertToHString(const unsigned char*)将 SQLite 返回的 unsigned char*转换为hstring,便于在 WinUI 中使用。
  2. 公有成员中openDataBase(const char* path)用于打开数据库文件,接受数据库路径作为参数;closeDatabase()用于关闭数据库连接;getAllUsers(Windows::Foundation::Collections::IObservableVector<WinUI3App::UserViewModel>& array);查询所有用户并将其存入传入的IObservableVector<UserViewModel>集合(向量/数组)中。IObservableVector是一个支持数据绑定的集合类型,常用于 UI 更新;updateUser(int id, hstring firstName, hstring lastName, hstring email)根据用户 ID 更新用户数据。返回布尔值表示是否成功执行;deleteUser(int id)删除指定 ID 的用户记录。返回布尔值表示操作是否成功。

DbHelper类的主要目的是封装 SQLite 数据库的操作,提供一系列方法来处理用户数据,包括打开和关闭数据库连接;执行 SQL 语句来插入、更新和删除数据;获取所有用户数据并填充到IObservableVector<UserViewModel>中,以便在 WinUI 中显示;检查表或记录是否存在等。

这段代码是DbHelper类的实现,用于与 SQLite 数据库进行交互,提供了与数据库的连接、表格检查、数据插入、更新、删除及查询用户数据等功能。

  • 打开数据库文件:void DbHelper::openDataBase(const char* path)
  1. openDataBase成员函数接受一个参数path,表示数据库文件的路径(const char*类型)。
  2. int result = sqlite3_open(path, &db);调用 SQLite 库中的sqlite3_open函数来打开数据库,path是数据库文件的路径,&db是一个指向sqlite3*类型的指针,用于接收数据库连接。如果打开数据库成功,result会被设置为SQLITE_OK,否则会返回一个错误码。
  3. if (result == SQLITE_OK),如果sqlite3_open返回的result等于SQLITE_OK,表示数据库成功打开,进入if语句块。
  4. OutputDebugString(L"Amazing!. It actually worked!\r\n");,调用OutputDebugString函数将调试信息输出到调试器。如果成功打开数据库,将输出"Amazing!. It actually worked!"
  5. if (!doesTableExist("users")) ,调用一个名为doesTableExist的函数,传入"users"作为参数,检查数据库中是否存在名为users的表。如果返回false,表示该表不存在,进入if语句块。
  6. executeStatement(std::string{ "create table users (id int primary key, first_name text, last_name text, email text);" }.c_str());,使用executeStatement函数执行一条 SQL 语句,创建一个名为users的表。该表包括四个字段:id(整数类型,主键),first_name(文本类型),last_name(文本类型),email(文本类型)。std::string{ ... }.c_str()将 C++ 字符串转换为 C 风格字符串(const char*)。
  7. OutputDebugString(L"Tabel 'users' created!\r\n");,如果成功执行创建表的 SQL 语句,将输出调试信息"Table 'users' created!"
  8. OutputDebugString(L"ERROR: Cannot open db~");,如果sqlite3_open返回的结果不为SQLITE_OK(即数据库无法打开),进入else语句块,输出错误信息"ERROR: Cannot open db~"

该函数尝试打开一个 SQLite 数据库。如果打开成功,检查是否存在名为users的表。如果该表不存在,则创建一个新的users表。如果数据库无法打开,会输出错误提示信息。

  • 检查指定名称的表是否存在:bool DbHelper::doesTableExist(std::string tableName)
  1. doesTableExist成员函数接受一个std::string类型的参数tableName,表示要检查的表的名称。函数返回一个布尔值(bool),表示表是否存在。
  2. return doesRecordExist(std::string{ "select 1 from sqlite_master where type='table' and name='" + tableName + "'" }.c_str());,这一行调用了doesRecordExist函数,并将一条 SQL 查询语句作为参数传递给它。SQL 查询的目的是检查数据库中是否存在指定名称的表。sqlite_master是 SQLite 内部使用的一个特殊表,存储数据库中所有对象的信息、包括表、索引和视图等。查询条件type='table'用来限定只查找类型为表的对象。name='tableName'检查表名是否匹配传入的tableName"select 1 from sqlite_master where type='table' and name='" + tableName + "'"动态生成一个 SQL 查询语句,将tableName插入到查询字符中,构建查询检查表是否存在。std::string{ ... }通过字符串拼接创建查询字符串,+ tableName将函数参数tableName插入到 SQL 语句的name部分。最终通过c_str()将生成的 C++ 字符串转换为 C 风格字符串(const char*),并传递给doesRecordExist函数。
  • 根据给定的 SQL 查询检查是否有记录符合条件:bool DbHelper::doesRecordExist(const char* sql)
  1. doesRecordExist成员函数接受一个const char*类型的参数sql,表示要执行的 SQL 查询。函数返回一个布尔值(bool),表示是否存在符合查询条件的记录。
  2. bool exists = false;,变量exists初始化为false,用于存储查询结果。如果查询到记录,exists将被设置为true
  3. sqlite3_stmt* st;,声明一个指向sqlite3_stmt类型的指针st,表示 SQL 语句的预处理句柄。sqlite3_stmt是 SQLite 的核心数据结构之一,用于表示已编译的 SQL 语句。编译 SQL 语句是指将文本形式的 SQL 语句转换为 SQLite 内部能够理解的二进制格式。这个二进制格式由sqlite3_stmt对象表示,用来执行动态的 SQL 查询或命令,并可以多次绑定参数和重新执行。sqlite3_stmt*是一个指针,用于操作sqlite3_stmt对象,由 SQLite API 函数返回(如sqlite3_prepare_v2()),开发者需要通过该指针控制 SQL 语句的执行。sqlite3_stmt*通常用于以下场景:

-编译 SQL 语句。使用sqlite3_prepare_v2()函数将 SQL 语句编译成sqlite3_stmt对象。

  sqlite3_stmt* st;
const char* sql = "SELECT * FROM users WHERE id = ?";
sqlite3_prepare_v2(db, sql, -1, &st, nullptr);
  

-绑定参数。使用sqlite3_bind_* 系列函数将值绑定到预处理语句中的参数(?占位符)。

  sqlite3_bind_int(st, 1, user_id);
  

-执行语句。使用sqlite3_step()执行预处理的语句,逐步获取查询结果。

  while (sqlite3_step(st) == SQLITE_ROW) {
    const unsigned char* name = sqlite3_column_text(st, 0);
    printf("User Name: %s\n", name);
}
  

-是否资源。使用sqlite3_finalize()释放sqlite3_stmt对象占用的资源。

  sqlite3_finalize(st);
  

sqlite3_stmt*是动态分配的,需要手动管理其生命周期。当完成对sqlite3_stmt*的使用后,必须调用sqlite3_finalize()来释放内存,否则会造成内存泄漏。每个sqlite3_stmt对象只能与一个数据库连接绑定,且在同一时间只能由一个线程使用。在绑定参数或读取结果时,应确保数据类型匹配,否则可能会导致运行时错误。

  1. if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK) sqlite3_prepare_v2是 SQLite 提供的一个 API,用于将文本形式的 SQL 语句编译成二进制的 SQLite 内部格式(sqlite3_stmt对象),是执行 SQL 查询或命令的第一步,之后可以使用返回的sqlite3_stmt对象进行操作。参数sqlite3* db为已打开的数据库连接句柄,通过sqlite3_open()sqlite3_open_v2()获得,表示将在此数据库上执行编译和查询操作。const char* zSql为待编译的 SQL 语句,必须是 UTF-8 编码。可以包含多个 SQL 命令(如SELECTINSERT),但函数只编译第一个命令。int nByte指定 SQL 语句的字节长度,如果为 -1则编译直到遇到字符串结束符\0。如果提供了具体的字节数,只会编译该长度范围内的 SQL。sqlite3_stmt** ppStmt为输出参数,用于接收已编译的 SQL 语句对象。成功时,ppStmt指向一个sqlite3_stmt对象,该对象用于绑定参数和执行查询。const char** pzTail为输出参数,用于返回zSql中未编译部分的起始位置。如果为NULL,则忽略未编译部分。如果sqlite3_prepare_v2返回SQLITE_OK,表示编译成功;SQLITE_ERROR,表示 SQL 语法错误;SQLITE_NOMEM表示内存不足;SQLITE_BUSY表示数据库忙。
  2. exists = sqlite3_step(st) == SQLITE_ROW;,如果预处理成功(返回值为SQLITE_OK),则进入if语句块。sqlite3_step执行查询语句,返回查询结果的状态,SQLITE_ROW表示查询成功并返回至少一行数据;SQLITE_DONE表示查询没有返回数据。如果sqlite3_step返回SQLITE_ROW,表示查询到了符合条件的记录,将exists设置为true
  3. sqlite3_finalize(st);用于释放预处理语句句柄st,清理资源,防止内存泄漏。
  4. return exists;,返回exists,如果查询到至少一行数据,existstrue,否则为false

doesRecordExist函数执行给定的 SQL 查询,并检查是否有符合条件的记录。通过sqlite3_prepare_v2预处理 SQL 查询,使用sqlite3_step执行查询,并根据返回值判断是否存在符合条件的记录。最后,调用sqlite3_finalize清理预处理句柄,并返回查询结果(布尔值)。

  • 执行给定的 SQL 语句:bool DbHelper::executeStatement(const char* sql)
  1. sqlite3_exec(db, sql, NULL, NULL, NULL) == SQLITE_OK;,被调用来执行sql中的语句,并通过返回值检查是否成功。参数sqlite3* db为已打开的数据库连接句柄,通过sqlite3_open()sqlite3_open_v2()获得。表示在此数据库上执行sql语句。const char* sql为要执行的 SQL 查询或命令字符串。可以包含多条 SQL 命令(用分号;分隔),sqlite3_exec会逐一执行。int (*callback)(void*, int, char**, char**)为执行SELECT查询时的回调函数,用于处理结果集的每一行。回调函数的参数第1个void*类型,表示用户提供的附加数据(通过arg传入);第2个参数是查询结果的列数;第3个参数是包含每列值的数组;第4个参数是包含每列名称的数组。如果设置为NULL,表示不需要处理回调,通常用于非查询语句。void* arg传递个回调函数的第1个参数,可用于传递上下文数据。如果不需要,可设置为NULLchar** errmsg指向字符串指针,用于返回执行失败时的错误消息。成功时,指针不会改变;失败时,指针指向动态分配的字符串,需调用sqlite3_free()释放内存。sqlite3_exec返回一个整数,用于指示操作是否成功。返回SQLITE_OK(0)为执行成功,其他值为执行失败,如SQLITE_ERROR表示 SQL 语法错误;SQLITE_BUSY为数据库忙;SQLITE_MISUSE为 API 使用错误。该语句调用sqlite3_exec执行sql中的 SQL 命令。将回调函数和上下文数据设置为NULL,意味着不需要处理查询结果或传递附加数据(一般用于INSERTUPDATEDELETE或非查询操作)。将错误消息参数设置为NULL,表示不需要返回错误信息。比较返回值是否为SQLITE_OK,以判断是否执行成功。
  • 关闭数据库连接:void DbHelper::closeDatabase()
  1. sqlite3_close(db);,使用sqlite3_close函数关闭由sqlite3_open打开的数据库连接。参数sqlite3* db为数据库连接句柄。成功关闭数据库返回SQLITE_OK;关闭失败则为其他错误代码。sqlite3_close会释放与数据库连接相关的所有资源,避免资源泄漏,提升应用的稳定性。如果存在未释放的语句句柄(例如未调用sqlite3_finalize),关闭操作可能会失败。为确保数据库始终能被正确关闭,可以在异常或程序结束时调用closeDatabase,添加错误检测和连接状态检查,进一步增强其可靠性。如,
  void DbHelper::closeDatabase()
{
    if (db != nullptr) {
        if (sqlite3_close(db) == SQLITE_OK) {
            db = nullptr; // 确保指针不再指向释放的资源
        } else {
            OutputDebugString(L"Failed to close the database.\n");
        }
    }
}
  
  • 获取下一个可用的 ID:int DbHelper::getNextId()

getNextId是一个用于获取数据库表中主键字段id的下一个可用值的函数。是通过查询users表中id的最大值来推断下一个主键值。返回的值通常用于插入新记录,确保主键的唯一性。

  1. int nextId = 0;定义并初始化变量nextId0,用于存储查询结果,为表中当前最大id的值。
  2. sqlite3_stmt* st;声明一个类型为sqlite3_stmt*的指针变量st,用于存储 SQL 语句的预处理句柄。
  3. if (sqlite3_prepare_v2(db, "select max(id) from users", -1, &st, NULL) == SQLITE_OK)调用 SQLite 的sqlite3_prepare_v2函数对 SQL 查询进行预处理,目的是从users表中查询最大id值。"select max(id) from users"为 SQL 查询语句,获取users表中id列的最大值。
  4. 执行查询并获取结果。if (sqlite3_step(st) == SQLITE_ROW)是使用sqlite3_step执行预处理后的 SQL 查询。如果返回SQLITE_ROW,表示查询返回了一行数据;如为其他值,表示查询没有返回数据或出错。当返回值为SQLITE_ROW,执行nextId = sqlite3_column_int(st, 0);,获取查询结果第一列的值(max(id)的结果),并赋值给变量nextId
  5. sqlite3_finalize(st);调用sqlite3_finalize函数释放预处理语句句柄st
  6. return ++nextId;,返回nextId之前,将其递增1,表示下一条记录的id是当前最大id加1。

该函数的功能是从users表中查询当前最大id,将其加1并返回。但存在潜在的问题,如并发问题:如果多个线程或进行同时插入数据,可能导致nextId冲突;性能影响:每次获取新 ID 都需要查询整个表,随着数据量的增大,可能影响性能;表为空时的情况:当表为空时,查询max(id)返回NULL,当前逻辑通过初始化nextId = 0处理了此情况。对当前函数的改进可以检查sqlite3_prepare_v2sqlite3_step的返回值,处理失败情况(异常处理),如,

  int DbHelper::getNextId()
{
    int nextId = 0;
    sqlite3_stmt* st;
    if (sqlite3_prepare_v2(db, "select max(id) from users", -1, &st, NULL) != SQLITE_OK) {
        OutputDebugString(L"Failed to prepare SQL statement.\n");
        return -1; // 返回错误值
    }

    if (sqlite3_step(st) == SQLITE_ROW) {
        nextId = sqlite3_column_int(st, 0);
    } else {
        OutputDebugString(L"Failed to retrieve the maximum ID.\n");
    }

    sqlite3_finalize(st);
    return ++nextId;
}
  

使用数据库自增功能替代手动管理 ID,避免冲突(并发控制),如,

  create table users (
    id integer primary key autoincrement,
    first_name text,
    last_name text,
    email text
);
  

如果手动管理 ID,考虑在程序内维护一个计数器变量,减少频繁的查询(优化性能),如,

  int lastId = getNextId(); // 初始化时获取最大 ID
// 插入新记录时:
int newId = ++lastId;
  

当前实现逻辑简单可靠,适合小型应用和低并发场景。如果需要处理高并发或大数据场景,推荐改用 SQLite 的AUTOINCREMENT功能,简化逻辑并提高效率。

  • 检索所有用户记录并存入容器中:void DbHelper::getAllUsers(Windows::Foundation::Collections::IObservableVector<WinUI3App::UserViewModel>& array)

getAllUsers函数从数据库中的users表中检索所有用户记录,并将这些记录填充到提供的IObservableVector<WinUI3App::UserViewModel>类型的容器中,每条记录对应一个UserViewModel实例。

  1. void DbHelper::getAllUsers(Windows::Foundation::Collections::IObservableVector<WinUI3App::UserViewModel>& array)DbHelper类的一个成员函数,名为getAllUsers,其作用是从 SQLite 数据库中的users表中获取所有用户数据。array是传入的参数,是一个IObservableVector<WinUI3App::UserViewModel>类型的引用,表示一个可观察的动态数组。函数通过该数组返回用户数据。
  2. sqlite3_stmt* st;,声明一个指向sqlite3_stmt类型的指针st,用于存储 SQL 语句的预处理句柄。
  3. if (sqlite3_prepare_v2(db, "select id, first_name, last_name, email from users order by first_name", -1, &st, NULL) == SQLITE_OK),调用sqlite3_prepare_v2函数对 SQL 查询进行预处理。该查询从users表中选择idfirst_namelast_name,和email字段,并按first_name字段排序。如果预处理成功(返回值为SQLITE_OK),则进入if语句块。
  4. while (sqlite3_step(st) == SQLITE_ROW),使用sqlite3_step执行查询语句。每次调用sqlite3_step返回的值表示查询的状态,sqlite3_step表示查询成功并返回一行数据;SQLITE_DONE表示查询已完成,没有更多数据。如果返回值为SQLITE_ROW,表示查询到了一行数据,进入while循环。
  5. WinUI3App::UserViewModel u = winrt::make<WinUI3App::implementation::UserViewModel>();为创建用户视图模型并设置数据。使用winrt::make创建一个UserViewModel类型的对象u,用于存储每个用户的数据。
  u.SetUserData(
    sqlite3_column_int(st, 0),
    convertToHString(sqlite3_column_text(st, 1)),
    convertToHString(sqlite3_column_text(st, 2)),
    convertToHString(sqlite3_column_text(st, 3))
);
  
  1. 调用UserViewModel类中的SetUserData方法,设置用户的数据。该方法接受四个参数,sqlite3_column_int(st, 0):获取当前行的第0列(id),并将其作为整数传递;convertToHString(sqlite3_column_text(st, 1)):获取当前行的第1列(first_name),然后通过convertToHString函数将其转换为HString类型;convertToHString(sqlite3_column_text(st, 2))convertToHString(sqlite3_column_text(st, 3))则分别对应第2、3列,为last_nameemail,并分别转换为HStringconvertToHString是一个辅助函数,用于将const char*类型的 C 字符串转换为 HString类型。sqlite3_column_*是 SQLite 提供的一组函数,用于根据列的索引或列名,从查询结果中提取特定的值。不同类型的数据列有不同的函数,格式如下,sqlite3_column_text()获取文本列的值;sqlite3_column_int()获取整数列的值;sqlite3_column_double()获取浮点(Real)列的值;sqlite3_column_blob()获取二进制数据列的值;sqlite3_column_bytes()获取列的数据长度(字节数);sqlite3_column_type()获取列的数据类型。
  2. array.Append(u);将用户数据添加到数组array中。
  3. sqlite3_finalize(st);调用sqlite3_finalize函数,释放预处理语句句柄st,清理资源。

该函数从数据库中查询所有用户数据,并将每个用户的数据存储到array数组中。array是一个IObservableVector<WinUI3App::UserViewModel>类型的数组。其执行流程为,使用sqlite3_prepare_v2准备 SQL 查询语句,查询users表中的idfirst_namelast_nameemail字段;使用sqlite3_step循环遍历查询结果;对于每一行数据,创建一个UserViewModel对象并填充数据;将UserViewModel对象添加到array中;释放 SQL 语句句柄,清理资源。

  • 根据用户 ID 更新用户数据:bool DbHelper::updateUser(int id, hstring firstName, hstring lastName, hstring email)

updateUser用于更新或插入用户数据到数据库的users表,是根据id来决定执行插入(INSERT)还是更新(UPDATE)操作。

函数定义。

  1. bool DbHelper::updateUser(int id, hstring firstName, hstring lastName, hstring email),参数id为用的id,如果为0,则表示插入操作;firstName用户的名;lastName用户的姓;email用户的电子邮件。返回值是布尔值updated,如果操作成功返回true;否则返回false

初始化变量。

  1. bool updated = false;定义初始化变量updatedfalse。这个变量用于表示操作是否成功,如果成功执行插入或更新操作,并将该变量设置为true
  2. int finalId = id == 0 ? getNextId() : id;用于确定插入或更新操作的id。如果id==0,表示需要插入新的用户记录,因此调用getNextId()获取下一个可用的id;如果id不为0, 使用传入的id来执行更新操作。

SQL 查询语句选择。

      const char* sql;
    if (id == 0) {
        sql = "insert into users (first_name, last_name, email, id) values (?, ?, ?, ?);";
    }
    else {
        sql = "update users set first_name=?, last_name=?, email=? where id=?;";
    }
  

根据id的值,选择要执行的 SQL 查询语句:

  1. 插入操作(INSERT):如果id==0,选择插入查询语句,插入新的first_namelast_nameemailidusers表。
  2. 更新操作(UPDATE):如果id != 0,选择更新查询语句,更新已有用户记录的first_namelast_nameemail,条件是id匹配。

预处理 SQL 查询。

      sqlite3_stmt* st;
    if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK) 
  
  1. 调用sqlite3_prepare_v2函数对 SQL 查询进行预处理。如果处理成功返回SQLITE_OK,进入if语句块。

绑定 SQL 参数。

          sqlite3_bind_text(st, 1, winrt::to_string(firstName).c_str(), -1, SQLITE_TRANSIENT);
        sqlite3_bind_text(st, 2, winrt::to_string(lastName).c_str(), -1, SQLITE_TRANSIENT);
        sqlite3_bind_text(st, 3, winrt::to_string(email).c_str(), -1, SQLITE_TRANSIENT);
        sqlite3_bind_int(st, 4, finalId);
  

firstNamelastNameemailfinalId的值绑定到 SQL 查询语句中的参数:

  1. sqlite3_bind_text:将firstNamelastNameemail转换为std::string(使用winrt::to_string),然后绑定到查询语句中的相应位置。SQLITE_TRANSIENT表示数据绑定时使用临时内存。
  2. sqlite3_bind_int:将finalId(用户的id)绑定到查询语句中的第4个参数位置。

执行查询。

  1. updated = sqlite3_step(st) == SQLITE_DONE;:调用sqlite3_step执行预处理的 SQL 查询,并返回执行状态。如果为SQLITE_DONE,表示 SQL 语句成功执行(插入或更新成功);其他返回值表示执行失败。如果执行成功且返回SQLITE_DONE,则将updated设置为true,表示操作成功。

结束 SQL 语句。

  1. sqlite3_finalize(st);:调用sqlite3_finalize函数释放预处理语句句柄st,清理资源,防止内存泄漏。

返回结果。

  1. return updated;:返回updated,表示操作是否成功。如果操作成功(插入或更新),返回true;否则返回false

该函数用于更新或插入用户数据到数据库的users表。如果id为0, 则执行插入操作;如果id不为0,则执行更新操作。其执行流程为,确定最终的id(如果id为0, 则调用getNextId()获取下一个可用的id);根据id的值选择相应的 SQL查询(插入或更新);预处理 SQL 查询语句;将firstNamelastNameemailid绑定到 SQL 查询中;执行查询,并检查操作是否成功;清理资源,释放预处理语句句柄;返回操作是否成功额结果。

  • 删除指定 ID 的用户记录:bool DbHelper::deleteUser(int id)
  1. bool DbHelper::deleteUser(int id)根据传入的id删除对应的用户记录。返回值是布尔值deleted,如果删除操作成功则返回true;否则返回false
  2. bool deleted = false;,定义并初始化变量deletedfalse。该变量用于表示删除操作是否成功。如果成功则返回true
  3. sqlite3_stmt* st;为 SQL 语句的预处理句柄。
  4. if (sqlite3_prepare_v2(db, "delete from users where id=?", -1, &st, NULL) == SQLITE_OK),调用sqlite3_prepare_v2函数对 SQL 查询语句进行预处理。如果预处理成功,则进入if语句块。
  5. sqlite3_bind_int(st, 1, id);调用sqlite3_bind_int函数将id参数绑定到 SQL 查询语句中的第一个参数(?),用于指定要删除的用户id
  6. deleted = sqlite3_step(st) == SQLITE_DONE;调用sqlite3_step执行预处理的 SQL 查询。SQLITE_DONE表示 SQL 语句成功执行(删除操作成功)。如果执行成功且返回SQLITE_DONE,则将deleted设置为true,表示删除操作成功。
  7. sqlite3_finalize(st);调用sqlite3_finalize函数释放预处理语句句柄st,清理资源。
  8. return deleted;返回deleted,表示删除操作是否成功。如果成功返回ture;否则返回false

该函数用于删除users表中指定id的用户数据。其执行流程为,定义deleted变量,用于标识删除操作是否成功;使用sqlite3_prepare_v2函数预处理删除 SQL 查询;使用sqlite3_bind_int函数绑定要删除的用户id;执行 SQL 查询,检查删除操作是否成功;释放 SQL 语句句柄,清理资源;返回删除操作是否成功的结果。

  • unsigned char*类型的值转换为hstring类型:hstring DbHelper::convertToHString(const unsigned char* value)
  1. convertToHString用于将unsigned char*类型的value转换为hstring类型。参数value是一个指向unsigned char类型数据的指针(通常是来自数据库查询的字符串值),需要被转换为hstring。返回值是一个hstring类型的对象,表示转换后的字符串。
  2. return winrt::to_hstring(std::string{ reinterpret_cast<char const*>(value) });,首先将unsigned char*类型的value转换为std::string类型,再将其转换为hstring类型并返回。reinterpret_cast<char const*>(value)unsigned char*转换为const char*reinterpret_cast是一种类型转换,允许强制将一个类型转换为其他类型。因为std::string构造函数需要一个const char*类型的参数初始化,因此这里将unsigned char*转换为const char*std::string{ ... }将转换后的const char*类型指针传给std::string构造函数,生成一个std::string对象。此处std::string构造函数会处理原始字节数据,生成一个正常的 C++ 字符串。winrt::to_hstring(...)std::string对象转换为hstring类型。winrt::to_hstring是 WinRT(Windows Runtime)提供的一个函数,用于将 C++ 标准字符串类型(如std::string)转换为hstring类型,后者是 WinUI 中的字符串类型。

这个转换函数在处理从数据库查询返回的字节数据(如文本数据)是非常有用,尤其是在需要将这些数据转换为 Windows 应用程序能够理解和处理的字符串类型时。

4-App

该示例中,App包含DbHelper类的定义文件,并声明和定义了DbHelper类的静态成员变量db,作为应用程序的全局数据库工具。

文件 App.xaml App.xaml.h App.xaml.cpp
代码
  <?xml version="1.0" encoding="utf-8"?>
<Application
    x:Class="WinUI3App.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3App"
    RequestedTheme="Light">
    <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>
  
  #pragma once

#include "App.xaml.g.h"
#include "DbHelper.h"

namespace winrt::WinUI3App::implementation
{
    struct App : AppT<App>
    {
        App();

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

    private:
        winrt::Microsoft::UI::Xaml::Window window{ nullptr };
    };
}
  
  #include "pch.h"
#include "App.xaml.h"
#include "MainWindow.xaml.h"

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::WinUI3App::implementation
{
    DbHelper App::db{};
    App::App()
    {
#if defined _DEBUG && !defined DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION
        UnhandledException([](IInspectable const&, UnhandledExceptionEventArgs const& e)
        {
            if (IsDebuggerPresent())
            {
                auto errorMessage = e.Message();
                __debugbreak();
            }
        });
#endif
    }

    void App::OnLaunched([[maybe_unused]] LaunchActivatedEventArgs const& e)
    {
        window = make<MainWindow>();
        window.Activate();
    }
}
  
🤖 代码解读

RequestedTheme="Light"设置应用程序的全局主题,指定为浅色模式。

  1. #include "DbHelper.h"为包含DbHelper类的定义文件,允许当前项目使用自定义的数据库处理类。
  2. static DbHelper db;声明了一个静态成员变量db,类型为DbHelperstatic表示db是一个全局共享的静态变量,所有App实例共享一个DbHelper实例。
  1. DbHelper App::db{};定义了DbHelper类的静态成员变量db。使用{}调用默认构造函数初始化,创建DbHelper实例。作为应用程序的全局数据库工具,用于处理数据库连接和操作。

5-MainWindow

MainWindow中嵌入MainPage页面,主要目的是为了实现职责分离和模块化设计,从而提高代码的可读性、可扩展性和可维护性。

文件 MainWindow.xaml MainWindow.xaml.h MainWindow.xaml.cpp MainWindow.idl
代码
  <?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="WinUI3App.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3App"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="WinUI3App">

    <local:MainPage/>
</Window>
  
  #pragma once

#include "MainWindow.g.h"

namespace winrt::WinUI3App::implementation
{
    struct MainWindow : MainWindowT<MainWindow>
    {
        MainWindow(){}
    };
}

namespace winrt::WinUI3App::factory_implementation
{
    struct MainWindow : MainWindowT<MainWindow, implementation::MainWindow>
    {
    };
}
  
  #include "pch.h"
#include "MainWindow.xaml.h"
#if __has_include("MainWindow.g.cpp")
#include "MainWindow.g.cpp"
#endif

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::WinUI3App::implementation
{
}
  
  namespace WinUI3App
{
    [default_interface]
    runtimeclass MainWindow : Microsoft.UI.Xaml.Window
    {
        MainWindow();
    }
}
  
🤖 代码解读

<local:MainPage/> 引用本地命名空间的MainPage页面,并将其嵌入到Window中。MainPage是另一个 XAML 文件,定义了用户界面的具体内容。

6-UserControl[UpdateUserControl]

通过WinUI3App(Desktop)上右键->Add->New Item...->User Control(WinUI3)建立UserControl(用户控件),命名为UpdateUserControl,会自动生成4个文件UpdateUserControl.xamlUpdateUserControl.idlUpdateUserControl.hUpdateUserControl.cppUserControl允许开发者将多个控件和功能组合在一起,形成一个新的复合控件,方便在项目中重复使用和维护。

文件 UpdateUserControl.xaml UpdateUserControl.xaml.h UpdateUserControl.xaml.cpp UpdateUserControl.idl
代码
  <?xml version="1.0" encoding="utf-8"?>
<UserControl
    x:Class="WinUI3App.UpdateUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3App"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <StackPanel Orientation="Vertical">
        <TextBox x:Name="firstNameBox" InputScope="NameOrPhoneNumber" Margin="0,0,0,12" Header="First name"/>
        <TextBox x:Name="lastNameBox" InputScope="NameOrPhoneNumber" Margin="0,0,0,12" Header="Last name"/>
        <TextBox x:Name="emailBox" InputScope="EmailNameOrAddress" Margin="0,0,0,12" Header="Email address"/>
        <Button x:Name="saveButton" Content="Save" Style="{StaticResource AccentButtonStyle}" HorizontalAlignment="Stretch" Click="saveButton_Click"/>
    </StackPanel>
</UserControl>
  
  #pragma once

#include "UpdateUserControl.g.h"

namespace winrt::WinUI3App::implementation
{
    struct UpdateUserControl : UpdateUserControlT<UpdateUserControl>
    {
    private:
        int32_t id;
        winrt::event < Windows::Foundation::EventHandler<int32_t>> userUpdatedEvent; // The actual event object used internally
    public:
        UpdateUserControl(){}

        void LoadUserData(int32_t _id, hstring firstName, hstring lastName, hstring email);
        winrt::event_token UserUpdated(const Windows::Foundation::EventHandler<int32_t>& handler); // Add an event handler and returns token
        void UserUpdated(const winrt::event_token& token); // Use the token to remove event handler        
        void saveButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
    };
}

namespace winrt::WinUI3App::factory_implementation
{
    struct UpdateUserControl : UpdateUserControlT<UpdateUserControl, implementation::UpdateUserControl>
    {
    };
}
  
  #include "pch.h"
#include "UpdateUserControl.xaml.h"
#if __has_include("UpdateUserControl.g.cpp")
#include "UpdateUserControl.g.cpp"
#endif
#include "App.xaml.h"

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::WinUI3App::implementation
{
	void UpdateUserControl::LoadUserData(int32_t _id, hstring firstName, hstring lastName, hstring email)
	{
		id = _id;
		firstNameBox().Text(firstName);
		lastNameBox().Text(lastName);
		emailBox().Text(email);
	}

	winrt::event_token UpdateUserControl::UserUpdated(const Windows::Foundation::EventHandler<int32_t>& handler)
	{
		// Add a new event handler (or listner)
		return userUpdatedEvent.add(handler);
	}

	void UpdateUserControl::UserUpdated(const winrt::event_token& token)
	{
		// Remove the event handler (or listner)
		userUpdatedEvent.remove(token);
	}

	void UpdateUserControl::saveButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		if (firstNameBox().Text().empty()) {
			firstNameBox().Focus(Microsoft::UI::Xaml::FocusState::Keyboard);
			return;
		}
		if (lastNameBox().Text().empty()) {
			lastNameBox().Focus(Microsoft::UI::Xaml::FocusState::Keyboard);
			return;
		}
		if (emailBox().Text().empty()) {
			emailBox().Focus(Microsoft::UI::Xaml::FocusState::Keyboard);
			return;
		}
		bool saved = App::db.updateUser(id, firstNameBox().Text(), lastNameBox().Text(), emailBox().Text());
		if (saved) {
			OutputDebugString(L"Saved!");
			firstNameBox().Text(L"");
			lastNameBox().Text(L"");
			emailBox().Text(L"");
			firstNameBox().Focus(Microsoft::UI::Xaml::FocusState::Keyboard);
		}
		userUpdatedEvent(*this, 0);
	}
}
  
  namespace WinUI3App
{
    [default_interface]
    runtimeclass UpdateUserControl : Microsoft.UI.Xaml.Controls.UserControl
    {
        UpdateUserControl();
        void LoadUserData(Int32 id, String firstName, String lastName, String email);
        event Windows.Foundation.EventHandler<Int32> UserUpdated;
    }
}
  
🤖 代码解读

这个 XAML 文件定义了一个名为UpdateUserControl的用户控件(UserControl),用于实现用户信息的更新界面。

  1. x:Class="WinUI3App.UpdateUserControl",表示这个 XAML 文件对应的后端代码所属类名。通过x:Class将前端(UI)和后端(逻辑)关联,即在前端定义 UI,在后端编写逻辑,处理用户交互,来提升代码的清晰度和模块化,支持复用和扩展性。
  2. <StackPanel Orientation="Vertical">为控件内部通过StackPanel布局,按垂直方向排列子控件。
  3. 输入框TextBox用于定义用户输入字段,包括名字firstNameBox、姓氏lastNameBox和邮箱emailBox,其中InputScope="NameOrPhoneNumber"设置输入范围,提示软键盘优化为适合输入姓名或电话号码;InputScope="EmailNameOrAddress"设置数据范围,提示软键盘优化为适合输入电子邮件地址。
  4. 定义按钮Button,设置点击事件关联到后端代码saveButton_Click事件处理函数。
  1. struct UpdateUserControl : UpdateUserControlT<UpdateUserControl>:定义了一个名为UpdateUserControl的结构体。UpdateUserControlT<UpdateUserControl>是一个模板类,通常由 XAML 编译器生成,作为自定义控件的基类。UpdateUserControlT提供了 XAML 与 C++ 后端代码的绑定机制,使得控件能够在 XAML 中被使用并自动与 C++ 后端逻辑进行交互。
  2. int32_t id;:用来存储用户的唯一标识(ID)。可以用来加载或更新特定用户的数据。
  3. winrt::event<Windows::Foundation::EventHandler<int32_t>> userUpdatedEvent; :是一个事件对象,用于内部处理用户更新事件。EventHandler<int32_t>表示事件的委托类型,事件触发时传递一个int32_t类型的参数(可能是更新后的用户 ID)。winrt::event是 WinRT 的事件类型,用于创建、处理和触发事件。
  4. UpdateUserControl(){}为默认的构造函数,初始化一个UpdateUserControl实例。目前此构造函数为空,没有进行任何额外的初始化操作。
  5. void LoadUserData(int32_t _id, hstring firstName, hstring lastName, hstring email);:该方法用于加载用户的数据,并将这些数据(如idfirstNamelastNameemail)填充到控件中。此方法通常会在控件初始化时调用,将数据展示到控件的 UI 元素中(如文本框或标签)。
  6. winrt::event_token UserUpdated(const Windows::Foundation::EventHandler<int32_t>& handler);:该方法允许其他代码为userUpdatedEvent添加处理程序(监听器)。参数handler是一个事件处理函数,用于响应userUpdatedEvent事件。当事件触发时,该处理程序会执行。返回值winrt::event_token为一个事件令牌(token),用于标识和移除事件处理程序。
  7. void UserUpdated(const winrt::event_token& token);:该方法使用event_token来移除之前添加的事件处理程序。参数token事件令牌,用来标识要移除的事件处理程序。
  8. void saveButton_Click(...);:保存按钮点击事件,用来保存用户在控件中输入的数据,并可能会触发userUpdatedEvent事件,通知其他部分的代码数据已被更新。

这个结构体UpdateUserControl实现了一个用于显示和编辑用户信息的控件,提供了如下功能,

  1. 加载用户数据:通过LoadUserData方法将用户信息填充到控件中。
  2. 事件机制:通过userUpdatedEvent事件,允许其他部分的代码订阅用户更新事件,当用户信息被更新时通知相关方。
  3. 保存用户信息:通过saveButton_Click方法处理用户点击保存按钮的行为,执行保存操作并触发事件。

此外,控件的事件处理程序可以通过UserUpdated方法添加和移除,从而提供灵活的事件订阅和取消订阅机制。这种设计符合事件驱动编程模型,并提供了很好的模块化和扩展性。

这段代码是UpdateUserControl控件的实现代码,主要用于显示和更新用户数据。包含了用户数据加载、事件处理和保存操作等功能。

  • 加载并显示用户数据:void UpdateUserControl::LoadUserData(int32_t _id, hstring firstName, hstring lastName, hstring email)
  1. id = _id,将传入的用户 ID 保存到控件的id成员变量。
  2. 分别将传入的firstNamelastNameemail显示在控件firstNameBoxlastNameBoxemailBox文本框中。
  • 添加事件处理程序:winrt::event_token UpdateUserControl::UserUpdated(const Windows::Foundation::EventHandler<int32_t>& handler)
  1. userUpdatedEvent.add(handler)调用userUpdatedEvent对象的add方法,将传入的handler(事件处理程序)注册到事件中。userUpdatedEvent是一个winrt::event类型的成员变量(事件对象),用于在 winRT 中实现事件订阅与触发机制。add方法将一个事件处理程序添加到事件的监听队列中,表示一旦事件触发,传入的handler函数就会被调用。返回值的类型是winrt::event_token,是一个唯一标识符,用于在以后移除该事件处理程序。

这个方法的作用是向userUpdatedEvent事件中添加一个新的事件处理程序handler。通过调用add(handler)handler被注册为事件的监听器,未来事件触发时,handler会被调用。返回值event_token可以用来在需要时从事件中移除该处理器。

  • 移除事件处理程序:void UpdateUserControl::UserUpdated(const winrt::event_token& token)
  1. userUpdatedEvent.remove(token);是根据提供的event_token,移除事件处理程序。
  • 按钮点击保存事件函数:void UpdateUserControl::saveButton_Click(...)
  1. if (firstNameBox().Text().empty())用于检查文本框是否为空。如果为空,则调用firstNameBox().Focus(Microsoft::UI::Xaml::FocusState::Keyboard)设置焦点到firstNameBox,并立即返回return,不继续执行后续代码。对于lastNameBoxemailBox同。
  2. bool saved = App::db.updateUser(id, firstNameBox().Text(), lastNameBox().Text(), emailBox().Text());:如果所有字段都有内容,尝试使用App::db.updateUser方法更新数据库中的用户数据。返回值saved表示更新操作是否成功。
  3. 如果savedtrue(即保存成功),则调用OutputDebugString(L"Saved!");"Saved!"消息输出到调试控制台。清空firstNameBoxlastNameBoxemailBox文本框中的内容。并将焦点设置到firstNameBox上,允许用户继续输入。
  4. userUpdatedEvent(*this, 0);用于触发事件。无论更新成功与否,都会触发userUpdatedEvent,发送事件通知给所有已注册的事件处理程序,通知其他部分用户数据已更新。(*this, 0)*this指的是当前的UpdateUserControl对象实例。事件处理程序会接收到该对象的引用,通常在事件处理程序中可以被用来获取控件的状态或执行某些操作。0是事件处理程序的第2个参数,是int32_t类型的值。这里,0表示一个简单的标识或状态,可能被用来传递某种信号,告诉事件的监听者操作完成或成功。

IDL(Interface Definition Language)文件,用于描述 Windows Runtime 组件的接口、类、事件和数据类型。这里定义了一个 Windows Runtime 类UpdateUserControl,并继承自Microsoft.UI.Xaml.Controls.UserControl,表示这是一个基于 WinUI3 的UserControl类自定义的用户控件,意味着可以像其他控件一样放置在应用程序的界面中,显示和交互。该部分提供了一个构造函数UpdateUserControl()、一个方法LoadUserData和一个事件UserUpdated

7-MainPage

嵌套于MainWindow中的MainPage包含应用程序的主要 UI 和交互逻辑。

文件 MainPage.xaml MainPage.xaml.h MainPage.xaml.cpp MainPage.idl
代码
  <?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="WinUI3App.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3App"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Loaded="Page_Loaded"
    Unloaded="Page_Unloaded">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="320"/>
        </Grid.ColumnDefinitions>

        <TextBlock x:Name="itemCountBlock" Margin="12"/>
        <Button x:Name="newUserButton" Content="New Users" HorizontalAlignment="Right" Click="newUserButton_Click"/>
        <ListView x:Name="userList" Grid.Row="1" ItemsSource="{x:Bind userArray}">
            <ListView.Resources>
                <MenuFlyout x:Key="contextMenu">
                    <MenuFlyoutItem x:Name="editButton" Icon="Edit" Text="Edit" Click="editButton_Click"/>
                    <MenuFlyoutItem x:Name="deleteButton" Icon="Delete" Text="Delete" Click="deleteButton_Click"/>
                </MenuFlyout>
            </ListView.Resources>
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="local:UserViewModel">
                    <StackPanel Orientation="Vertical" Background="Transparent" ContextFlyout="{StaticResource contextMenu}">
                        <TextBlock>
                            <Run Text="{x:Bind FirstName}"/>
                            <Run Text="{x:Bind LastName}"/>
                        </TextBlock>
                        <TextBlock Style="{StaticResource CaptionTextBlockStyle}" Text="{x:Bind Email}"/>
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

        <Border BorderBrush="LightGray" BorderThickness="1,0,0,0" Padding="8" Grid.Column="1" Grid.Row="1">
            <ContentControl x:Name="rightPanel" HorizontalContentAlignment="Stretch"/>
        </Border>
    </Grid>

</Page>
  
  #pragma once

#include "MainPage.g.h"
#include "UserViewModel.h"

namespace winrt::WinUI3App::implementation
{
    struct MainPage : MainPageT<MainPage>
    {
    private:       
        void closeDatabase();
        Windows::Foundation::Collections::IObservableVector<WinUI3App::UserViewModel> _userArray{ winrt::single_threaded_observable_vector<WinUI3App::UserViewModel>() };
        void loadAllUsers();   
        void closeRightPanel();

    public:
        MainPage(){}

        void Page_Loaded(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
        void Page_Unloaded(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);

        Windows::Foundation::Collections::IObservableVector<WinUI3App::UserViewModel> userArray();
        void newUserButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
        void editButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
        void deleteButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
    };
}

namespace winrt::WinUI3App::factory_implementation
{
    struct MainPage : MainPageT<MainPage, implementation::MainPage>
    {
    };
}
  
  #include "pch.h"
#include "MainPage.xaml.h"
#if __has_include("MainPage.g.cpp")
#include "MainPage.g.cpp"
#endif
#include <winrt/Windows.Storage.h>
#include "App.xaml.h"
#include "UpdateUserControl.xaml.h"

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::WinUI3App::implementation
{
	void MainPage::Page_Loaded(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		std::string path = winrt::to_string(Windows::Storage::ApplicationData::Current().LocalFolder().Path() + L"\\database.db");
		const char* dbFilename = path.c_str();
		App::db.openDataBase(dbFilename);
		loadAllUsers();
	}

	void MainPage::Page_Unloaded(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		closeDatabase();
	}

	void MainPage::closeDatabase()
	{
		App::db.closeDatabase();
	}

	Windows::Foundation::Collections::IObservableVector<WinUI3App::UserViewModel> MainPage::userArray()
	{
		return _userArray;
	}

	void MainPage::loadAllUsers()
	{
		_userArray.Clear();
		App::db.getAllUsers(_userArray);
		itemCountBlock().Text(winrt::to_hstring(_userArray.Size()) + L" Users");
	}

	void MainPage::closeRightPanel()
	{
		rightPanel().Content(nullptr);
	}

	void MainPage::newUserButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		WinUI3App::UpdateUserControl update = winrt::make<WinUI3App::implementation::UpdateUserControl>();
		update.UserUpdated([this](const IInspectable&, int32_t) {
			closeRightPanel();
			loadAllUsers();
			});
		rightPanel().Content(update);
	}

	void MainPage::editButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		WinUI3App::UserViewModel user = sender.try_as<Microsoft::UI::Xaml::FrameworkElement>().DataContext().try_as<WinUI3App::UserViewModel>();
		WinUI3App::UpdateUserControl update = winrt::make<WinUI3App::implementation::UpdateUserControl>();
		update.LoadUserData(user.Id(), user.FirstName(), user.LastName(), user.Email());
		update.UserUpdated([this](const IInspectable&, int32_t) {
			closeRightPanel();
			loadAllUsers();
			});
		rightPanel().Content(update);
	}

	void MainPage::deleteButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		WinUI3App::UserViewModel user = sender.try_as<Microsoft::UI::Xaml::FrameworkElement>().DataContext().try_as<WinUI3App::UserViewModel>();
		if (App::db.deleteUser(user.Id())) {
			closeRightPanel();
			loadAllUsers();
		}
	}

}
  
  import "UserViewModel.idl";
namespace WinUI3App
{
    [default_interface]
    runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
    {
        MainPage();
        Windows.Foundation.Collections.IObservableVector<UserViewModel>userArray{ get; };
    }
}
  
🤖 代码解读

MainPage页面布局包含了一个Grid,用来组织控件,并且使用了ListView来显示用户信息。

  1. Loaded="Page_Loaded"Unloaded="Page_Unloaded"将页面的加载和卸载事件处理函数与相应的事件关联,于后端代码中定义。
  2. RowDefinitions定义了网格的行,第一行Height="Auto"表示根据内容的大小自动调整;第二行Height="*"表示该行占用剩余的空间。
  3. ColumnDefinitions定义了网格的列,第一行Width="*"占用剩余空间;第二列Width="320"固定宽度为320。
  4. <TextBlock x:Name="itemCountBlock" Margin="12"/>为一个显示用户数量的文本块,设置Margin="12"来控制其与四周的间距。
  5. <Button x:Name="newUserButton" Content="New Users" HorizontalAlignment="Right" Click="newUserButton_Click"/>是一个按钮,标签为"New Users"。当点击按钮时,会触发newUserButton_Click方法。
  6. <ListView x:Name="userList" Grid.Row="1" ItemsSource="{x:Bind userArray}">是一个ListView,用于显示用户数据。其数据源是通过x:Bind绑定到userArray(后端代码变量,为容器向量/数组),意味着userArray会提供所有要显示的用户数据。Grid.Row="1"是将ListView放置在网格的第二行。
  7. ListView.Resources内的MenuFlyoutListView添加一个上下文菜单(右键点击菜单)contextMenu。在MenuFlyout中定义了两个菜单项,Edit点击后触发editButton_Click事件;Delete点击后触发deleteButton_Click事件。
  8. ListView.ItemTemplate定义了每个列表项的模板。DataTemplate x:DataType="local:UserViewModel"使用UserViewModel类型的数据定义模板。
  9. <StackPanel Orientation="Vertical" Background="Transparent" ContextFlyout="{StaticResource contextMenu}">,每个列表项是一个垂直排列的StackPanel,背景透明。通过ContextFlyout属性将之前定义的上下文菜单(contextMenu)应用到每个项。
  10. TextBlock显示用户的名字,使用Run控件将FirstNameLastName显示在一行。另一个TextBlock显示用户的电子邮件,使用CaptionTextBlockStyle样式。分别通过x:BindText属性绑定到FirstNameLastNameEmail
  11. <Border BorderBrush="LightGray" BorderThickness="1,0,0,0" Padding="8" Grid.Column="1" Grid.Row="1">是一个边框,用来包装右侧面板的内容(rightPanel)。这个面板用于增加或编辑用户信息。Grid.Column="1" Grid.Row="1"是将这个元素放置在网格的第二列的第二行。
  12. <ContentControl x:Name="rightPanel" HorizontalContentAlignment="Stretch"/>是一个ContentControl,是右侧面板的容器。rightPanel具体内容由 C++/WinRT 后代代码动态设置。
  1. #include "UserViewModel.h"引入UserViewModel类,通常用于表示页面中的数据模型。
  2. void closeDatabase();用于关闭数据库连接,清理数据库相关资源。
  3. Windows::Foundation::Collections::IObservableVector<WinUI3App::UserViewModel> _userArray{ winrt::single_threaded_observable_vector<WinUI3App::UserViewModel>() };:该行代码定义并初始化了一个IObservableVector类型的成员变量_userArray,并通过winrt::single_threaded_observable_vector对其进行初始化。Windows::Foundation::Collections::IObservableVector是 Windows Runtime 中用于表示可观察向量的数据结构,是一个动态的容器,可以存储任意类型的对象,IObservableVector<WinUI3App::UserViewModel>表示存储用户定义的数据模型类UserViewModel对象。IObservableVector实现了对数据变化的通知功能,当数据发生更改时,会自动通知 UI 更新。_userArray是该类中的一个私有成员变量,是一个IObservableVector<WinUI3App::UserViewModel>类型的容器,用于存储和管理所有的用户视图模型(UserViewModel)。_userArray用于在界面中动态显示用户数据,并且由于是一个可观察的向量,任何对它的修改都会自动通知 UI 进行更新。winrt::single_threaded_observable_vector<WinUI3App::UserViewModel>()是一个工厂函数,创建并返回一个线程安全、初始为空的IObservableVector实例,是用于生成IObservableVector对象的标准方法。
  4. void loadAllUsers(); 用于加载所有用户数据到_userArray中。
  5. void closeRightPanel();用于关闭页面上右侧面板。
  6. Page_Loaded()为页面加载时触发的事件处理函数,用于执行初始化操作。
  7. Page_Unloaded()为页面卸载时触发的事件处理函数,用于清理操作。
  8. Windows::Foundation::Collections::IObservableVector<WinUI3App::UserViewModel> userArray();返回_userArray,即用户数据集合,用于 XAML 数据绑定。
  9. newUserButton_Click()处理New Users按钮点击事件。
  10. editButton_Click()处理Edit按钮点击事件,用于编辑用户数据。
  11. deleteButton_Click()处理Delete按钮点击事件,用于删除用户。
  • 头文件的引入
  1. #include <winrt/Windows.Storage.h>包含Windows.Storage命名空间中的头文件,提供对存储(如文件系统、应用程序数据等)的访问。
  2. #include "App.xaml.h",这是应用程序主头文件,通常包含应用程序的全局初始化和设置,这里在App中包含了DbHelper类的定义文件,含有处理数据库相关操作的工具。
  3. #include "UpdateUserControl.xaml.h"是用于更新用户信息的复合控件(User Control)的头文件。
  • 页面加载事件处理程序:void MainPage::Page_Loaded(...)
  1. std::string path = winrt::to_string(Windows::Storage::ApplicationData::Current().LocalFolder().Path() + L"\\database.db");:这里std::string path = winrt::to_string(...)通过winrt::to_string将一个hstring转换为std::string类型。hstring是 WinRT 中的字符串类型,而std::string是 C++ 标准库中的字符串类型。Windows::Storage::ApplicationData::Current().LocalFolder().Path()获取应用程序本地存储文件夹的路径。LocalFolder是应用的本地文件夹,通常用于存储与应用相关的数据文件。L"\\database.db"是数据库文件名,通过加上\\database.db构建出本地存储文件夹下的数据库路径。最终,path存储的是数据库文件的完整路径。
  2. const char* dbFilename = path.c_str();是将std::string类型的path转换为 C 风格的字符串const char*类型。path.c_str()返回一个指向path内容的常量字符指针。
  3. App::db.openDataBase(dbFilename);调用App类中的静态成员db(为DbHelper类的实例)的openDataBase方法,传入数据库文件路径dbFilename,打开数据库就。
  4. loadAllUsers();MainPage类中的另一个方法,用于加载所有用户数据。调用此方法会从数据库中检索所有用户数据并将其显示在 UI 上。
  • 页面卸载事件处理程序:void MainPage::Page_Unloaded(...)
  1. closeDatabase();MainPage类中的一个方法调用,用于关闭数据库连接,其可能会调用数据库相关的清理工作,确保资源得以释放,防止资源泄漏。
  • 关闭数据库连接:void MainPage::closeDatabase()
  1. App::db.closeDatabase();调用了App类中db对象的closeDatabase方法,关闭数据库连接。
  • 数据绑定读取器:Windows::Foundation::Collections::IObservableVector<WinUI3App::UserViewModel> MainPage::userArray()
  1. return _userArray;返回_userArray变量。_userArray是一个IObservableVector<WinUI3App::UserViewModel>类型的成员变量,存储用户数据。由于IObservableVector实现了观察者模式,意味着_userArray中内容发生变化,相关的 UI 元素自动发生更新。
  • 加载所有用户数据:void MainPage::loadAllUsers()
  1. _userArray.Clear();清空了_userArray中当前存储的所有数据,以确保加载新数据时不会保留旧的数据。
  2. App::db.getAllUsers(_userArray);调用App类中的静态db成员,使用DbHelper类中的getAllUsers方法获取所有用户的数据。
  3. itemCountBlock().Text(winrt::to_hstring(_userArray.Size()) + L" Users");用于更新 UI 上显示用户数量的文本控件。itemCountBlock()为 UI 中的文本控件。_userArray.Size()是获取_userArray元素中的数量,即当前加载的用户数量。winrt::to_hstring(...)int类型的用户数量转换为hstring,WinRT 中的字符串类型。L" Users"是将字符串" Users"拼接到数字后面,用于显示用户数量和单位(如3 users)。最后调用Text()方法将更新后的文本设置为文本控件的内容,从而在 UI 上显示用户数量。
  • 关闭页面上右侧面板:void MainPage::closeRightPanel()
  1. void MainPage::closeRightPanel()Content(nullptr)ContentControl类的一个方法,用于设置该控件的内容。nullptr表示将该控件的内容清空或设置为无内容。此操作实际上是将右侧面板的所有显示内容移除,效果上相当于关闭该面板。
  • 处理New Users按钮点击事件:void MainPage::newUserButton_Click(...)
  1. WinUI3App::UpdateUserControl update = winrt::make<WinUI3App::implementation::UpdateUserControl>();创建了一个新的UpdateUserControl实例。winrt::make<...>是用于创建 WinRT 对象的 C++/WinRT 工厂方法,将返回一个新的UpdateUserControl对象,用于更新用户信息。
  update.UserUpdated([this](const IInspectable&, int32_t) {
    closeRightPanel();
    loadAllUsers();
});
  
  1. update.UserUpdated(...)UpdateUserControl控件注册了一个事件处理程序(回调函数)。当UserUpdated事件被触发时,这个回调将被调用。
  2. [this](const IInspectable&, int32_t)是一个 Lambda 表达式,表示UserUpdated事件触发时会调用 Lambda。this表示捕获MainPagethis指针,使 Lambda 可以访问MainPage的成员函数(如closeRightPanel()loadAllUsers())。(const IInspectable&, int32_t)是事件处理函数的参数类型,IInspectable是基础接口类型,int32_t可能表示用户更新的信息或其他信息。在 Lambda 内部调用了两个方法,closeRightPanel()关闭右侧面板;和loadAllUsers()重新加载用户列表。
  3. rightPanel().Content(update);将创建的UpdateUserControl控件设置为右侧面板的内容。rightPanel()是一个方法,返回一个 UI 控件(如ContentControl)。通过设置Content属性为update,即将UpdateUserControl控件显示在右侧面板上。
  • 编辑用户数据:void MainPage::editButton_Click(...)
  1. WinUI3App::UserViewModel user = sender.try_as<Microsoft::UI::Xaml::FrameworkElement>().DataContext().try_as<WinUI3App::UserViewModel>();sender.try_as<Microsoft::UI::Xaml::FrameworkElement>()是触发事件的控件,可能是一个按钮。通过try_as<FrameworkElement>()将其转换为FrameworkElement类型,以便访问DataContext属性(这是绑定到控件的数据)。DataContext是绑定到控件的数据,通常是一个UserViewModel类型的对象。这里DataContext()返回与按钮控件绑定的用户数据。try_as<WinUI3App::UserViewModel>()使用try_as方法将DataContext转换为UserViewModel类型。如果转换成功,user将持有绑定的UserViewModel对象,表示该用户的数据。
  2. WinUI3App::UpdateUserControl update = winrt::make<WinUI3App::implementation::UpdateUserControl>();winrt::make<...>创建一个新的UpdateUserControl实例。这个控件将用于显示用户信息并允许编辑。
  3. update.LoadUserData(user.Id(), user.FirstName(), user.LastName(), user.Email());方法将用户的现有数据加载到UpdateUserControl控件中。传入的参数包括用户的ID、名字、姓氏和电子邮件地址。这些数据将填充控件中的相应字段,以便用户可以编辑。
  4. update.UserUpdated(...);rightPanel().Content(update);的解释同void MainPage::newUserButton_Click(...)
  • 删除用户:void MainPage::deleteButton_Click(...)
  1. user解释同void MainPage::editButton_Click(...)
  2. App::db.deleteUser(user.Id())调用数据库的删除方法,删除对应 ID 的用户。user.Id()获取用户的唯一标识符 ID。如果删除操作成功,deleteUser()方法返回true,否则返回false
  3. 如果删除成功,则依次调用closeRightPanel();loadAllUsers();,关闭右侧面板和刷新用户列表。

SQLite 数据库保存在了类似C:\Users\richie\AppData\Local\Packages\300da4f9-d702-4a8e-b3e0-b320da8596e0_5zfe55br6hb1t\LocalState\database.db的文件路径下。可以用DB Browser for SQLite辅助查看数据库中存储的数据内容。

PYC icon

图 10-14 用 DB Browser for SQLite 查看数据库

10.3.6 UI 动画

该示例应用界面显示了一个联系人列表,包括姓名和国家。并配置了鼠标指针进入和离开联系人项上时的交互动画效果(放大等),使得 UI 更加动态,互动性增强。

PYC icon

图 10-15 动画效果

1-App

在创建动画和视觉效果时,Compositor(位于winrt::Microsoft::UI::Xaml::Window类型下)提供了许多方法来创建和管理动画,通过static winrt::Microsoft::UI::Xaml::Window window;(App.xaml.h)和winrt::Microsoft::UI::Xaml::Window App::window{ nullptr };(App.xaml.cpp)将window配置为静态成员,可以轻松的在应用的任何地方访问窗口的Compositor,进而创建和管理动画效果。

文件 App.xaml App.xaml.h App.xaml.cpp
代码
  <?xml version="1.0" encoding="utf-8"?>
<Application
    x:Class="WinUI3Experiment.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3Experiment"
    RequestedTheme="Light">
    <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>
  
  #pragma once

#include "App.xaml.g.h"

namespace winrt::WinUI3Experiment::implementation
{
    struct App : AppT<App>
    {
        App();

        void OnLaunched(Microsoft::UI::Xaml::LaunchActivatedEventArgs const&);
           
        static winrt::Microsoft::UI::Xaml::Window window;
    };
}
  
  #include "pch.h"
#include "App.xaml.h"
#include "MainWindow.xaml.h"

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::WinUI3Experiment::implementation
{
    winrt::Microsoft::UI::Xaml::Window App::window{ nullptr };
    App::App()
    {
#if defined _DEBUG && !defined DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION
        UnhandledException([](IInspectable const&, UnhandledExceptionEventArgs const& e)
        {
            if (IsDebuggerPresent())
            {
                auto errorMessage = e.Message();
                __debugbreak();
            }
        });
#endif
    }
    void App::OnLaunched([[maybe_unused]] LaunchActivatedEventArgs const& e)
    {
        window = make<MainWindow>();
        window.Activate();
    }
}
  
🤖 代码解读

RequestedTheme="Light"用于指定页面外观主题为Light浅色主题(还有Dark深色等主题)。

static winrt::Microsoft::UI::Xaml::Window window;声明了一个静态成员变量window,类型是 WinUI3 中的Window类,表示应用的主窗口。static关键字声明成员变量属于类本身,在项目中的其它页面代码文件中可以通过#include "App.xaml.h",例如使用animation = App::window.Compositor().CreateSpringVector3Animation();来调用winrt::Microsoft::UI::Xaml::Window提供的一些内置方法。

winrt::Microsoft::UI::Xaml::Window App::window{ nullptr };初始化了App类的静态成员变量window,类型为winrt::Microsoft::UI::Xaml::Window{ nullptr }初始化列表,将window初始值为nullptr,意味着在类的实例化之前,window的默认值为空(指针未指向任何有效的窗口对象)。这种初始化通常用于确保成员变量在类的构造过程中有一个明确的初始值,防止其在未初始化的情况下被访问。

2-ContactViewModel

ContactViewModel类用于表示和管理一个联系人的信息,包括名称(Name)和国家(Country);并实现了获取联系人数据的属性(NameCountry),提供了设置这些数据的一个方法SetContactDataContactViewModel类是典型的视图模型(ViewModel)类,通过数据绑定与 XAML 视图交互,更新和显示数据。

文件 ContactViewModel.idl ContactViewModel.h ContactViewModel.cpp
代码
  namespace WinUI3Experiment
{
    [bindable]
    [default_interface]
    runtimeclass ContactViewModel 
    {
        ContactViewModel();
        String Name{ get; };
        String Country{ get; };
        void SetContactData(String _name, String _country);
    }
}
  
  #pragma once

#include "ContactViewModel.g.h"

namespace winrt::WinUI3Experiment::implementation
{
    struct ContactViewModel : ContactViewModelT<ContactViewModel>
    {
    private:
        winrt::hstring name;
        winrt::hstring country;
    public:
        ContactViewModel() = default;

        winrt::hstring Name();
        winrt::hstring Country();
        void SetContactData(winrt::hstring, winrt::hstring);

    };
}

namespace winrt::WinUI3Experiment::factory_implementation
{
    struct ContactViewModel : ContactViewModelT<ContactViewModel, implementation::ContactViewModel>
    {
    };
}
  
  #include "pch.h"
#include "ContactViewModel.h"
#if __has_include("ContactViewModel.g.cpp")
#include "ContactViewModel.g.cpp"
#endif

namespace winrt::WinUI3Experiment::implementation
{
	winrt::hstring ContactViewModel::Name() {
		return name;
	}

	winrt::hstring ContactViewModel::Country() {
		return country;
	}

	void ContactViewModel::SetContactData(winrt::hstring _name, winrt::hstring _country) {
		name = _name;
		country = _country;
	}
}
  
🤖 代码解读

该 IDL 文件定义了ContactViewModel类的接口,包含一个构造函数ContactViewModel(),用于创建ContactViewModel类的实例。两个只读属性NameCountry,分别返回联系人的姓名和国家。一个方法SetContactData,用于设置NameCountry的值。这个类的设计符合 MVVM 模式的要求,将 UI 和数据分离,提供了绑定和数据更新的接口。[bindable]属性让类与 XAML 数据绑定系统兼容,从而可以将其用于显示和操作 UI 中的联系人数据。

  1. winrt::hstring name;winrt::hstring country;声明了两个私有成员变量,namecountry,类型为winrt::hstring,用于存储联系人的姓名和国家信息。
  2. ContactViewModel() = default;为默认构造函数,使用= default表示编译器会为ContactViewModel类自动生成一个默认构造函数。该构造函数不会执行任何特殊初始化。
  3. winrt::hstring Name();winrt::hstring Country();是公有方法,分别返回私有变量namecountry的值,为成员变量的getter方法,用于访问联系人的姓名和国家信息。
  4. void SetContactData(winrt::hstring, winrt::hstring);用于设置联系人的数据,接受两个winrt::hstrin类型的参数,分别用于设置namecountry。此方法可以通过视图模型更新联系人信息。

这段代码是ContactViewModel类的实现部分,定义了该类中声明的成员函数。Name()Country()方法是ContactViewModel类的getter方法,分别用于获取联系人的姓名和国家信息。SetContactData()方法用于设置联系人信息,通过传入新的名称和国家来更新namecountry成员变量。通过这些方法,ContactViewModel类实现了对联系人数据的访问和更新,这符合 MVVM 模式中的视图模型(ViewModel)职责,即将 UI 和底层数据模型之间的逻辑封装起来,使得视图(View)可以通过数据绑定等方式访问这些数据。

3-MainWindow

MainPage嵌套到MainWindow中,让界面逻辑和窗口设置分离,便于管理。删除自动生成的按钮和属性等代码行,在MainWindow.xaml中增加一行<local:MainPage/>代码,实现窗口嵌套。

4-MainPage

用于应用程序的主要 UI 和交互逻辑,为窗口的内容部分,定义具体的用户界面。

文件 MainPage.xaml MainPage.xaml.h MainPage.xaml.cpp MainPage.idl
代码
  <?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="WinUI3Experiment.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3Experiment"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Loaded="Page_Loaded">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="400"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Border BorderBrush="{ThemeResource SystemListLowColor}" BorderThickness="0,0,1,0" Padding="20">
            <ListView x:Name="list">
                <ListView.ItemTemplate>
                    <DataTemplate x:DataType="local:ContactViewModel">
                        <Grid Padding="8" Background="Transparent" PointerEntered="Grid_PointerEntered" PointerExited="Grid_PointerExited">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
                            <FontIcon Glyph="&#xE13D;"/>
                            <TextBlock Text="{x:Bind Name}" Grid.Column="1" Margin="12,0,0,0"/>
                            <TextBlock Text="{x:Bind Country}" Grid.Column="2"/>
                        </Grid>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </Border>      
    </Grid>    
</Page>
  
  #pragma once

#include "MainPage.g.h"
#include "ContactViewModel.h"

namespace winrt::WinUI3Experiment::implementation
{
    struct MainPage : MainPageT<MainPage>
    {
    private:
        Windows::Foundation::Collections::IObservableVector< WinUI3Experiment::ContactViewModel> contactArray{ winrt::single_threaded_observable_vector<WinUI3Experiment::ContactViewModel>()};
        void prepareContactData();
        Microsoft::UI::Composition::SpringVector3NaturalMotionAnimation animation{ nullptr };
        void initSpringAnimation();
        void startAnimation(Microsoft::UI::Xaml::Controls::Grid grid, float scale);

    public:
        MainPage(){}

        void Page_Loaded(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
        void Grid_PointerEntered(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& e);
        void Grid_PointerExited(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& e);
    };
}

namespace winrt::WinUI3Experiment::factory_implementation
{
    struct MainPage : MainPageT<MainPage, implementation::MainPage>
    {
    };
}
  
  #include "pch.h"
#include "MainPage.xaml.h"
#if __has_include("MainPage.g.cpp")
#include "MainPage.g.cpp"
#endif
#include "App.xaml.h"

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::WinUI3Experiment::implementation
{
	void MainPage::Page_Loaded(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		list().ItemsSource(contactArray);
		prepareContactData();
		initSpringAnimation();
	}

	void MainPage::prepareContactData() {
		WinUI3Experiment::ContactViewModel contact = winrt::make<WinUI3Experiment::implementation::ContactViewModel>();
		contact.SetContactData(L"Liam O'Connor", L"Ireland");
		contactArray.Append(contact);

		contact = winrt::make<WinUI3Experiment::implementation::ContactViewModel>();
		contact.SetContactData(L"Maria Garcia", L"Spain");
		contactArray.Append(contact);

		contact = winrt::make<WinUI3Experiment::implementation::ContactViewModel>();
		contact.SetContactData(L"Yuki Takahashi", L"Japan");
		contactArray.Append(contact);

		contact = winrt::make<WinUI3Experiment::implementation::ContactViewModel>();
		contact.SetContactData(L"Sofia Petrova", L"Russia");
		contactArray.Append(contact);

		contact = winrt::make<WinUI3Experiment::implementation::ContactViewModel>();
		contact.SetContactData(L"David Johnson", L"United States");
		contactArray.Append(contact);

		contact = winrt::make<WinUI3Experiment::implementation::ContactViewModel>();
		contact.SetContactData(L"Marco Rossi", L"Italy");
		contactArray.Append(contact);

		contact = winrt::make<WinUI3Experiment::implementation::ContactViewModel>();
		contact.SetContactData(L"Juan Rodríguez", L"Mexico");
		contactArray.Append(contact);
	}

	void MainPage::initSpringAnimation()
	{
		animation = App::window.Compositor().CreateSpringVector3Animation();
		animation.Target(L"Scale");

	}

	void MainPage::startAnimation(Microsoft::UI::Xaml::Controls::Grid grid, float scale)
	{
		animation.FinalValue(Windows::Foundation::Numerics::float3{ scale });
		grid.CenterPoint(Windows::Foundation::Numerics::float3{ (grid.ActualSize().x / 2.f),(grid.ActualSize().y / 2.f), 1.f });
		grid.StartAnimation(animation);
	}

	void MainPage::Grid_PointerEntered(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& e)
	{
		startAnimation(sender.try_as<Microsoft::UI::Xaml::Controls::Grid>(), 1.1f);
	}


	void MainPage::Grid_PointerExited(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& e)
	{
		startAnimation(sender.try_as<Microsoft::UI::Xaml::Controls::Grid>(), 1.0f);
	}
}
  
  namespace WinUI3Experiment
{
    [default_interface]
    runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
    {
        MainPage();
    }
}
  
🤖 代码解读

这段 XAML 代码定义了一个 MainPage页面,页面中有一个ListView控件来显示联系人列表。每个联系人数据通过ContactViewModel绑定到NameCountry属性,并通过DataTemplate定义了显示方式。页面还为Grid元素添加了鼠标事件处理函数,能够响应鼠标进入(PointerEntered="Grid_PointerEntered")和退出(PointerExited="Grid_PointerExited")的交互操作。

  1. #include "ContactViewModel.h"引入视图模型ContactViewModel
  2. contactArray声明了一个IObservableVector类型的变量,是一个可以通知绑定 UI 列表项变更的动态数组,用于存储ContactViewModel类型的数据。inrt::single_threaded_observable_vector支持单线程访问和更新。
  3. prepareContactData用于准备和初始化联系人数据。
  4. animation声明了一个SpringVector3NaturalMotionAnimation类型的动画对象,用于创建控件的动画效果,此例中为缩放。
  5. initSpringAnimation方法用于初始化动画效果。
  6. startAnimation用于启动动画,接受两个参数,一个为gridGrid控件),指示动画作用的 UI 元素;scale表示动画的缩放比例。
  7. Page_Loaded处理页面加载事件的函数,在页面加载完成后执行,用于初始化页面中的数据或控件。
  8. Grid_PointerEnteredGrid_PointerExited分别处理Grid控件上鼠标进入和离开事件。在鼠标指针进入/离开指定的Grid区域时被触发,用于动画交互效果和恢复。
  1. #include "App.xaml.h",引入App.xaml.h头文件,用于访问应用级别的资源或窗口对象。
  • 页面加载事件:void MainPage::Page_Loaded(...)
  1. list().ItemsSource(contactArray)contactArray(一个存储联系人信息的IObservableVector)绑定到页面中的ListView控件。这样ListView控件会显示contactArray中的内容。
  2. prepareContactData()调用prepareContactData方法,初始化联系人数据。
  3. initSpringAnimation()调用initSpringAnimation方法,初始化动画。
  • 准备和初始化联系人数据:void MainPage::prepareContactData()

该方法创建了多个ContactViewModel实例,每个实例代表一个联系人,并将其添加到contactArray中。

  1. WinUI3Experiment::ContactViewModel contact = winrt::make<WinUI3Experiment::implementation::ContactViewModel>();创建了ContactViewModel实例。
  2. contact.SetContactData(L"Liam O'Connor", L"Ireland");设置每个联系人的姓名和国家信息。
  3. contactArray.Append(contact);将每个联系人对象添加到contactArray中。
  • 初始化动画效果:void MainPage::initSpringAnimation()
  1. animation = App::window.Compositor().CreateSpringVector3Animation();创建了一个“弹簧”动画(SpringVector3Animation),该动画应用了Vector3(三维向量),通常用于实现平滑、自然的过度动画。
  2. animation.Target(L"Scale")设置动画目标的属性为"Scale",即在动画过程中,目标元素的缩放(scale)将发生变化。
  • 启动动画:void MainPage::startAnimation(Microsoft::UI::Xaml::Controls::Grid grid, float scale)
  1. animation.FinalValue(Windows::Foundation::Numerics::float3{ scale })设置动画的最终值。scale表示缩放的目标值。
  2. grid.CenterPoint(Windows::Foundation::Numerics::float3{ (grid.ActualSize().x / 2.f),(grid.ActualSize().y / 2.f), 1.f });设置Grid控件的动画中心点为Grid的中心位置。grid.ActualSize()获取控件的实际大小,grid.ActualSize().x / 2.fgrid.ActualSize().y / 2.f用于计算控件的中心位置。
  3. grid.StartAnimation(animation)启动动画,应用于Grid控件。
  • Grid控件上鼠标进入和离开事件:void MainPage::Grid_PointerEntered(...)void MainPage::Grid_PointerExited(...)
  1. Grid_PointerEntered当鼠标指针进入Grid控件时,调用startAnimation函数并将缩放比例设置为1.1f(放大10%)。
  2. Grid_PointerExited当鼠标指针离开Grid控件时,调用startAnimation函数并将缩放比例设置回1.0f(恢复原始大小)。

10.3.7 网络

该示例是点击按钮Request,向指定的网址(API)发起一个网络请求,获取 JSON 格式的数据,解析并显示在应用界面上。

PYC icon

图 10-16 网络请求

1-MainPage

文件 MainPage.xaml MainPage.xaml.h MainPage.xaml.cpp MainPage.idl
代码
  <?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="WinUI3Experiment.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3Experiment"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <StackPanel Orientation="Horizontal" Margin="40">
        <Button x:Name="requestButton" Content="Request" VerticalAlignment="Top" Click="requestButton_Click"/>
        <ProgressRing x:Name="pRing" Width="20" Height="20" IsActive="False" VerticalAlignment="Top" Margin="4"/>
        <Border BorderBrush="Gray" Padding="8" BorderThickness="1" Margin="20,0,0,0" VerticalAlignment="Top" Width="480" CornerRadius="4">
            <TextBlock x:Name="responseBlock" Style="{StaticResource SubtitleTextBlockStyle}" TextWrapping="Wrap"/>
        </Border>
    </StackPanel>  
</Page>
  
  #pragma once

#include "MainPage.g.h"

namespace winrt::WinUI3Experiment::implementation
{
    struct MainPage : MainPageT<MainPage>
    {
    private:
        Windows::Foundation::IAsyncAction callEndpoint();
        void parseJoke(winrt::hstring json);

    public:
        MainPage(){}

        void requestButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
    };
}

namespace winrt::WinUI3Experiment::factory_implementation
{
    struct MainPage : MainPageT<MainPage, implementation::MainPage>
    {
    };
}
  
  #include "pch.h"
#include "MainPage.xaml.h"
#if __has_include("MainPage.g.cpp")
#include "MainPage.g.cpp"
#endif
#include <sstream>

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::WinUI3Experiment::implementation
{
	void MainPage::requestButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e)
	{
		callEndpoint();
	}

	Windows::Foundation::IAsyncAction MainPage::callEndpoint()
	{
		pRing().IsActive(true);
		Windows::Foundation::Uri url{ L"https://official-joke-api.appspot.com/random_joke" };
		Windows::Web::Http::Filters::HttpBaseProtocolFilter filter{};
		filter.CacheControl().ReadBehavior(Windows::Web::Http::Filters::HttpCacheReadBehavior::MostRecent);
		Windows::Web::Http::HttpClient client{ filter };

		try {
			auto response{ co_await client.GetAsync(url) };
			if (response.IsSuccessStatusCode()) {
				winrt::hstring content = co_await response.Content().ReadAsStringAsync();
				parseJoke(content);
			}
		}
		catch (winrt::hresult_error& ex) {
			responseBlock().Text(ex.message().c_str());
		}
		client.Close();
		pRing().IsActive(false);
		co_return;
	}

	void MainPage::parseJoke(winrt::hstring json)
	{
		//OutputDebugString(json.c_str());
		//OutputDebugString(L"\r\n");
		Windows::Data::Json::JsonObject obj{ Windows::Data::Json::JsonObject::Parse(json) };
		std::wostringstream joke;
		joke << obj.GetNamedString(L"setup");
		joke << "\r\n\r\n";
		joke << obj.GetNamedString(L"punchline");
		responseBlock().Text(joke.str().c_str());
	}
}
  
  namespace WinUI3Experiment
{
    [default_interface]
    runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
    {
        MainPage();
    }
}
  
🤖 代码解读

这个页面包含一个水平排列的布局(StackPanel),其中包括:一个按钮(Button),用于触发请求操作;一个进度环(ProgressRing),用于表示加载或等待状态;一个边框包裹的文本框(TextBlock),用于显示响应内容。页面通过requestButton_Click事件处理程序控制按钮点击行为,显示进度环并更新文本框内容。

  1. <ProgressRing x:Name="pRing" Width="20" Height="20" IsActive="False" VerticalAlignment="Top" Margin="4"/>是一个表示加载过程的控件,通常用于显示等待状态。IsActive="False"用于设置进度环的激活状态为False,即进度环目前时关闭状态(不显示动画)。
  2. <Border BorderBrush="Gray" Padding="8" BorderThickness="1" Margin="20,0,0,0" VerticalAlignment="Top" Width="480" CornerRadius="4">是一个具有边框的容器控件,通常用于包裹其它控件。
  1. callEndpoint()是一个私有成员函数,返回类型是Windows::Foundation::IAsyncAction,表示异步操作。函数的作用是发起一个网络请求,调用外部的 API 来获取数据。异步操作意味着这个方法会在后台执行,不会阻塞 UI 线程。
  2. parseJoke(winrt::hstring json),接受一个hstring类型的 JSON 字符串,用于解析 API 返回的笑话数据。提取笑话的内容,并将其展示在页面上。
  3. void requestButton_Click(...)是按钮点击事件的处理程序,调用callEndpoint()方法,发起网络请求。

这段代码是MainPage的实现部分,负责处理页面上的按钮点击事件、发起 HTTP 请求、解析 JSON 响应并更新 UI。

  • 包含头文件
  1. #include <sstream>:包含 C++ 标准库中的字符串流库,通常用于格式化和拼接字符串。
  • 按钮点击事件:void MainPage::requestButton_Click(...)
  1. 当按钮被点击时,调用callEndpoint();方法来发起 HTTP 请求并获取数据。
  • 发起一个网络请求:Windows::Foundation::IAsyncAction MainPage::callEndpoint()
  1. pRing().IsActive(true):显示进度环(ProgressRing),表示正在加载中。
  2. Windows::Foundation::Uri url{ L"https://official-joke-api.appspot.com/random_joke" };:创建一个Uri对象,表示 API 的 URL(这里是一个随机笑话 API)。
  3. Windows::Web::Http::Filters::HttpBaseProtocolFilter filter{};:创建一个 HTTP 过滤器对象,配置缓存行为,设置为读取最近的缓存数据filter.CacheControl().ReadBehavior(Windows::Web::Http::Filters::HttpCacheReadBehavior::MostRecent);
  4. Windows::Web::Http::HttpClient client{ filter };:创建一个 HTTP 客户端对象,使用前面创建的过滤器进行请求。

try

  1. try异常处理块用来捕获可能在执行GetAsync或其它操作时发生的异常(例如网络问题、解析错误等)。
  2. auto response{ co_await client.GetAsync(url) };client.GetAsync(url)使用HttpClient对象发起一个 HTTP GET 请求到指定的 URL(即url)。client是一个HttpClient实例,而url是定义的网址。co_await关键字使得当前函数暂停,直到GetAsync(url)返回响应。这个操作是非阻塞的,意味着其不会冻结主线程(UI 线程),而是在后台异步等待响应完成。responseHttpResponseMessage类型的对象,包含了 HTTP 请求的响应内容(包括状态码(StatusCode)、响应体(Content)等)。
  3. if (response.IsSuccessStatusCode())IsSuccessStatusCode()检查响应的状态是否成功。状态码在 200 到 209 范围内表示成功(例如 200 表示 OK,201 表示已创建等)。如果响应成功,继续执行后面的操作。
  4. winrt::hstring content = co_await response.Content().ReadAsStringAsync();:response.Content()获取响应体内容,返回一个HttpContent对象。ReadAsStringAsync()异步读取响应体内容,并返回字符串数据。这个操作同样使用co_await来等待异步读取完成,读取的内容是一个hstring的字符串,并存储到content变量中(JSON 字符串)。
  5. parseJoke(content);调用parseJoke()函数来解析获取到的 JSON 数据,提取文本并展示到界面上。

异常捕获

如果try块中的代码执行过程中发生了错误(例如网络连接失败、服务器错误等),则会跳转到这里处理异常。

  1. catch (winrt::hresult_error& ex)winrt::hresult_error是 C++/WinRT 中的异常类型,封装了错误信息。
  2. responseBlock().Text(ex.message().c_str());ex.message()调用异常对象exmessage()方法来获取错误的详细信息(通常是一个描述错误的字符串)。ex.message().c_str()将错误消息转换为 C 风格字符串(const char*),以便传递给TextBlock控件responseBlock(),将捕获到的错误消息显示在 UI 中。

清理操作

  1. client.Close();:显式关闭HttpClient对象,释放网络资源。虽然HttpClient在 C++/WinRT 中是智能指针形式(会自动管理资源),但显式关闭可以帮助更早地释放网络连接等资源,尤其在执行完 HTTP 请求后。
  2. pRing().IsActive(false);pRing()指向 UI 上的进度环控件(ProgressRing)。IsActive(false)将进度环的IsActive属性设置为false,表示加载完成,停止显示进度环。这是清理操作的一部分,确保用户看到请求的结束状态。
  3. co_return;为结束当前的异步操作,表示协程的结束,相当于异步方法的返回。
  • JSON 格式数据解析:void MainPage::parseJoke(winrt::hstring json)
  1. Windows::Data::Json::JsonObject obj{ Windows::Data::Json::JsonObject::Parse(json) };JsonObject::Parse(json)是 C++/WinRT 提供的一个静态方法,用于将传入的 JSON 字符串json(例如,{"type":"general","setup":"Why did the programmer bring a broom to work?","punchline":"To clean up all the bugs.","id":113})解析为一个JsonObject对象。JsonObject是一个键值对集合,可以用来访问 JSON 数据中的各个字段。解析后的 JSON 对象被存储在变量obj中,obj可以用于获取 JSON 对象中的具体数据(如setuppunchline)。
  2. std::wostringstream joke;:创建一个宽字符输出字符串流joke,用于将objsetuppunchline 两部分拼接成一个完整的字符串。wostringstream是 C++ 标准库中的一个输出流类,专门处理宽字符(wchar_t类型的字符)。
  3. joke << obj.GetNamedString(L"setup");obj.GetNamedString(L"setup")从解析得到的 JSON 对象obj中获取名为setup的字符串字段。GetNamedString方法用于获取指定键名对应的字符串值。setup字段是笑话(从网页检索的数据)的前半部分,将被追加到joke字符串流中。
  4. joke << "\r\n\r\n";:添加两个换行符,用于在setup部分和punchline部分之间插入适当的间隔。"\r\n"是回车和换行符,通常用于表示新的一行。
  5. joke << obj.GetNamedString(L"punchline");:从 JSON 对象中获取名为"punchline"的字符串字段。"punchline"字段是笑话的结尾部分,同样被追加到joke字符串流中。
  6. responseBlock().Text(joke.str().c_str());:将拼接好的完整文本显示在 UI 上的TextBlock控件中。responseBlock()返回TextBlock控件的引用,用于在界面上显示文本。joke.str()调用wostringstreamstr()方法获取当前字符串流中的内容,即拼接后的文本字符串,并用c_str()将其转换为 C 风格字符串(const wchr_t*),传递给TextBlock控件的Text()方法。

10.4 C# + WinUI3

10.4.1 默认应用(C#)代码解读和 Hello world!

10.4.1.1 默认应用(C#)代码解读

打开 VS->Create a new porject时,选择语言类型(languages)为 C#,选择的项目类型为WinUI,在列出的项目模板中选择Blank App, Packaged(WinUI 3 in Desktop)[C#、XAML、Windows、Desktop、WinUI],建立项目,默认的应用界面如图 10-17,与前文建立的 C++ 默认应用结果相同。

PYC icon

图 10-17 C# 默认应用示例

与使用 C++/WinRT 的 WinUI 3 项目相比(图10-18),使用 C# 的 WinUI 3 项目的文件结构要简洁。对于一个页面(窗口),如MainWindow, C++/WinRT 包含有 4 个文件,为一个标记文件MainWindow.xaml和一个 C++/WinRT 代码文件MainWindow.xaml.cpp,及其对应的头文件MainWindow.xaml.h,及MainWindow.idl IDL 文件;而 C# 只包含 2 个文件,为一个标记文件MainWindow.xaml,和一个 C# 代码文件MainWindow.xaml.cs

PYC icon

图 10-18 C# 和 C++/WinRT 默认应用项目文件结构

在 WinUI3 中,AppMainWindow是两个核心类,分布负责应用程序的生命周期管理和主窗口的界面呈现。

1-App

App类是整个应用程序的入口点,继承自Microsoft.UI.Xaml.Application类,主要作用包括:生命周期管理(处理应用程序的启动、挂起、恢复等事件(如OnLaunched);在应用启动时创建主窗口并启动用户界面),初始化(加载应用程序的资源(如 XAML 资源字典);执行全局的配置和初始化代码),全局对象管理(提供一个应用级别的上下文,允许共享全局状态和数据)。

文件 App.xaml App.xaml.cs

代码

  <?xml version="1.0" encoding="utf-8"?>
<Application
    x:Class="WinUI3CSharp.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3CSharp">
    <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>
  
  using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Microsoft.UI.Xaml.Shapes;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Foundation.Collections;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace WinUI3CSharp
{
    /// <summary>
    /// Provides application-specific behavior to supplement the default Application class.
    /// </summary>
    public partial class App : Application
    {
        /// <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();
        }

        /// <summary>
        /// Invoked when the application is launched.
        /// </summary>
        /// <param name="args">Details about the launch request and process.</param>
        protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
        {
            m_window = new MainWindow();
            m_window.Activate();
        }

        private Window? m_window;
    }
}
  

🤖 代码解读

App.xaml 文件与 C++/WinRT 中定义的 App.xaml 文件代码相同,此处不再赘述。

  • using声明部分
  1. using System;:C# 的核心库,提供常用功能。
  2. using System.Collections.Generic;:泛型集合类(如List<T>Dictionary<TKey, TValue>等)的支持。
  3. using System.IO;:输入输出操作,例如文件处理。
  4. using System.Linq;:LINQ 查询操作的支持。
  5. Microsoft.UI.Xaml 和相关命名空间:包含 WinUI3 中用于界面、控件、数据绑定、输入、导航等功能的类。Xaml是 Windows UI 的标记语言,通常与 C# 一起使用来构建 UI。
  6. Windows.ApplicationModelusing Windows.ApplicationModel.Activation;:处理应用程序生命周期(如启动和激活)以及应用的基本配置。
  7. using Windows.Foundation;using Windows.Foundation.Collections;提供 Windows 平台的基础 API 和集合支持。
  • 命名空间
  1. namespace WinUI3CSharp:声明了一个命名空间,命名空间是用来组织代码的逻辑单元。WinUI3CSharp是该应用程序的命名空间。
  • 类定义部分
  1. public partial class App : Applicationpublic表示该类可以被应用程序中的其它部分访问。partial是 C# 中的一个修饰符,表示类的实现可能分布在多个文件中。对于 WinUI 应用程序,partial允许将 XAML (界面)和 C# 代码分开。class App定义了一个App类,这是应用程序的主类,通常用于处理应用程序生命周期(如启动、推出)。: Application表示App类继承自Application类,继承意味着App类可以访问Application类的功能。
  • 构造函数
  1. public App()App类的构造函数,应用程序初始化时将被调用。
  2. this.InitializeComponent();:是 WinUI 生成的代码,用于初始化 XAML 文件中定义的所有控件和界面元素。通常在构造函数中被调用来确保 UI 元素被正确加载和绑定。
  • OnLaunched方法
  1. protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args):该方法是重写自Application类的虚方法OnLaunched,在应用程序启动时被调用。LaunchActivatedEventArgs参数提供了关于启动事件的详细信息。
  2. m_window = new MainWindow();:创建了一个MainWindow实例。MainWindow是应用程序的主窗口,通常包含应用的 UI。
  3. m_window.Activate();:激活主窗口,使其成为当前活动窗口并显示出来。调用这个方法会启动窗口并显示 UI。

private Window? m_window;:是一个私有字段m_window,类型为Window,用来保存应用程序的主窗口。Window?中的问号表示该字段是可空的,即它可以为null

这段代码实现了一个 WinUI3 应用程序的基本框架。从Application类继承并重写了OnLaunched方法,用于在应用程序启动时创建并激活主窗口。在构造函数中,InitializeComponent用于初始化 XAML 文件中定义的 UI 元素。整体结构是一个标准的 WinUI3 应用程序启动和初始化流程。

2-MainWindow

MainWindow是应用程序的主窗口,继承自Microsoft.UI.Xaml.Window,主要作用包括:用户界面承载(显示应用程序的主界面,通常由一个 XAML 文件定义其布局;包含用户控件、按钮、文本框等元素,用于实现交互功能),事件处理(定义界面控件的交互逻辑(如按钮点击事件、输入事件)),窗口管理(设置窗口的标题、大小、布局等属性;可以在窗口内导航到其他页面或管理多个子窗口)。

文件 MainWindow.xaml MainWindow.xaml.cs

代码

  <?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="WinUI3CSharp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3CSharp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="WinUI3CSharp">

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
    </StackPanel>
</Window>
  
  using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace WinUI3CSharp
{
    /// <summary>
    /// An empty window that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
        }

        private void myButton_Click(object sender, RoutedEventArgs e)
        {
            myButton.Content = "Clicked";
        }
    }
}
  

🤖 代码解读

MainWindow.xaml 文件与 C++/WinRT 中定义的 MainWindow.xaml 文件代码基本相同,此处不再赘述。

  • 命名空间
  1. namespace WinUI3CSharp :定义了应用程序的命名空间WinUI3CSharp,便于组织代码和避免命名冲突。
  • MainWindow类定义
  1. public sealedsealed表示该类不能被继承,通常在特定上下文中使用,例如窗口类。
  2. partial指定类的部分实现可能在其它文件中(例如 XAML 文件)。WinUI3 使用这种方式将 XAML 与 C# 代码分离。
  3. class MainWindow : Window定义MainWindow,继承自Window类,表示这是应用程序的主窗口。
  • 构造函数
  1. public MainWindow()MainWindow类的构造函数。当窗口被创建时,自动调用此方法。
  2. this.InitializeComponent();调用生成的代码,初始化与MainWindow.xaml文件相关的 UI 元素和控件。
  • myButton_Click事件处理器(方法)
  1. private void myButton_Click(object sender, RoutedEventArgs e) 定义了一个按钮的点击事件处理器。object sender触发该事件的控件(通常是按钮)。RoutedEventArgs e事件参数,提供有关事件的信息。
  2. myButton.Content = "Clicked";将按钮的Content属性(显示在按钮上的文本)更改为"Clicked"myButton是按钮控件的名称,在XAML文件中定义。

这段代码包含几个关键信息:

  1. 类:MainWindow是应用程序的主窗口类。
  2. 事件处理:通过myButton_Click方法处理按钮点击事件。
  3. XAML 与(后端)代码分离:UI 在 XAML 文件中定义,逻辑在 C# 文件中实现,这种分离是 WinUI 的核心设计理念。

10.4.1.2 Hello world!

C# 版的 Hello world! 与 C++/WinRT 版的 Hello world! 除了后端代码一个用 C# 实现,一个用 C++/WinRT 实现,各自所对应的文件MainWindow.xaml.csMainWindow.xaml.cpp存在差异外,其他内容基本保持一致,如在App.xaml标记文件中增加RequestedTheme="Light",将应用界面主题色修改亮色模式;在MainWindow.xaml中调整按钮Button的样式,并增加文本块TextBlock控件等。

PYC icon

图 10-19 Hellow world! (C#版)

文件 MainWindow.xaml MainWindow.xaml.cs
代码
  <?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="WinUI3CSharp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3CSharp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="WinUI3CSharp">

    <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button x:Name="myButton" Click="myButton_Click" Content="Click Me" Style="{StaticResource AccentButtonStyle}" HorizontalAlignment="Center"/>
        <TextBlock x:Name="TB_SayHi" FontFamily="Arial" FontSize="24" FontStyle="Italic" TextWrapping="WrapWholeWords" CharacterSpacing="200" Foreground="CornflowerBlue" Text="Hi, there." Margin="0,10,0,0" />
    </StackPanel>
</Window>
  
  using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace WinUI3CSharp
{
    /// <summary>
    /// An empty window that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
        }

        private void myButton_Click(object sender, RoutedEventArgs e)
        {
            myButton.Content = "Clicked";
            TB_SayHi.Text = "Hello World!";
        }
    }
}
  
🤖 代码解读

myButton_Click 按钮点击事件处理器逻辑处理

  1. myButton.Content = "Clicked";:更改按钮的Content属性,将显示内容更改为"Clicked"myButton是按钮控件的名称。
  2. TB_SayHi.Text = "Hello World!";:更改TB_SayHi(一个TextBlock控件)的Text属性,显示"Hello World!"TB_SayHiTextBlock控件的名称(定义于 XAML 文件中)。

WinUI3 是原生 UI 平台组件,随 Windows 应用程序 SDK(Windows App SDK(Software Development Kit,软件开发工具包)) 一起发布,并与其完全解耦。Windows App SDK 提供了一套统一的 APIs 和工具,可用于创建针对 Windows 10 及更高版本生产桌面的应用程序,并可以发布到微软的应用商店中。为了使开发者快速的了解、掌握 WinUI 3, 并有助于开发者快速开始开发自己的项目,微软公司开发了WinUI 3 Gallery应用程序(图 10-20),演示了所有的 WinUI 3 中的控件和样式(用 Windows App SDK 制作 WinUI 3 应用程序)。并提供了 WinUI 3 Gallery 应用程序的源码,极大方便了开发者对 WinUI 3 的学习,和对控件的应用,且可直接代码迁移于开发者正在开发的项目,加快开发的速度、提升开发的效率。

PYC icon

图 10-20 WinUI 3 Gallery

  • 用 WinUI 3 Gallery 辅助构建应用示例

学习 WinUI 3(C#) 的一种非常有效的方法是通过 WinUI 3 Gallery 示例应用。WinUI 3 Gallery 应用展示了 WinUI 3 的各种功能、控件和样式,可以帮助开发者快速理解如何在实际项目中使用这些功能。图10-21 为集合了 WinUI 3 Gallery 应用中的 7 个典型的布局和控件示例完成的应用结果。这几个布局或控件包括 NavigationView(Navigation)、MenuBar(Menus & toolbars)、SelectorBar(Navigation)、CommandBar(Menus & toolbars)、Scratch Pad(System)、WebView2(Media)和 GridView(Collection)。在应用 WinUI 3 Gallery 学习布局或控件时,在应用中的每个主题示例中均给出了对应示例的代码,并在每页的顶部给出了对应的文档链接(Documentation)和托管于 GitHub上 XAML 和 C#的源码(Source)链接。

PYC icon

图 10-21 实例应用(C#)(Selector Bar 下 Grid View 中使用的图片来自于 AI 图片生成,包括 ChatGPTGoogle GeminiAhakker

构建该示例应用时,采用了MainWindow嵌套MainPage的设计模式。并将布局或控件的示例置于SamplePages文件夹下,具体项目文件结构如图10-22。

PYC icon

图 10-22 项目文件结构

本部分应用示例因为代码相对较多,并且迁移于 WinUI 3 Gallery 应用,因此读者可以直接学习 WinUI 3 Gallery 对应的布局或控件部分源码,或者从coding-x 下载。下述仅对该示例应用主要的代码部分进行解释。

1-MainPage

导航主页面。包含的导航视图控件通过一个可折叠的导航菜单为应用程序的顶层区域提供了一个通用的垂直布局。

文件 MainPage.xaml MainPage.xaml.cs
代码
  <?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="WinUI3CSharp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3CSharp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <TextBlock TextWrapping="WrapWholeWords"
                           Margin="10,25,0,12"
                           Text="If you have five or more equally important navigation categories that should prominently appear on larger window widths, consider using a left navigation pane." />

        <NavigationView x:Name="nvSample5" 
                                            Grid.Row="1" 
                                            Height="Auto"
                                            Header="This is Header Text" 
                                            PaneDisplayMode="Auto" 
                                            IsTabStop="False" 
                                            SelectionChanged="NavigationView_SelectionChanged5" >
            <NavigationView.MenuItems>
                <NavigationViewItem Content="Menu Bar" Tag="MenuBar" Icon="Bullets" />
                <NavigationViewItem Content="Selector Bar" Tag="SelectorBar" Icon="Forward" />
                <NavigationViewItem Content="Command Bar" Tag="CommandBar" Icon="SwitchApps" />
                <NavigationViewItem Content="Scratch Pad" Tag="ScratchPad" Icon="Go" />
            </NavigationView.MenuItems>
            <Frame Margin="0,0,0,0" x:Name="contentFrame5" />
        </NavigationView>
    </Grid>

</Page>
  
  using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text.RegularExpressions;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;

using WinUI3CSharp.SamplePages;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace WinUI3CSharp
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        private void NavigationView_SelectionChanged5(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
        {
            if (args.IsSettingsSelected)
            {
                contentFrame5.Navigate(typeof(WebViewToPage));
            }
            else
            {
                var selectedItem = (Microsoft.UI.Xaml.Controls.NavigationViewItem)args.SelectedItem;
                string selectedItemTag = ((string)selectedItem.Tag);
                sender.Header = SplitPascalCaseToSentence(selectedItemTag);
                string pageName = "WinUI3CSharp.SamplePages." + selectedItemTag;
                Type pageType = Type.GetType(pageName);
                contentFrame5.Navigate(pageType);
            }
        }

        static string SplitPascalCaseToSentence(string input)
        {
            // Add space before each uppercase letter, except the first one
            string result = Regex.Replace(input, @"([a-z])([A-Z])", "$1 $2");

            // Capitalize the first letter of the result string
            result = char.ToUpper(result[0]) + result.Substring(1);

            return result;
        }
    }
}
  

🤖 代码解读

这段 XAML 定义了一个包含文本和导航菜单的页面。页面使用了Grid布局容器,定义了两行,第一行包含文本,第二行包含一个NavigationView控件,NavigationView是一个常用于包含导航项和子页面项的控件。页面设置了响应式布局,菜单项包括一些基本的导航功能,并且绑定了事件处理程序来处理选项变更。

  1. <Grid>是一个布局容器,用于排列其子元素,通常用来实现响应式布局。
  2. Grid.RowDefinitions定义了网格中的行。第一行和第二行(RowDefinition)都设置了Height="Auto",意味着行的高度会根据其内容自适应。
  3. <TextBlock>是一个用于显示文本的控件。TextWrapping="WrapWholeWords"设置文本自动换行,并确保换行不会在单词中间断开。
  4. <NavigationView>用于创建一个导航面板,通常用于带有侧边栏的布局。
  5. Header="This is Header Text"设置导航视图的标题。
  6. PaneDisplayMode="Auto"控制导航窗格的显示模式(自动使用设备宽度)。
  7. IsTabStop="False"禁用该控件的 Tab 键焦点循环。
  8. SelectionChanged="NavigationView_SelectionChanged5"绑定了一个事件处理程序,处理选中项变化时的事件。
  9. NavigationView.MenuItems定义了在导航视图中显示的菜单项。 每个<NavigationViewItem>代表一个菜单。Content="..."是菜单的文本。Tag="..."是一个标记,用于在后端代码中识别该菜单。Icon="...设置每个菜单项的图标。
  10. <Frame>用于显示内容,支持导航。

MainPage作为应用程序的主页面,包含一个NavigationView控件,用户可以选择不同的菜单项进行导航。根据用户的选择,页面将动态加载不同的内容,通过contentFrame5.Navigate()来更新页面内容。代码中使用了一个方法SplitPascalCaseToSentence,将 PascalCase(驼峰型)格式的标签(如菜单中的Tag)转换为分隔的易于阅读的句子格式。

  1. WinUI3CSharp.SamplePages 是一个自定义的命名空间,用于引用项目中的示例页面类(如 WebViewToPage等)。
  2. this.InitializeComponent();用于初始化页面上的所有控件。这个方法在 XAML 页面和后台代码关联时自动生成。
  • NavigationView_SelectionChanged5方法
  1. NavigationView_SelectionChanged5NavigationView控件的事件处理程序,在用户选择不同的导航时触发。
  2. args.IsSettingsSelected判断用户是否选择了设置项。如果是,页面将导航到WebViewToPage页面。
  3. 否则,获取被选中的菜单项(args.SelectedItem),并从中提取Tag属性,Tag通常用于表示菜单项。
  4. 使用SplitPascalCaseToSentence方法将Tag中的 PascalCase(驼峰命名)转换为普通句子格式,然后将转换后的字符串设置为NavigationView控件中的Header(标题)。
  5. 然后,根据选中项的Tag动态构建一个页面类的完整名称(例如,"WinUI3CSharp.SamplePages.MenuBar")。
  6. 使用Type.GetType()方法获取对应的页面类型,然后通过contentFrame5.Navigate(pageType)来导航到这个页面。
  • SplitPascalCaseToSentence方法
  1. 这个方法将 PascalCase (如 MenuBar)转换成普通句子格式(如Menu Bar)。
  2. 使用正则表达式@"([a-z])([A-Z])"查找小写字母后面紧跟大写字母的位置,并在它们之间插入空格。
  3. char.ToUpper(result[0]) + result.Substring(1)将结果字符串的第一个字母大写,确保转换后的句子格式正确。

2-MenuBar

MenuBar通过在应用程序或窗口的顶部提供一组菜单,简化了基本应用程序的创建。

文件 MenuBar.xaml MenuBar.xaml.cs

代码

  <?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="WinUI3CSharp.SamplePages.MenuBar"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3CSharp.SamplePages"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <StackPanel>
        <TextBlock x:Name="SelectedOptionText2" Text="" Margin="12,10,0,0"/>
        <MenuBar x:Name="Example3">
            <MenuBarItem Title="File">
                <MenuFlyoutSubItem Text="New">
                    <MenuFlyoutItem x:Name="z1" Text="Plain Text Document" Click="OnElementClicked"/>
                    <MenuFlyoutItem x:Name="z2" Text="Rich Text Document" Click="OnElementClicked"/>
                    <MenuFlyoutItem x:Name="z3" Text="Other Formats" Click="OnElementClicked"/>
                </MenuFlyoutSubItem>
                <MenuFlyoutItem x:Name="z4" Text="Open" Click="OnElementClicked"/>
                <MenuFlyoutItem x:Name="z5" Text="Save" Click="OnElementClicked"/>
                <MenuFlyoutSeparator />
                <MenuFlyoutItem x:Name="z6" Text="Exit" Click="OnElementClicked"/>
            </MenuBarItem>

            <MenuBarItem Title="Edit">
                <MenuFlyoutItem x:Name="z7" Text="Undo" Click="OnElementClicked"/>
                <MenuFlyoutItem x:Name="z8" Text="Cut" Click="OnElementClicked"/>
                <MenuFlyoutItem x:Name="z9" Text="Copy" Click="OnElementClicked"/>
                <MenuFlyoutItem x:Name="z11" Text="Paste" Click="OnElementClicked"/>
            </MenuBarItem>

            <MenuBarItem Title="View">
                <MenuFlyoutItem x:Name="z12" Text="Output" Click="OnElementClicked"/>
                <MenuFlyoutSeparator/>
                <RadioMenuFlyoutItem x:Name="z13" Text="Landscape" GroupName="OrientationGroup" Click="OnElementClicked"/>
                <RadioMenuFlyoutItem x:Name="z14" Text="Portrait" GroupName="OrientationGroup" IsChecked="True" Click="OnElementClicked"/>
                <MenuFlyoutSeparator/>
                <RadioMenuFlyoutItem x:Name="z15" Text="Small icons" GroupName="SizeGroup" Click="OnElementClicked"/>
                <RadioMenuFlyoutItem x:Name="z16" Text="Medium icons" IsChecked="True" GroupName="SizeGroup" Click="OnElementClicked"/>
                <RadioMenuFlyoutItem x:Name="z17" Text="Large icons" GroupName="SizeGroup" Click="OnElementClicked"/>
            </MenuBarItem>

            <MenuBarItem Title="Help">
                <MenuFlyoutItem x:Name="z18" Text="About" Click="OnElementClicked"/>
            </MenuBarItem>
        </MenuBar>
    </StackPanel>
</Page>
  
  using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace WinUI3CSharp.SamplePages
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MenuBar : Page
    {
        public MenuBar()
        {
            this.InitializeComponent();
        }

        private void OnElementClicked(object sender, RoutedEventArgs e)
        {
            //var selectedFlyoutItem = sender as MenuFlyoutItem;
            //string exampleNumber = selectedFlyoutItem.Name.Substring(0, 1);

            SelectedOptionText2.Text = "You clicked: " + (sender as MenuFlyoutItem).Text;

        }
    }
}
  

🤖 代码解读

这段 XAML 定义了一个包含菜单栏的页面,菜单栏包含多个菜单项(如FileEditViewHelp)。每个菜单项下有多个操作选项,并且包含子菜单和分隔符。所有菜单项的点击事件都绑定到后端代码中的OnElementClicked方法,以便在用户选择时进行响应,并简化示例代码,便于演示。实际响应事件需要根据应用的具体功能进行编写。

  1. <MenuBar>是 WinUI 3 中的菜单控件,类似于桌面应用程序中的顶部菜单栏,包含多个MenuBarItem(菜单项)。菜单项可以包含多个子项,使用MenuFlyoutSubItemMenuFlyoutItem进行组织。基本结构如,
          <MenuBar x:Name="">
            <MenuBarItem Title="">
                <MenuFlyoutSubItem Text="">
                    <MenuFlyoutItem x:Name="" Text="" Click=""/>
                </MenuFlyoutSubItem>
                <MenuFlyoutItem x:Name="" Text="" Click=""/>
                <MenuFlyoutSeparator />
                <MenuFlyoutItem x:Name="" Text="" Click=""/>
            </MenuBarItem>
        </MenuBar>            
  

这段 C# 代码是 WinUI 3 中MenuBar页面的后端代码,用于处理菜单项的点击事件。代码的主要功能是更新页面上的文本框,以显示用户点击的菜单项的名称。

  • OnElementClicked方法
  1. OnElementClicked方法是菜单项点击事件的处理程序。
  2. sender参数是触发事件的控件,如MenuFlyoutItem菜单项控件。
  3. sender强制转换为MenuFlyoutItem,然后访问它的Text属性,获取该菜单项的文本。
  4. 通过设置SelectedOptionText2.Text,将该文本显示在页面上。SelectedOptionText2是 XAML 中定义的一个TextBlock控件,其作用是显示用户点击的菜单项名称。

3-CommandBar

命令栏(Command bars)使用户可以轻松访问应用程序中最常见的任务。命令栏可以提供对应用级或特定于页面的命令的访问,可以与任何导航模式一起使用。默认情况下,命令栏显示一行图标按钮和一个可选的由[...]省略号表示的“查看更多(see more)”按钮。

文件 CommandBar.xaml CommandBar.xaml.cs
代码
  <?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="WinUI3CSharp.SamplePages.CommandBar"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3CSharp.SamplePages"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>

        <StackPanel Grid.Column="0" HorizontalAlignment="Left">
            <CommandBar x:Name="PrimaryCommandBar" IsOpen="False" DefaultLabelPosition="Right">
                <AppBarButton x:Name="addButton" Icon="Add" Label="Add" Click="OnElementClicked"/>
                <AppBarButton x:Name="editButton" Icon="Edit" Label="Edit" Click="OnElementClicked"/>
                <AppBarButton x:Name="shareButton" Icon="Share" Label="Share" Click="OnElementClicked"/>
                <CommandBar.SecondaryCommands>
                    <AppBarButton x:Name="settingsButton" Icon="Setting" Label="Settings" Click="OnElementClicked">
                        <AppBarButton.KeyboardAccelerators>
                            <KeyboardAccelerator Modifiers="Control" Key="I" />
                        </AppBarButton.KeyboardAccelerators>
                    </AppBarButton>
                </CommandBar.SecondaryCommands>
            </CommandBar>
            <TextBlock x:Name="SelectedOptionText" Padding="0,8,0,0"></TextBlock>
        </StackPanel>

        <StackPanel Grid.Column="1" HorizontalAlignment="Right">
            <TextBlock Text="Show or hide" />
            <Button Margin="0,12,0,0" Content="Open command bar" Click="OpenButton_Click" />
            <Button Margin="0,12,0,0" Content="Close command bar" Click="CloseButton_Click" />
            <TextBlock Margin="0,16,0,0" Text="Modify content" />
            <Button Margin="0,12,0,0" Content="Add secondary commands" Click="AddSecondaryCommands_Click" />
            <Button Margin="0,12,0,0" Content="Remove secondary commands" Click="RemoveSecondaryCommands_Click" />
        </StackPanel>
    </Grid>
</Page>
  
  using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace WinUI3CSharp.SamplePages
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class CommandBar : Page, INotifyPropertyChanged
    {
        private bool multipleButtons = false;

        public bool MultipleButtons
        {
            get
            {
                return multipleButtons;
            }
            set
            {
                multipleButtons = value;
                OnPropertyChanged("MultipleButtons");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged(string PropertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
        }
        public CommandBar()
        {
            this.InitializeComponent();
            AddKeyboardAccelerators();
        }

        private void OpenButton_Click(object sender, RoutedEventArgs e)
        {
            PrimaryCommandBar.IsOpen = true;
            PrimaryCommandBar.IsSticky = true;
        }

        private void CloseButton_Click(object sender, RoutedEventArgs e)
        {
            PrimaryCommandBar.IsOpen = false;
            PrimaryCommandBar.IsSticky = false;
        }

        private void OnElementClicked(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
        {
            SelectedOptionText.Text = "You clicked: " + (sender as AppBarButton).Label;
        }

        private void AddSecondaryCommands_Click(object sender, RoutedEventArgs e)
        {
            // Add compact button to the command bar. It provides functionality specific
            // to this page, and is removed when leaving the page.

            if (PrimaryCommandBar.SecondaryCommands.Count == 1)
            {
                var newButton = new AppBarButton();
                newButton.Icon = new SymbolIcon(Symbol.Add);
                newButton.Label = "Button 1";
                newButton.KeyboardAccelerators.Add(new Microsoft.UI.Xaml.Input.KeyboardAccelerator()
                {
                    Key = Windows.System.VirtualKey.N,
                    Modifiers = Windows.System.VirtualKeyModifiers.Control
                });
                PrimaryCommandBar.SecondaryCommands.Add(newButton);

                newButton = new AppBarButton
                {
                    Icon = new SymbolIcon(Symbol.Delete),
                    Label = "Button 2"
                };
                PrimaryCommandBar.SecondaryCommands.Add(newButton);
                newButton.KeyboardAccelerators.Add(new Microsoft.UI.Xaml.Input.KeyboardAccelerator()
                {
                    Key = Windows.System.VirtualKey.Delete
                });
                PrimaryCommandBar.SecondaryCommands.Add(new AppBarSeparator());

                newButton = new AppBarButton();
                newButton.Icon = new SymbolIcon(Symbol.FontDecrease);
                newButton.Label = "Button 3";
                newButton.KeyboardAccelerators.Add(new Microsoft.UI.Xaml.Input.KeyboardAccelerator()
                {
                    Key = Windows.System.VirtualKey.Subtract,
                    Modifiers = Windows.System.VirtualKeyModifiers.Control
                });
                PrimaryCommandBar.SecondaryCommands.Add(newButton);

                newButton = new AppBarButton();
                newButton.Icon = new SymbolIcon(Symbol.FontIncrease);
                newButton.Label = "Button 4";
                newButton.KeyboardAccelerators.Add(new Microsoft.UI.Xaml.Input.KeyboardAccelerator()
                {
                    Key = Windows.System.VirtualKey.Add,
                    Modifiers = Windows.System.VirtualKeyModifiers.Control
                });
                PrimaryCommandBar.SecondaryCommands.Add(newButton);

            }
            MultipleButtons = true;
        }

        private void RemoveSecondaryCommands_Click(object sender, RoutedEventArgs e)
        {
            RemoveSecondaryCommands();
        }

        protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
        {
            RemoveSecondaryCommands();
            base.OnNavigatingFrom(e);
        }

        private void RemoveSecondaryCommands()
        {
            while (PrimaryCommandBar.SecondaryCommands.Count > 1)
            {
                PrimaryCommandBar.SecondaryCommands.RemoveAt(PrimaryCommandBar.SecondaryCommands.Count - 1);
            }
            MultipleButtons = false;
        }


        private void AddKeyboardAccelerators()
        {
            editButton.KeyboardAccelerators.Add(new Microsoft.UI.Xaml.Input.KeyboardAccelerator()
            {
                Key = Windows.System.VirtualKey.E,
                Modifiers = Windows.System.VirtualKeyModifiers.Control
            });

            shareButton.KeyboardAccelerators.Add(new Microsoft.UI.Xaml.Input.KeyboardAccelerator()
            {
                Key = Windows.System.VirtualKey.F4
            });

            addButton.KeyboardAccelerators.Add(new Microsoft.UI.Xaml.Input.KeyboardAccelerator()
            {
                Key = Windows.System.VirtualKey.A,
                Modifiers = Windows.System.VirtualKeyModifiers.Control
            });

        }
    }
}
  

🤖 代码解读

这段代码定义了一个包含CommandBar控件的页面。CommandBar是一种常用于桌面应用程序中的工具栏,包含了主命令和可选的辅助命令按钮,用户可以通过点击这些按钮触发相应的操作。页面还提供了一些控制按钮,用于打开、关闭和修改CommandBar上的内容。

  1. <CommandBar>IsOpen="False"表示CommandBar默认处于关闭状态。CommandBar控件可以通过交互或代码来打开和关闭。DefaultLabelPosition="Right"将命令按钮的标签文本放在图标的右边。
  2. CommandBar主命令按钮AppBarButton中,每个都有一个图标(通过Icon属性指定)标签(通过Label属性指定),及一个Click事件处理程序(Click="OnElementClicked")。
  3. CommandBar次要命令按钮<CommandBar.SecondaryCommands>用于定义次要的命令按钮,并通常会出现在CommandBar的右侧,或者在更多选项中显示。
  4. <AppBarButton.KeyboardAccelerators><KeyboardAccelerator Modifiers="Control" Key="I" />设置了一个键盘快捷键,用户按下Control + I时会触发此按钮的点击事件。
  • CommandBar类定义和INotifyPropertyChanged的实现
  1. public sealed partial class CommandBar : Page, INotifyPropertyChangedpublic sealed partial class CommandBar定义了一个名为CommandBar的类。sealed表示这个类不能被继承,partial表示这个类可以在多个文件夹中分开定义。
  2. : Page表示CommandBar类继承自Page类,意味着其是一个页面类,通常用于 WinUI 或 UWP(Universal Windows Platform)应用程序中。
  3. INotifyPropertyChanged接口允许对象通知其 UI 属性值发生了变化。
  4. private bool multipleButtons = false;定义了一个名为multipleButtons的私有字段,类型为bool(布尔型),初始化为false,用于存储一个布尔值,表示是否启用多个按钮的状态。
  5. public bool MultipleButtons是一个公共属性,提供了对multipleButtons字段的访问权限。
  6. get{return multipleButtons;}get访问器,返回multipleButtons字段的当前值,即获取该属性的值。
  7. set{multipleButtons = value; OnPropertyChanged("MultipleButtons");}set访问器,用于设置MultipleButtons属性的值。每次设置新值时,会更新multipleButtons字段,并调用OnPropertyChanged方法,通知属性发生了变化。value代表传递给属性的值。
  8. public event PropertyChangedEventHandler PropertyChanged;声明一个事件PropertyChanged,类型为PropertyChangedEventHandler,其是INotifyPropertyChanged接口的一部分。该事件用于在属性值发生变化时通知订阅者。
  9. OnPropertyChanged 方法用于触发PropertyChanged事件,其接受一个字符串参数PropertyName,该参数表示发生变化的属性名称。
  10. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));检查PropertyChanged事件是否有订阅者。如果有订阅者,则调用Invoke方法触发事件,并传递当前对象thisPropertyChangedEventArgs 对象。PropertyChangedEventArgs是一个用于描述属性变化的事件参数类,通常与INotifyPropertyChanged接口一起使用,其包含有关发生更改的属性的信息。具体来说,PropertyChangedEventArgs用于在属性值更改时通知事件的订阅者。
  • 构造函数
  1. 在构造函数中,nitializeComponent()初始化 XAML 中的 UI 组件。
  2. 调用AddKeyboardAccelerators()来为CommandBar中的按钮设置键盘快捷键。
  • OpenButton_ClickCloseButton_Click事件处理程序

这两个方法控制一个名为PrimaryCommandBar的命令栏(CommandBar)的状态,分别用于打开和关闭命令栏。

  1. PrimaryCommandBar.IsOpentrue时打开命令栏(隐藏的命令栏显示出来);为false时关闭命令栏。
  2. PrimaryCommandBar.IsStickytrue时,使得打开的隐藏命令栏固定在屏幕上,保持可见;为false时,打开的隐藏命令栏不再固定在屏幕上。命令栏将根据页面滚动的情况表现为普通的可滑动组件。
  • OnElementClicked方法

用于处理一个AppBarButton按钮点击事件。当用户点击某个AppBarButton时,会更新文本控件(如TextBlock)的内容,显示出点击的按钮标签。

  1. object sender表示触发事件的对象。这里sender实际上是被点击的AppBarButton对象。Microsoft.UI.Xaml.RoutedEventArgs e是事件的附加信息,通常包含与事件相关的详细数据,如事件源、事件的处理状态等。
  2. (sender as AppBarButton).Label会返回点击按钮的标签文本。是将事件的触发者sender强制转换为AppBarButton类型。而LabelAppBarButton的一个属性,表示按钮的显示文本。
  • AddSecondaryCommands_Click方法

该方法将多个AppBarButton按钮添加到PrimaryCommandBarCommandBar.SecondaryCommands下,即PrimaryCommandBar.SecondaryCommands

  1. if (PrimaryCommandBar.SecondaryCommands.Count == 1)检查PrimaryCommandBarSecondaryCommands集合中是否已经有一个按钮。如果SecondaryCommands.Count的值为1,表示已经有一个次级命令按钮。
  2. var newButton = new AppBarButton();创建一个新的AppBarButton控件,newButton即为将要添加到命令栏中的按钮。
  3. newButton.Icon = new SymbolIcon(Symbol.Add);设置新按钮的图标为Symbol.Add,是一个表示“加号”(+)图标的符号。
  4. newButton.Label = "Button 1";设置新按钮的标签为"Button 1"。标签文本是显示在按钮旁的文本,用于描述按钮的功能。
  5. newButton.KeyboardAccelerators.Add(...)是为按钮添加一个快捷键方式。KeyboardAccelerators是一个集合,用于存储与控件相关的快捷键。此处配置的快捷键为Ctrl + N,意味着当用户按下Ctrl + N时,就会触发这个按钮的点击事件。Key = Windows.System.VirtualKey.N表示按下字母N键。Modifiers = Windows.System.VirtualKeyModifiers.Control表示同时按下 Ctrl键。
  6. PrimaryCommandBar.SecondaryCommands.Add(newButton);将新创建的newButton添加到PrimaryCommandBarSecondaryCommands集合中。这样,按钮就会出现在命令栏的次级命令区域。
  7. MultipleButtons = true;是设置MultipleButtons属性的值为true,表示已向CommandBar添加了多个次要命令按钮。
  • RemoveSecondaryCommands_ClickOnNavigatingFromRemoveSecondaryCommands方法

这三个方法组合实现了移除PrimaryCommandBar中次级命令(SecondaryCommands)的功能,并更新与按钮数量相关的状态。

  1. RemoveSecondaryCommands();,当点击按钮时,调用RemoveSecondaryCommands方法,移除PrimaryCommandBar中的次级命令按钮。
  2. protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)是页面声明周期中的一个方法,表示当页面开始导航离开当前页面时(例如,用户点击返回按钮或切换到其他页面时),系统会调用此方法。NavigatingCancelEventArgs e提供了关于导航的信息,可能包括目标页面、是否取消导航等。
  3. RemoveSecondaryCommands();,在离开页面时,调用RemoveSecondaryCommands方法,确保移除PrimaryCommandBar中的多余按钮,防止它们在其他页面中出现。
  4. base.OnNavigatingFrom(e);调用基类的OnNavigatingFrom方法,确保系统在执行自定义逻辑后还能够正确执行父类的导航逻辑。
  5. private void RemoveSecondaryCommands()是一个自定义方法,用于从PrimaryCommandBarSecondaryCommands中移除按钮。
  6. while (PrimaryCommandBar.SecondaryCommands.Count > 1),这个while循环检查PrimaryCommandBar.SecondaryCommands中是否有超过1个按钮。SecondaryCommands是命令栏中的次级命令按钮集合。循环会持续执行,直至SecondaryCommands中只有一个按钮。
  7. PrimaryCommandBar.SecondaryCommands.RemoveAt(PrimaryCommandBar.SecondaryCommands.Count - 1);通过RemoveAt方法从SecondaryCommands集合中移除最后一个按钮。每次循环都会移除最后一个按钮,直到剩下一个按钮为止。
  8. MultipleButtons = false;设置MultipleButtonsfalse,表示当前状态下已经移除了多余的按钮,恢复为仅有一个按钮的状态。
  • AddKeyboardAccelerators方法

此方法为CommandBar中的按钮(editButtonshareButtonaddButton)添加键盘快捷键。

  1. editButton.KeyboardAccelerators.Add(...)KeyboardAcceleratorseditButto控件(AppBarButton)的一个属性,是一个KeyboardAcceleratorCollection类型的集合,存储了与按钮相关的键盘快捷键。
  2. Add(...)KeyboardAccelerators集合中添加一个新的键盘快捷键。
  3. new Microsoft.UI.Xaml.Input.KeyboardAccelerator()是创建一个新的KeyboardAccelerator对象,用于定义一个新的键盘快捷键。KeyboardAccelerator是一个类,表示一个键盘快捷键,包含了触发该快捷键的按钮(Key)和修饰键(Modifiers)。
  4. Key = Windows.System.VirtualKey.E是设置快捷键的按键为EWindows.System.VirtualKey.E是枚举值,表示字母E键。
  5. Modifiers = Windows.System.VirtualKeyModifiers.Control是设置快捷键的修饰键为Control(即Ctrl键)。Windows.System.VirtualKeyModifiers.Control是枚举值,表示 Ctrl 键。

4-ScratchPad

提供了一个编辑框,在其中键入 XAML 标记代码,加载后可以查看其外观和行为。

该部分的内容可以详细查看 WinUI 3 Gallery 中对应 Scratch Pad 的解释,并从该页面的顶部查看文档和源码(XAML 和 C#)。

5-WebView2

一个基于 Microsoft Edge(Chromium)的控件,在应用程序中托管 HTML 内容。

文件 WebViewToPage.xaml WebViewToPage.xaml.cs
代码
  <?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="WinUI3CSharp.SamplePages.WebViewToPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3CSharp.SamplePages"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <TextBlock TextWrapping="Wrap" Margin="0,0,0,12">
                WebView2 is powered by the Chromium engine.
        </TextBlock>
        <WebView2 x:Name="MyWebView2" Source="https://coding-x.tech" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="1" MinHeight="560" MinWidth="200"/>
    </Grid>
</Page>
  
  using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;


namespace WinUI3CSharp.SamplePages
{
    public sealed partial class WebViewToPage : Page
    {
        public WebViewToPage()
        {
            this.InitializeComponent();
        }
    }
}
  
🤖 代码解读

<WebView2 x:Name="MyWebView2" Source="https://coding-x.tech" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="1" MinHeight="560" MinWidth="200"/><WebView2>是一个 WebView 控件,用于显示网页内容,是基于 Chromium 的嵌入式浏览器,提供了比传统WebView更强的功能。Source="https://coding-x.tech"指定 WebView2 要加载的网页 URL。HorizontalAlignment="Stretch"VerticalAlignment="Stretch"表示 WebView2 会占据父容器(此处为Grid)中指定区域的全部宽度和高度。

6-GridView + ViewMOdel

允许显示按水平滚动的行和列排列的项目集合。

文件 GridView.xaml GridView.xaml.cs
代码
  <?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="WinUI3CSharp.SamplePages.GridView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3CSharp.SamplePages"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Page.Resources>
        <DataTemplate x:Key="ImageTemplate" x:DataType="local:CustomDataObject">
            <Image Stretch="UniformToFill" Source="{x:Bind ImageLocation}" AutomationProperties.Name="{x:Bind Title}" Width="190" Height="190" AutomationProperties.AccessibilityView="Raw" />
        </DataTemplate>
    </Page.Resources>

    <StackPanel>
        <TextBlock Margin="0,0,0,15">
                    This is a basic GridView that has the full source code below. <LineBreak/>Other samples on this page display only the additional markup needed to customize the specific GridView.
        </TextBlock>
        <GridView
                    x:Name="BasicGridView"
                    ItemTemplate="{StaticResource ImageTemplate}"
                    IsItemClickEnabled="True"
                    ItemClick="BasicGridView_ItemClick"
                    SelectionMode="Single"/>
        <TextBlock x:Name="ClickOutput" Style="{StaticResource OutputTextBlockStyle}" />
    </StackPanel> 
</Page>
  
  using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;

using System.Collections.ObjectModel;
using Microsoft.UI;

namespace WinUI3CSharp.SamplePages
{
    public sealed partial class GridView : Page
    {
        public GridView()
        {
            this.InitializeComponent();
            this.DataContext = this;
        }

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
            // Get data objects and place them into an ObservableCollection
            List<CustomDataObject> tempList = CustomDataObject.GetDataObjects();
            ObservableCollection<CustomDataObject> Items = new ObservableCollection<CustomDataObject>(tempList);
            BasicGridView.ItemsSource = Items;
        }

        private void BasicGridView_ItemClick(object sender, ItemClickEventArgs e)
        {
            ClickOutput.Text = "You clicked " + (e.ClickedItem as CustomDataObject).Title + ".";
        }
    }
}
  
🤖 代码解读

这段 XAML 代码展示了如何使用 GridView控件来显示项,并且每项会使用DataTemplate来定制显示样式。同时,点击GridView中的项会触发ItemClick事件,会在下面的TextBlock中显示用户点击项的相关信息。

  • <Page.Resources>...</Page.Resources>部分
  1. <Page.Resources>内部,定义了一个DataTemplate,用于在 XAML 中呈现一个 CustomDataObject类型的数据对象,展示图像和标题信息,用于数据绑定,以便在页面中显示图像,并提供可访问性功能。
  2. <Page.Resources>是一个 XAML 资源字典,用于在页面中定义可以在整个页面中使用的资源(如样式、模板、颜色、数据模板等)。这里,<Page.Resources>包含了一个DataTemplate,可以在页面中绑定数据并使用此模板来呈现数据对象。
  3. <DataTemplate x:Key="ImageTemplate" x:DataType="local:CustomDataObject"><DataTemplate>是一个用来定义如何展示数据的模板。如定义一个图像和标题的显示方法。x:Key="ImageTemplate"是模板的键,表示在 XAML 中唯一标识该模板。通过x:Key,可以在绑定时引用此模板,例如在GridViewContentControlListView中使用。
  4. x:DataType="local:CustomDataObject"表示模板是针对local:CustomDataObject类型的对象创建的。x:DataType用于定义绑定的类型,以便在编译时进行类型检查和优化。
  5. <Image .../>是一个用于显示图像的控件。Stretch="UniformToFill"设置图像的缩放方式为UniformToFill,意味着图像会被缩放以填充其容器,同时保持纵横比,可能会裁切图像的部分内容以适应容器大小。Source="{x:Bind ImageLocation}"Source属性绑定到数据对象的ImageLocation属性(图像存储的位置)。AutomationProperties.Name="{x:Bind Title}"AutomationProperties.Name是一个可访问性工具(如屏幕阅读器)的属性,并绑定到数据对象的Title属性。Width="190" Height="190"设置图像的固定宽度和高度为 190 像素。图像会被拉伸或裁切以适应此大小。AutomationProperties.AccessibilityView="Raw"是 WinUI 和 UWP 中增强无障碍功能的一个属性,指示屏幕阅读器(或其他辅助技术)应如何访问和呈现该元素的信息。AutomationProperties.AccessibilityView属性用于控制元素在自动化客户端(如屏幕阅读器、放大镜、语音命令工具等)的可见性。该属性有两个值:Content(默认值),屏幕阅读器将元素的内容提供给用户,通常这是最常用的设置;Raw屏幕阅读器将该元素的原始 UI 内容呈现给用户,忽略任何装饰或复杂的 UI 结构。即,Raw会让自动化工具更直接地读取元素地内容,而不进行任何特殊地格式化或层级呈现。
  • <GridView.../>部分
  1. <GridView>是一种集合展示控件,用于显示数据集合,每个数据项会在网格中作为一个项呈现。支持图像、文本等多种数据类型的显示,并且用户可以与这些项目交互(如选择、点击等)。
  2. x:Name="BasicGridView"是对GridView控件的命名。通过x:Name,可以在后端代码(C#)引用这个控件。通过BasicGridView访问这个控件,并操作其属性或事件。
  3. ItemTemplate="{StaticResource ImageTemplate}"指定了GridView中每个项的显示模板(即每一项的外观)。ItemTemplate绑定到一个资源,资源的键为ImageTemplate,为先前定义的DataTemplateImageTemplate中定义了如何展示每个项的数据(如显示图像和标题)。通过ItemTemplate,每个GridView项目都将应用ImageTemplate来展示数据(图像和标题)。
  4. IsItemClickEnabled="True"属性启动了项点击功能。设置为True表示用户可以点击每个项。当用户点击某个项时,控件会触发ItemClick事件,允许处理点击后的事件;如果为False,则无法通过点击项来触发事件。
  5. ItemClick="BasicGridView_ItemClick"是一个事件处理程序,当用户点击GridView中某个项时触发。ItemClick事件会传递一个参数,包含有关被点击项的信息(如项的索引、数据等)。后端代码的事件处理程序为BasicGridView_ItemClick方法。
  6. SelectionMode="Single"表示GridView中用户只能选择一个项目。如果设置为Multiple,则用户可以选择多个项。
  • 构造函数
  1. this.DataContext = this;的核心作用是绑定上下文,使页面的控件能够绑定到页面类中的属性和方法。this指代当前类的实列(即当前页面对象GridView)。当前页面中的属性和方法可以通过this来访问。DataContext是一个属性,表示 XAML 中控件的数据绑定上下文。数据绑定会通过DataContext查找绑定源(即要绑定的对象),从而在 UI 元素上显示对应的数据。即当 UI 元素(如TextBlockListView)使用{Binding}时,会根据DataContext来解析属性或数据对象。this.DataContext = this;将页面对象作为DataContext,这样,页面中的控件就可以绑定到页面类(GridView类)中的属性或方法。DataContext 决定了控件的数据绑定来源。如果不设置,控件无法找到数据源。例如,如果在页面类中(后端代码)有一个属性public string PageTitle { get; set; } = "Welcome to GridView";,然后在前端用户界面 XAML中有一个<TextBlock Text="{Binding PageTitle}" /> ,如果没有设置DataContext,绑定无法解析到PageTitleText属性会显示为空。
  • OnNavigatedTo方法

该方法在导航到该页面时被触发,用于初始化数据并设置页面的内容。

  1. protected override void OnNavigatedTo(NavigationEventArgs e)protected表示此方法只能在当前类或其派生类中访问。override表示这是一个重写方法,用于覆盖基类(Page类)的OnNavigatedTo方法。OnNavigatedTo是一个生命周期方法,在导航到页面时被调用。NavigationEventArgs e提供与导航事件相关的信息,包含一些属性,如导航来源、参数数据等。
  2. (调用基类方法)base.OnNavigatedTo(e);调用基类PageOnNavigatedTo方法,确保基类的逻辑得以执行。
  3. (获取数据对象列表)List<CustomDataObject> tempList = CustomDataObject.GetDataObjects();调用CustomDataObject类的静态方法GetDataObjects(),获取一个CustomDataObject类型的列表。
  4. (转换为可观察集合)ObservableCollection<CustomDataObject> Items = new ObservableCollection<CustomDataObject>(tempList);使用tempList构造一个ObservableCollection对象。将数据从普通List转换为具有动态更新功能的集合ItemsObservableCollection是一个动态数据集合,与普通List的区别在于,其支持通知机制。当集合内容发生变化(例如增加、删除或修改元素)时,UI 会自动更新。
  5. (绑定数据)BasicGridView.ItemsSource = Items;ItemsObservableCollection<CustomDataObject>)设置为数据源,GridView将自动生成 UI 项目并显示数据。BasicGridView是 XAML 中定义的GridView控件。ItemsSourceGridView的数据源属性。
  • BasicGridView_ItemClick方法
  1. (方法签名)private void BasicGridView_ItemClick(object sender, ItemClickEventArgs e)中,private表示此方法仅在GridView类中可访问。void表示方法没有返回值。BasicGridView_ItemClick方法名称表明其用途是处理BasicGridViewItemClick事件。参数object sender为触发事件的对象,这里是BasicGridViewGridView的控件实例)。ItemClickEventArgs e提供关于所点击项的信息。
  2. (事件处理逻辑)ClickOutput.Text = "You clicked " + (e.ClickedItem as CustomDataObject).Title + ".";e.ClickedItem as CustomDataObjectClickedItem是被点击的对象。使用as运算符将其转换为CustomDataObject类型。如果转换失败,结果为nullGridView数据源(ItemsSource)为ObservableCollection<CustomDataObject>,因此ClickedItem应该是CustomDataObjectCustomDataObject类中有一个Title属性,表示数据项的标题。获取标题后,建立一个字符串用于ClickOutputTextBlock)控件的Text属性,显示输出文本。
文件 ViewModel.xaml ViewModel.xaml.cs
代码
  <?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="WinUI3CSharp.SamplePages.ViewModel"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3CSharp.SamplePages"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

</Page>
  
  using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;

namespace WinUI3CSharp.SamplePages
{
    public sealed partial class ViewModel : Page
    {
        public ViewModel()
        {
            this.InitializeComponent();
        }
    }

    public class CustomDataObject
    {
        public string Title { get; set; }
        public string ImageLocation { get; set; }
        public string Views { get; set; }
        public string Likes { get; set; }
        public string Description { get; set; }

        public CustomDataObject()
        {
        }

        public static List<CustomDataObject> GetDataObjects(bool includeAllItems = false)
        {
            string[] dummyTexts = new[] {
                @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. ...",
                @"Nullam eget mattis metus. Donec pharetra, ...",
                @"Quisque accumsan pretium ligula in faucibus. ...",
                @"Aenean in nisl at elit venenatis blandit ut vitae lectus. ...",
                @"Ut consequat magna luctus justo egestas vehicula. ...",
                @"Proin malesuada, libero vitae aliquam venenatis, ...",
                @"Aenean vulputate, turpis non tincidunt ornare, ...",
                @"Duis facilisis, quam ut laoreet commodo, elit ex aliquet massa, ...",
            };

            Random rand = new Random();
            int numberOfLocations = includeAllItems ? 13 : 8;
            List<CustomDataObject> objects = new List<CustomDataObject>();
            for (int i = 0; i < numberOfLocations; i++)
            {
                objects.Add(new CustomDataObject()
                {
                    Title = $"Item {i + 1}",
                    ImageLocation = $"/Assets/SampleMedia/codingx-{i + 1}.jpg",
                    Views = rand.Next(100, 999).ToString(),
                    Likes = rand.Next(10, 99).ToString(),
                    Description = dummyTexts[i % dummyTexts.Length],
                });
            }
            return objects;
        }
    }
}
  
🤖 代码解读

这段代码定义了一个ViewModel页面类,用于与 XAML 文件绑定。提供了CustomDataObject数据模型,支持包含标题、图片路径、浏览次数、点赞数和描述的对象。提供了GetDataObjects静态方法,生成模拟的CustomDataObject列表,便于测试或展示功能。在GridView页面中绑定了生成的CustomDataObject数据模型,对应于OnNavigatedTo方法。

  • CustomDataObject
  public class CustomDataObject
{
    public string Title { get; set; }
    public string ImageLocation { get; set; }
    public string Views { get; set; }
    public string Likes { get; set; }
    public string Description { get; set; }
}
  
  1. 定义了一个数据模型类CustomDataObject,用于存储每个数据项的信息。包含的属性有Title,数据项的标题;ImageLocation,图像文件的路径;Views浏览次数;Likes点赞数;Description数据项的描述文本。这些属性通常绑定到 XAML 的控件,如TextBlockImage等,用于显示内容。
  • 构造函数
  public CustomDataObject()
{
}
  
  1. 为默认的空构造函数,方便创建实例。
  • GetDataObjects静态方法
  1. (方法签名)public static List<CustomDataObject> GetDataObjects(bool includeAllItems = false),返回一组CustomDataObject的集合。通过模拟数据生成一个List<CustomDataObject>对象列表。includeAllItems参数,控制生成的对象数量,默认为false(生成8个对象),如果为true,则生成13个对象。对应到方法内部的int numberOfLocations = includeAllItems ? 13 : 8;代码,? :是条件(三元)运算符,语法为condition ? value_if_true : value_if_false;condition是需要判断的布尔表达式;value_if_true是条件为true时的返回值;value_if_false是条件为false时的返回值。这里的代码逻辑是根据includeAllItems参数值来确定要生成的CustomDataObject的数量,includeAllItems = true时,生成13个数据项;includeAllItems = false时生成8个数据项。通过这种方式,可以灵活控制生成的集合大小,便于在不同场景下使用。
  string[] dummyTexts = new[] {
    @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. ...",
    @"Nullam eget mattis metus. Donec pharetra, ...",
    ...
};
  
  1. (模拟的文本数组)是一个包含示例描述文本的数组,每个文本段落是示意的占位符内容。
  2. (随机数生成器)Random rand = new Random();,用于生成浏览次数(Views)和点赞数(Likes)的随机数。
  List<CustomDataObject> objects = new List<CustomDataObject>();
for (int i = 0; i < numberOfLocations; i++)
{
    objects.Add(new CustomDataObject()
    {
        Title = $"Item {i + 1}",
        ImageLocation = $"/Assets/SampleMedia/codingx-{i + 1}.jpg",
        Views = rand.Next(100, 999).ToString(),
        Likes = rand.Next(10, 99).ToString(),
        Description = dummyTexts[i % dummyTexts.Length],
    });
}
  
  1. (创建CustomDataObject的实例,并设置其属性):Title,根据索引生成,如"Item 1"ImageLocation,每个示例图片存储的文件路径为$"/Assets/SampleMedia/codingx-{i + 1}.jpg"ViewsLikes使用随机数生成模拟的数字;DescriptiondummyTexts数组中取描述文本,循环使用数组内容。
  2. return objects;返回对象列表。

10.5 打包和发布应用

10.5.1 打包配置(应用程序配置清单)

Solution Explorer下的Package.appxmanifest 文件,其选项卡参数的配置有关应用程序的元数据、设置和声明信息,用于定义应用的基本信息及如何与操作系统交互。

  • Application 选项卡主要用于配置识别和描述应用程序的属性。

PYC icon

图 10-23 Application 选项卡

  1. Display name(显示名称):用于定义应用程序的显示名称。
  2. Entry point(入口点):指定应用程序启动时的入口点,一般是主类或程序集的名称。当前值为$targetentrypoint$,表示为一个占位符,会在应用构建时替换为具体的入口点。
  3. Default language(默认语言):设置应用程序的默认语言。与清单配置器中大多数字段不同,此值保存在项目文件中,而不是应用程序清单中。
  4. Description(描述):描述应用程序的功能或用途,是指定在Set Default Programs界面中显示的文本。
  5. Trust level(信任级别):配置应用程序的信任级别。取值为AppContainerMediumIL。试验中为(not set),未设置。
  6. Runtime behavior(运行时行为):定义应用程序运行时行为,取值为WindowsAppPackageClassAppWin32AppAppSilo。试验中为(not set),未设置。
  7. Supported rotations(支持的屏幕旋转):一个可选的配置,指示应用程序的方向偏好,包括Landscape(横向)、Portrait(纵向)、Landscape-flipped(横向翻转)Portrait-flipped(纵向翻转)。实验中选择了LandscapePortrait
  8. Lock screen notifications(锁屏通知):配置程序在用户锁屏屏幕上显示通知的方式。包含的值有BadgeBadgeAndTileText。试验中为(not set),未设置。
  9. Resource group(资源组):一个应用程序定义的名称,用于确定多个任务的相同主机进程。
  10. Tile Upate(磁贴更新):通过定期轮询 URI (Uniform Resource Identifier,统一资源标识符)更新应用程序标题。URI 模板可以包含languageregion令牌(tokens),这些令牌将在运行时被替换,以生成要轮询的 URI。
  11. URI Template(URI 模板):指定用于在应用程序首次启动之前开始更新该应用程序标题的 URI。
  • Visual Assets 选项卡用于生成和配置应用程序不同分辨率的视觉资源,确保应用在各种显示设备上都能正确展示图标和启动画面。

PYC icon

图 10-24 Visual Assets 选项卡

Asset Generator(资源生成器)

  1. Source(源文件):选择用于应用程序图标的图像。
  2. Destination(目标文件夹):设置生成的资源文件存放的目标文件夹。这里按默认为Assets
  3. Assets(资源类型):选择生成的资源类型,例如小图标(Small Tile)、中等图标(Large Tile)、应用图标(App Icon)、启动画面(Splash Screen)等。
  4. Scales(缩放比例):选择资源的缩放比例。
  5. Scaling Mode(缩放模式):选择图像缩放的方式,包括Bicubic(Smoother Edges)Nearest Neighbor(Sharper Edges)
  6. Apply recommended padding(应用推荐的填充):选择是否应用建议的图像填充。
  7. Apply color conversion for Windows Light Theme(应用 Windows Light Theme 的颜色转换):选择是否为 Windows Light Theme(浅色主题)进行颜色转换。
  8. Generate(生成):点击按钮来生成所需的所有图像资源。

Display Settings(显示设置)

  1. Short name(短名称):应用程序的缩写名称。
  2. Show name(显示名称):选择在磁贴中显示名称的类型,可以选择Medium TileWide TileLarge Tile
  3. Tile background(磁贴背景):设置磁贴的背景颜色或透明度。这里设置为transparent(透明)。
  4. Splash screen background(启动画面背景):设置启动画面的背景颜色。

Preview Images(预览图像)

显示源图像及其缩放版本的预览图。这些缩放后的图像用于不同尺寸的磁贴显示。

  • Capabilities 选项卡用于设置应用程序需要使用的系统功能或设备权限。

PYC icon

图 10-25 Capabilities 选项卡

可以通过点击Capabilities列表框中的各项,在Description下会对应给出所选项的解释说明。

  • Declarations 选项卡可添加声明并指定其属性

PYC icon

图 10-26 Declarations 选项卡

打开Available Declarations(可用声明)下拉框,可以看到支持的声明,包括有Account Picture Provider(账户图片提供程序)、Alarm(闹钟)、App Extension Host(应用程序扩展主机)、App Service(应用程序服务)、Appointments Provider(约定提供程序)、AutoPlay Content(自动播放内容)、AutoPlay Device(自动播放设备)、Background Tasks(后台任务)、Cached File Updater(缓存文件更新程序)、Camera Settings(相机设置)、Certificates(证书)、Dial Protocol(拨号协议)、File Open Picker(文件打开选择器)、File Save PickerFile Type Associations(文件类型关联)、Lock Screen(锁屏)、Media Playback(媒体回放)、Personal Assistant launcher(个人助理启动)、Pre-installed Configuration(预安装配置)、Print 3D Workflow(3D 打印工作流)、Print Task Settings(打印任务设置)、Protocol(协议)、Restricted Launch(受限启动)、Search(搜索)、Share Target(共享目标)。

这些声明帮助开发者定义应用程序的功能和与操作系统集成方式,以便更好地利用 Windows 平台的特性和服务。

  • Content URIs 选项卡指定可以使用window.external.notify向应用程序发送ScriptNotify事件的 URIs。

PYC icon

图 10-27 Content URIs 选项卡

URIs 可以在子域名中包含通配符,如https://*.microsoft.comhttps://*.*.microsoft.com

  • Packaging 选项卡设置应用程序的打包信息,即设置应用程序包在部署时的标识(identify)和描述(describe)属性。

PYC icon

图 10-28 Packaging 选项卡

  1. Package name(包名称):指定在系统上标识包的唯一名称。当包上传到商店时,此名称将被替换。示例使用默认生成的名称。
  2. Package display name(包显示的名称):指定出现在应用商店中的应用程序名称。当包上传到商店时,此名称将被替换。示例中使用的名称为WinU3CSharp
  3. Version(版本):一个四元表示法的版本字符串,Major.Minor.Build.Revision
  4. Publisher(发布者):应用程序的发布者信息。指定用于对包进行身份验证的签名证书的主题字段。当包上传到商店时,此名称将被替换。(证书在Create App Packages...时配置,见后文)
  5. Publisher display name(发布者显示名称):指定在开发人员门户网站上的“Publisher Name”字段上使用的名称。当软件包上传到 Windows App Store 时,此名称将被替换。
  6. Package Family name(包家族名称):标识系统上包的唯一名称,由包名称和发布者字符串的散列(hash)组成。

10.5.2 创建应用程序包

Solution Explorer(解决方案资源管理器)项目名称(示例的项目名称为WinUI3CSharp)上右键菜单栏选择Package and Publish->Create App Packages...创建应用程序包。

  • 步骤1:Select distribution method

PYC icon

图 10-29 Select distribution method

可以选择的部署方式包括官方渠道Microsoft Store(微软商店),和第三方渠道SideloadingSideloading(侧载)是指安装来自非官方渠道的应用程序,以创建自己的应用,包括企业应用程序(line-of-business,LOB)。当侧载应用程序时,需将签名的应用程序包(signed app package)部署到安装的设备上,并维护应用程序的签名、托管和部署。并且只有将分配给应用程序的受信任安全证书导入到本地设备,以允许设备信任该应用程序。

  • 步骤2:Select signing method

PYC icon

图 10-30 Select signing method

应用程序包只有签名(signed)才能在设备上安装。选择证书对应用程序包进行签名的证书选择包括从Azure密钥库;从应用商店;从文件;或自行创建(Create)。当选择创建将进入创建自签名测试证书弹出框(Create a Self-Signed Test Certificate)。自签名证书通常用于测试,当安装到其他设备上可能会受到限制。填写发布者通用名称(Publisher Common Name)和密码后返回Select signing method弹窗,并显示了当前所用的证书。

  • 步骤3:Select and configure packages

PYC icon

图 10-31 Select and configure packages

选择输出应用程序包的存储位置,配置应用版本,选择架构(Architecture)和解决方案配置(Solution Configuration)后开始创建(Create)应用程序包。创建完成后,打开输出路径文件夹,点击*.cer文件安装证书后,通过双击*.msix文件安装应用;或右键点击Add-AppDevPackage.ps1文件,从弹出菜单中选择Run with PowerShell执行安装。完成安装后,就可以从系统的开始菜单中找到已安装的应用或搜索应用,打开应用,运行、调试和测试应用程序包,可以使用 Windows 调试工具调试应用程序。

参考文献(Reference):

[1] Overview of framework options (https://learn.microsoft.com/en-us/windows/apps/get-started/?tabs=cpp-win32%2Cpwa).

[2] XAML overview (https://learn.microsoft.com/en-us/windows/uwp/xaml-platform/xaml-overview).

[3] Fluent 2 (https://fluent2.microsoft.design/get-started/whatisnew).

[4] WinUI in the Windows App SDK (WinUI 3) (https://learn.microsoft.com/en-us/windows/apps/winui/winui3/).

[5] Build a Hello World app using C# and WinUI 3 / Windows App SDK(https://learn.microsoft.com/en-us/windows/apps/how-tos/hello-world-winui3).

[6] C++/WinRT+WinUI (https://www.youtube.com/@Tothepoint-uw3ws/playlists).

[7] Package a desktop or UWP app in Visual Studio(https://learn.microsoft.com/en-us/windows/msix/package/packaging-uwp-apps).