前言
WinUI 基于 Windows App SDK(就是头文件、库和工具集),使用 XAML (前端)和 C# 或 C++ (后端)创建出符合 Windows 用户期望 Fluent Design 外观和风格的应用。
介于这是聪明的巨硬又一次做出的愚蠢决定,以后很可能被抛弃,学习纯属娱乐。

本文为作者跟着官方教程制作一个NotesApp(WinUINotes)所记录的笔记(放心吧,你可以不用去看官方文档了,我把文档内容中译中了,方便想阅读本文的人理解,一起学习)。
我已经跟着微软的教程成功新建并构建了项目,如果想要使用本文入门WinUI,需要先查看此教程:快速入门:设置环境并创建 WinUI 3 project,通过此教程学习如何利用 Visual Studio 创建项目并构建项目。
其实学习 Visual Studio 并不是错误的选择,这个IDE自带开发环境,帮忙自动配置好,非常方便。
新建项目
打开 Visual Studio ,点击右侧的新建项目按钮

在这里搜索“winui”模板

可以发现有两个模板,简单来说,本文选择 Blank App, Packaged (WinUI 3 in Desktop)(WinUI空白应用(已打包))
- 选择“Blank App, Packaged (WinUI 3 in Desktop)”:单项目,应用和打包配置在一起。
- 选择“Blank App, Packaged with Windows Application Packaging Project”:双项目,应用和打包器分开。
打包器就是把复杂项目所有东西放到一起,统一签名和打包,生成
.msix或.msixbundle安装包。
根据下方图片输入"WinUINotes",然后点击“ 创建”

项目文件用途
点击菜单栏的"视图 - 解决方案管理器"

在解决方案资源管理器中可以看到 WinUI 项目的文件

Assets 文件夹:可以存放图片等资源文件
App.xaml 和 App.xaml.cs :程序入口点(程序的启动逻辑),可以控制全局样式,启动逻辑等。
MainWindow.xaml 和 MainWindow.xaml.cs :应用的主窗口(用户看到的第一个界面),用界面层(XAML 标记语言写)和 逻辑层(C#写)
Package.appxmanifest :Visual Studio 自动生成的清单文件,包项目配置文件(这个不重要)
修改界面
由于我之前写过 HTML ,所以这里用标签指代
<object>方便理解。
打开 MainWindow.xaml 。

可以看到默认是这样的👇
<!--?xml version="1.0" encoding="utf-8"?-->
<window x:class="WinUINotes.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:WinUINotes" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:ignorable="d" title="WinUINotes">
<window.systembackdrop>
<micabackdrop>
</micabackdrop></window.systembackdrop>
<grid>
</grid>
</window>在 MainWindow.xaml ,可以控制窗口的内容,就是图中的这片区域:

窗口的顶部区域由 Windows 系统控制,它包括标题栏,标题控件(最小/最大/关闭按钮)、应用图标、标题和拖动区域。 它还包括包围窗口外侧的框架。

XAML入门
众所不周知,HTML实际就是面向对象编程,因为每个元素都可以添加修改属性(在XAML里面,这也是一样的。
既然没学过XML,那就直接理解XAML好了,不用看两者区别了。
这里与HTML及其相似,元素在XAML中叫做标记,大多数标签是成对出现的,即一个开标签(如 <div>)和一个闭标签(如 </div>),自闭合也是可以用的,例如 <Canvas />,它们之间的内容就是元素(标记)的内容。
一个 XAML 元素标签就代表创建一个对象
<!-- 创建一个按钮对象 -->
<Button />
<!-- 创建一个文本框对象 -->
<TextBox />可以给对象设置属性
XAML 也可以在标签里添加属性,例如简单的属性语法 <object attribute="value"> 也就是 属性名="值" ,如果属性过于复杂或者想要让格式更加整齐,可以使用复杂的属性语法。形式为 <类型.属性名> 嵌套在标签内:
<object>
<object.property>
value
</object.property>
</object>先不要在语法这里浪费时间与热情,等下边做边学,会讲到的awa
给界面添加一些内容
将 <Window.. > 标签的内容替换为以下代码:
<window.systembackdrop>
<micabackdrop kind="Base">
</micabackdrop></window.systembackdrop>
<grid>
<grid.rowdefinitions>
<!-- Title Bar -->
<rowdefinition height="Auto">
<!-- App Content -->
<rowdefinition height="*">
</rowdefinition></rowdefinition></grid.rowdefinitions>
<titlebar x:name="AppTitleBar" title="WinUI Notes">
<titlebar.iconsource>
<fonticonsource glyph="">
</fonticonsource></titlebar.iconsource>
</titlebar>
<!-- App content -->
</grid>现在看起来是这样的:

按 Ctrl + S 或点击工具栏中的“保存”图标都可以保存文件。
保存后点击顶部的编译按钮

可以看到成功构建了窗口,窗口也显示了 TitleBar 元素,就是标题栏下方的图标和"WinUI Notes"文字

去掉自带的标题栏
其实,我们是想把这个原本的难看的系统标题栏替换掉,换成 TitleBar 元素内容。
需要在 MainWindow.xaml.cs 写一些代码来替换掉系统标题栏。
打开 MainWindow.xaml.cs .....?这个文件被隐藏了,需要在 MainWindow.xaml 代码编辑窗口中按下 F7 ,或者在编辑窗口右键并选择“查看代码”也是可以的。
其实在解决方案资源管理器展开 MainWindow.xaml 也可以看到 MainWindow.xaml.cs qwq
可以看到 MainWindow.xaml.cs 默认内容是这样的:
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 System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
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 WinUINotes
{
/// <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()
{
InitializeComponent();
}
}
}将 MainWindow 函数的代码替换为:
public MainWindow()
{
this.InitializeComponent();
// ↓ 添加了这些内容 ↓
// 隐藏默认系统标题栏
ExtendsContentIntoTitleBar = true;
// 用 WinUI TitleBar 替换系统标题栏
SetTitleBar(AppTitleBar);
// ↑ 添加了这些内容 ↑
}这里的 AppTitleBar 对应了之前在 MainWindow.xaml 写的 TitleBar 的参数。

按下 F5 可以直接调试编译()
保存并编译一下,可以看到成功替换了标题栏:

学到这里很厉害啦,可以休息一下了,等会再来学习后面的内容..
创建一个新的页面
现在,我们将创建一个页面,允许用户编辑笔记,然后编写代码以保存或删除笔记。
在解决方案资源管理器的项目名"WinUINotes"处右键点击,在菜单中选择"添加 - 新建项"

点击左下角的"显示所有模板"

选择"空白页"模板,修改名称为 NotePage.xaml,点击添加

给新的页面添加内容
打开 NotePage.xaml ,可以看到默认内容是这样的👇
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="WinUINotes.NotePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WinUINotes"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
</Grid>
</Page> 将 <Grid> ... </Grid> 元素中的内容替换为以下代码:
<Grid Padding="16" RowSpacing="8">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="400"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="NoteEditor"
AcceptsReturn="True"
TextWrapping="Wrap"
PlaceholderText="Enter your note"
Header="New note"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Width="400"
Grid.Column="1"/>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="4"
Grid.Row="1" Grid.Column="1">
<Button Content="Save" Style="{StaticResource AccentButtonStyle}"/>
<Button Content="Delete"/>
</StackPanel>
</Grid>现在看起来应该是这样的:

回到 MainWindow.xaml,将 Frame 的内容修改为
<Frame x:Name="rootFrame" Grid.Row="1" SourcePageType="local:NotePage"/>
- Frame:框架
Frame本身不显示内容,而是负责在多个Page(页面)之间切换。
现在来编译一下看看效果 :)

现在我们可以来研究一下这个页面的构成了。
XAML基础
回顾我们在 MainWindow.xaml 写的代码,可以注意到我们把控件写在了 Grid (网格)元素中:

- Grid:网格(布局容器,用于按行和列排列子元素)
- RowDefinition:行定义(定义网格中某一行的属性,如高度)
可以看到,我们把网格的每一行的属性写在了 <Grid.RowDefinitions> 元素里面,有两个 <RowDefinitions>,也就是创建了2个行。
行是从0开始数的,在子元素中通过 Grid.Row="哪一行" 属性来指定该元素所处的行。可以看看下面代码的注释,方便理解~
<Grid>
<!-- Grid.RowDefinitions 定义网格的行 -->
<Grid.RowDefinitions>
<!-- 标题栏行:高度自适应(Auto) -->
<!-- 从上往下定义,现在规定第0行的高度 -->
<RowDefinition Height="Auto" />
<!-- 内容行:占满剩余的所有空间(*) -->
<!-- 从上往下定义,现在规定第1行的高度 -->
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- 以下为子元素(也就是内容) -->
<!-- 现在是第一个子元素,没写Grid.Row属性默认放在第0行 -->
<TitleBar x:Name="AppTitleBar"
Title="WinUI Notes">
<TitleBar.IconSource>
<FontIconSource Glyph=""/>
</TitleBar.IconSource>
</TitleBar>
<!-- 内容 -->
<!-- 现在是第二个子元素,Grid.Row属性为1放在第1行 -->
<Frame x:Name="rootFrame" Grid.Row="1" SourcePageType="local:NotePage"/>
</Grid>现在再查看 NotePage.xaml 中的代码,同样可以看到我们把所有子元素写在了 Grid (网格)元素中:

- RowDefinition:行定义(定义网格中某一行的属性,如高度)
- Grid.ColumnDefinitions:列定义(定义网格中某一列的属性,如宽度)
可以数一下,有两个 <RowDefinitions> ,三个 <ColumnDefinitions>,所以这个网格规定了 2 行 3 列。
我们给 <RowDefinitions> (行定义)设置了 Height="*" (高度="占满剩余空间")以及 Height="Auto" (高度="高度自适应")属性,给 <ColumnDefinitions> (列定义)设置了 Width="*" (宽度="占满剩余空间")以及 Width="400" (宽度="400")属性。
你可能注意到了,宽度="400"是什么...?
其实这里代表宽度为
400epx,在 XAML 不用 px(像素)来表达布局尺寸和间距,而是使用 epx (有效像素),这个 epx 单位是一个虚拟的度量单位(而 px 是实际存在的物理的像素单位),它会自适应调整间距,只要你设置一个合适的值即可。
通过这张图也可以清晰看到网格的行列,以及每个格子。(顶部的一行是在 MainWindow.xaml 规定的,内容区域的两行是在 NotePage.xaml 规定的)

我们继续研究 NotePage.xaml 。可以发现界面是英文的,如何把每个元素的文本改为中文呢?

其实看一下也能理解这几个控件了,还是简单介绍一下吧:
- TextBox:文本框(用于接收用户输入或显示文本的控件,支持单行或多行)
- Button:按钮(用于触发操作或命令的控件,用户单击时执行逻辑)
通过对比,可以发现注释编辑器的标题是 Header="" 属性在控制,灰色提示文字是 PlaceholderText="" 属性在控制;两个按钮显示的文本是 Content="" 属性在控制

将其中的英文替换为中文,就像这样👇
<!-- 注释编辑器 -->
<TextBox x:Name="NoteEditor"
AcceptsReturn="True"
TextWrapping="Wrap"
PlaceholderText="请输入注释文字"
Header="新建笔记"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Width="400"
Grid.Column="1"/>
<!-- 操作按钮 -->
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="4"
Grid.Row="1" Grid.Column="1">
<!-- 保存按钮 -->
<Button Content="保存" Style="{StaticResource AccentButtonStyle}"/>
<!-- 删除按钮 -->
<Button Content="删除"/>
</StackPanel>保存后编译,看看效果:

你可能也注意到了,正常情况下页面是竖向排列,可是按钮区域却是横向排列子元素

观察按钮区域的代码,可以发现按钮在 <StackPanel Orientation="Horizontal" ... > ... </StackPanel> 中
<!-- 操作按钮 -->
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="4"
Grid.Row="1" Grid.Column="1">
<!-- 保存按钮 -->
<Button Content="保存" Style="{StaticResource AccentButtonStyle}"/>
<!-- 删除按钮 -->
<Button Content="删除"/>
</StackPanel>
- StackPanel:堆叠面板(将子元素放在同一行列,可水平或垂直排列,默认垂直排列)
左边的按钮是蓝色的,这是因为它添加了 Style="{StaticResource AccentButtonStyle}" 属性,这就很像 HTML 中的 Class 属性
实现点击按钮功能
打开 NotePage.xaml.cs 文件,我们要开始写后端啦
这个文件默认是这样的:
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 System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
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 WinUINotes
{
///
/// An empty page that can be used on its own or navigated to within a Frame.
///
public sealed partial class NotePage : Page
{
public NotePage()
{
InitializeComponent();
}
}
}
InitializeComponent();就是触发解析 XAML 的函数。触发后就可以把内容渲染到窗口中了XAML 本身只是一段文本,它不能直接渲染。必须由框架(WinUI / WPF / UWP)中的 XAML 解析器( C# 的函数实现)来读取、解析
如果学过类,那么可以轻松理解这里的知识点。在这里简单说一下:
类其实就是模板,类可以定义很多成员函数,每个成员函数都可以实现一个单独小功能,放到一个类里面就可以完整实现一个功能(就像Python的一个库那样子),这样就可以把程序的每一个部分分开维护,并且大大减少了代码量,通过调用自己写好的成员函数即可实现一个功能。(在C#中,所有代码必须属于类/结构,函数也是这样的,这就是面向对象编程)
之所以叫做成员函数,因为在类里面写的函数就是类的其中一员,字面意思。也可以在类定义成员变量,这样这些变量可以通过类来访问
在类里面可以给每个成员声明 public(公共)或者 private(私有),私有的成员变量/函数只能被本类的成员调用;相反,公共的成员不再限于在本类调用,其他类的成员也可以调用
刚刚说了,类只是模板。可以通过构造将类变成对象,的每个对象都是独立的
例如:定义一个类,叫做笔记页面
public class 笔记页面模板 { ...很多与页面相关的功能写在里面了 } 然后通过 new 创建(构造)一个页面(对象)
笔记页面模板 第一页 = new 笔记页面模板();不知道你理解了吗(doge)
声明用到的函数
将下面的变量声明代码添加到 NotePage 类中
private StorageFolder storageFolder = ApplicationData.Current.LocalFolder;
private StorageFile? noteFile = null;
private string fileName = "note.txt";就是放到这里:
public sealed partial class NotePage : Page
{
// ↓ 添加了这些内容 ↓
private StorageFolder storageFolder = ApplicationData.Current.LocalFolder;
private StorageFile? noteFile = null;
private string fileName = "note.txt";
// ↑ 添加了这些内容 ↑
public NotePage()
{
InitializeComponent();
}
}第 1 行:
private StorageFolder storageFolder = ApplicationData.Current.LocalFolder;定义了一个
private类内部变量,数据类型为StorageFolder,变量名为storageFolder,值为LocalFolder这个对象。用来获取本地数据存储根目录,保存文件时会用到第 2 行:
private StorageFile? noteFile = null;定义了一个
private类内部变量,数据类型为StorageFolder,?表示允许为null(空值),变量名为noteFile,值为null。用来临时存文件的,之后会用到第 3 行:
private string fileName = "note.txt";定义了一个
private类内部变量,数据类型为string,变量名为fileName(嗯对,就是字面意思),值为 "note.txt"。就是字面意思,定义一个文件名变量(在C#中字符串必须双引号(单引号是字符))
添加加载文件事件函数
将下面的函数声明代码添加到 NotePage 类中
public NotePage()
{
this.InitializeComponent();
// ↓ 之前的内容是存在的,只需要添加这一行即可 ↓
Loaded += NotePage_Loaded;
// ↑ 之前的内容是存在的,只需要添加这一行即可 ↑
}
// ↓ 添加事件处理程序函数 ↓
private async void NotePage_Loaded(object sender, RoutedEventArgs e)
{
noteFile = (StorageFile)await storageFolder.TryGetItemAsync(fileName);
if (noteFile is not null)
{
NoteEditor.Text = await FileIO.ReadTextAsync(noteFile);
}
}这段代码的作用是在笔记页面(NotePage)加载完成后,自动从本地存储中读取一个 fileName 文本文件(对应之前写的 note.txt ),并将其内容显示在页面上的文本框(NoteEditor)的 Text 属性中
Loaded += NotePage_Loaded; 当页面完成布局并准备就绪时,会自动触发 Loaded 事件,从而执行这个 NotePage_Loaded 函数
可以注意到在声明成员函数(
private async void NotePage_Loaded(object sender, RoutedEventArgs e))时使用了async关键字
async:在后台执行操作,也就是异步处理。这样,在读取文件期间,页面可以正常显示、用户可以进行滚动等操作,UI 界面不会出现“卡死”现象
await:等待本行代码异步操作完成,然后继续执行下面的代码
添加保存和删除按钮事件处理函数
回到 NotePage.xaml 文件
把 Click="" 添加到保存按钮中,此时会提示自动补全

点击 <新建事件处理程序> 选项,此时 Visual Studio 会自动帮我们创建好 Button_Click 函数

修改后的代码看起来就是这样的:
<!-- 保存按钮新增了属性 Click="Button_Click" -->
<Button Content="保存" Style="{StaticResource AccentButtonStyle}" Click="Button_Click"/>再回到 NotePage.xaml.cs 文件,可以看到在我们的 NotePage 类下方出现了成员函数 private void Button_Click(object sender, RoutedEventArgs e)

Button_Click 函数名是自动生成的,我们需要改一个函数名
在 Button_Click 将光标移到函数名之前,就像这样:

输入将函数名称改为 saveButton_Click。这个函数可能在任何位置被调用,所以我们不仅需要修改这里,还需要修改
Visual Studio 可以同步修改所有位置的该函数名,只需要将鼠标移到函数名中,在弹出的提示框中点击显示可能的修补程序

选择 将"Button_Click"重命名为"saveButton_Click” 选项即可

回到 NotePage.xaml 文件中再次查看,可以看到成功被修改

再次重复对 “删除 ”按钮重复这些步骤,创建好删除按钮的成员函数

现在我们就可以写代码实现保存和删除笔记文件的功能了
在 SaveButton_Click 成员函数中添加以下代码实现保存文件的功能:
private void saveButton_Click(object sender, RoutedEventArgs e)
{
if (noteFile is null)
{
noteFile = await storageFolder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);
}
await FileIO.WriteTextAsync(noteFile, NoteEditor.Text);
}粘贴进去之后 Visual Studio 直接就报错了,可以看到是异步处理的问题。回顾之前学习的内容,函数如果需要调用异步 API,声明函数时需要添加 async 关键字


将 async 添加到函数的声明中,这个报错就解决了,就像这样:
private async void saveButton_Click(object sender, RoutedEventArgs e)在 deleteButton_Click 成员函数中添加以下代码实现删除文件的功能, 还需要注意在声明函数中添加 async 关键字
private async void deleteButton_Click(object sender, RoutedEventArgs e)
{
if (noteFile is not null)
{
await noteFile.DeleteAsync();
noteFile = null;
NoteEditor.Text = string.Empty;
}
}最终的 NotePage.xaml.cs 文件中的 NotePage 类就是这样的了👇
public sealed partial class NotePage : Page
{
// ↓ 添加了这些内容 ↓
private StorageFolder storageFolder = ApplicationData.Current.LocalFolder;
private StorageFile? noteFile = null;
private string fileName = "note.txt";
// ↑ 添加了这些内容 ↑
public NotePage()
{
this.InitializeComponent();
// ↓ 之前的内容是存在的,只需要添加这一行即可 ↓
Loaded += NotePage_Loaded;
// ↑ 之前的内容是存在的,只需要添加这一行即可 ↑
}
// ↓ 添加事件处理程序函数 ↓
private async void NotePage_Loaded(object sender, RoutedEventArgs e)
{
noteFile = (StorageFile)await storageFolder.TryGetItemAsync(fileName);
if (noteFile is not null)
{
NoteEditor.Text = await FileIO.ReadTextAsync(noteFile);
}
}
private async void saveButton_Click(object sender, RoutedEventArgs e)
{
if (noteFile is null)
{
noteFile = await storageFolder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);
}
await FileIO.WriteTextAsync(noteFile, NoteEditor.Text);
}
private async void deleteButton_Click(object sender, RoutedEventArgs e)
{
if (noteFile is not null)
{
await noteFile.DeleteAsync();
noteFile = null;
NoteEditor.Text = string.Empty;
}
}
}保存,编译,查看效果:

恭喜恭喜可以再休息一下了捏(好厉害
重构项目结构
接下来重构现在的代码!!!
参考教程原文中的内容就是这样安排的。其实就是按照开发规范,将XAML页面以及对应的.cs文件放到页面文件夹,将页面的数据放到另一个文件夹。
如果你想看原文,可以展开看看
在本教程的前面步骤中,你向project添加了一个新页面,允许用户保存、编辑或删除单个笔记。 但是,由于应用需要处理多个笔记,因此需要添加另一个页面来显示所有备注(调用它
AllNotesPage)。 通过此页面,用户可以选择要在编辑器页中打开的备注,以便用户可以查看、编辑或删除它。 它还应该允许用户创建新笔记。若要实现此目的,
AllNotesPage需要具有笔记集合和显示集合的方法。 这是应用遇到问题的地方,因为笔记数据紧密绑定到NotePage文件。 在AllNotesPage中,只需显示列表或其他集合视图中的所有笔记,其中包含有关每个笔记的信息,如创建日期和文本预览。 由于笔记文本紧密绑定到TextBox控件,因此无法做到这一点。重构现有代码以将模型与视图分开。 接下来的几个步骤将组织代码,以便分别定义视图和模型。
按照下图的提示,在解决方案资源管理器中新建两个文件夹
分别命名为 Models 和 Views, Models 存放模块化的后端代码,Views 存放不同页面的前端代码

迁移前端
将 NotePage.xaml 拖到 Views 文件夹里
如果看到有关移动操作可能需要很长时间的警告,直接点击 是 即可
会看到下图这个弹窗,我们点击 否

点击确定按钮

命名空间可以再多一个分类层级,可以有两个不同命名空间里的
NotePage类,互不干扰,需要时使用using即可引入命名空间。默认命名空间就是 “项目根命名空间 + 文件夹路径(用点分隔)”,所以是WinUINotes.Views
现在 NotePage.xaml 已经移动到 Views 文件夹,接着需要更新命名空间
命名空间原本为 WinUINotes,现在要改为 WinUINotes.Views
打开 NotePage.xaml.cs,将找到下图这一行代码,改为 WinUINotes.Views:

namespace WinUINotes.Views打开 NotePage.xaml ,将找到下图这一行代码, 将 x:Class 属性的值改为 WinUINotes.Views.NotePage:

x:Class="WinUINotes.Views.NotePage"
x:Class:后端代码绑定
"WinUINotes.Views.NotePage"就是这个 XAML 文件的后端(C#)位于WinUINotes.Views命名空间下,类名为NotePage
现在我们只修改了命名空间的映射(绑定后端文件),XAML 文件中还有其他代码引用了旧的命名空间,我们需要为其修改。
往下排查,还有这一行代码存在旧的命名空间,我们还需要修改命名空间的引用(将命名空间声明到本地),现在这行代码无效了。

xmlns:local: 命名空间引用,这个需要拆分来理解👇
xmlns本身是 XML 的关键字,表示“声明一个命名空间”:分隔符,前面是关键字,后面是本地名称,在一起就变成了完整的 XML 属性。local给这个命名空间起一个别名(本地名称)"using:WinUINotes" : 表示引用命名空间
之后在 XAML 里,用
local:某个命名空间就表示“去using:WinUINotes里找这个命名空间
在本行命名空间的下方添加这个新的命名空间引用,将本地的 views 命名空间指向 WinUINotes.Views 👇
xmlns:views="using:WinUINotes.Views"修改后看起来就是这样的👇

新的写好了,
那旧的呢?)其实旧的只是无效了,不删也不会报错
同时也要在 MainWindow.xaml 中的 xmlns:local 属性下添加这个新的命名空间引用。修改后是这样的👇

翻到页面底部,Frame 元素的 SourcePageType 属性还在引用旧的命名空间

修改这行代码为:
现在看起来是这样的👇

现在再保存编译程序就不会有任何报错了:)

迁移后端
按照下图的提示,在解决方案资源管理器中右键点击 Models 文件夹,点击"添加 - 类"

将名称改为 Note.cs,点击添加按钮

打开 Note.cs 文件
这文件默认是这样的:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WinUINotes.Models
{
class Note
{
}
}将文件全部内容替换为以下代码:
using System;
using System.Threading.Tasks;
using Windows.Storage;
namespace WinUINotes.Models
{
public class Note
{
private StorageFolder storageFolder = ApplicationData.Current.LocalFolder;
// 属性
public string Filename { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
public DateTime Date { get; set; } = DateTime.Now;
public Note()
{
Filename = "notes" + DateTime.Now.ToBinary().ToString() + ".txt";
}
public async Task SaveAsync()
{
// 将笔记保存到文件
StorageFile noteFile = (StorageFile)await storageFolder.TryGetItemAsync(Filename);
if (noteFile is null)
{
noteFile = await storageFolder.CreateFileAsync(Filename, CreationCollisionOption.ReplaceExisting);
}
await FileIO.WriteTextAsync(noteFile, Text);
}
public async Task DeleteAsync()
{
// 从文件系统中删除该笔记
StorageFile noteFile = (StorageFile)await storageFolder.TryGetItemAsync(Filename);
if (noteFile is not null)
{
await noteFile.DeleteAsync();
}
}
}
}属性 (Property):属性是类的成员,看起来很像在声明变量。它的主要作用是存放类的私有数据,但升级了。可以通过其名称后跟着自动属性
{ get; set; }判断为属性。它可以实现复杂逻辑,例如在读写变量时实现各种功能,总之很强大就对了
欸你会发现 Filename 和 Text 成员变量变成了 public (公共)属性,并且添加了一个新属性 Date (存放本地日期时间)。现在,我们的App会支持多个笔记,笔记会保存到不同的文件中,所以 StorageFile 引用文件没了,现在变成了每次保存删除都重新打开一次指定的笔记文件,这样可以实现修改不同的笔记文件
为了实现每个笔记文件都是不同且唯一的,我们在构造函数中设置 Filename 属性。 可以使用 DateTime.ToBinary (64位整数时间)使文件名变成唯一的。 生成的文件名如下所示: notes-8584626598945870392.txt
具体可以看看全部代码的注释,自己理解👇
// 引入基础系统功能(如字符串、日期等)
using System;
// 引入异步任务支持(用于文件操作等耗时任务)
using System.Threading.Tasks;
// 引入Windows存储API(用于访问应用本地存储)
using Windows.Storage;
namespace WinUINotes.Models
{
public class Note
{
// 存储笔记文件的位置
private StorageFolder storageFolder = ApplicationData.Current.LocalFolder;
// 以下为属性
public string Filename { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
public DateTime Date { get; set; } = DateTime.Now;
// 生成唯一文件名:使用"notes"前缀 + 当前时间的二进制表示 + ".txt"后缀
// DateTime.Now.ToBinary()将当前时间转换为64位整数,确保文件名唯一性
public Note()
{
Filename = "notes" + DateTime.Now.ToBinary().ToString() + ".txt";
}
// 保存笔记文件
public async Task SaveAsync()
{
// 尝试获取已存在的笔记文件(如果存在)
StorageFile noteFile = (StorageFile)await storageFolder.TryGetItemAsync(Filename);
// 如果文件不存在,则创建新文件
if (noteFile is null)
{
// CreateFileAsync就是创建新文件,ReplaceExisting选项表示如果已存在则替换
noteFile = await storageFolder.CreateFileAsync(Filename, CreationCollisionOption.ReplaceExisting);
}
// 将笔记文本内容写入文件
await FileIO.WriteTextAsync(noteFile, Text);
}
// 删除该笔记文件
public async Task DeleteAsync()
{
// 尝试获取要删除的笔记文件
StorageFile noteFile = (StorageFile)await storageFolder.TryGetItemAsync(Filename);
// 如果文件存在,则执行删除操作
if (noteFile is not null)
{
await noteFile.DeleteAsync();
}
}
}
}删掉历史遗留代码
还记得我们之前在 NotePage.xaml.cs 实现了模块的内容了吗?现在我们需要把历史遗留代码清理掉
打开 Views 文件夹中的 NotePage.xaml.cs 文件
如下图所示,在页面顶部的最后一个 using 语句之后,添加一个新的 using 语句用来引用 Models 中的代码
using WinUINotes.Models;
把这些成员变量删掉

在原本成员变量所在的位置,添加一个名为 Note 的 noteModel 对象,现在交给 Note 模型来处理,页面只负责显示数据
private Note? noteModel;
删除整个 NotePage_Loaded 成员函数,因为现在数据会通过 noteModel 自动同步(绑定)到页面中,不需要手动加载了

将 saveButton_Click 成员函数中的所有代码更改为以下内容:
private async void saveButton_Click(object sender, RoutedEventArgs e)
{
if (noteModel is not null)
{
await noteModel.SaveAsync();
}
}将 deleteButton_Click 成员函数中的所有代码更改为以下内容:
private async void deleteButton_Click(object sender, RoutedEventArgs e)
{
if (noteModel is not null)
{
await noteModel.DeleteAsync();
}
}就是全部交给 Note.cs 中的函数去处理了,这里只是调用一下,起到中转的作用(
数据绑定
数据绑定就是在界面中显示数据,还可以让数据保持同步,自动更新。
打开 Views\NotePage.xaml 文件
将 TextBox 元素代码替换为:
<TextBox x:Name="NoteEditor"
Text="{x:Bind noteModel.Text, Mode=TwoWay}"
AcceptsReturn="True"
TextWrapping="Wrap"
PlaceholderText="请输入注释文字"
Header="{x:Bind noteModel.Date.ToString()}"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Width="400"
Grid.Column="1"/>
其实就是新增了 TextBox 的 Text 属性,更新了 Header 属性,并且用到了 {x:Bind} (编译时绑定)
Text="{x:Bind noteModel.Text, Mode=TwoWay}"
x:Bind noteModel.Text:将TextBox.Text与后端的noteModel.Text绑定起来Mode=TwoWay:双向同步。用户在输入框里打字 →noteModel.Text自动更新;后端修改noteModel.Text→ 界面显示也会变
这样就简单多了,前后端数据自动保持一致
Header="{x:Bind noteModel.Date.ToString()}"
- 直接在绑定路径里调用
.ToString()方法,最终Header显示的就是格式化的日期字符串- 热知识:
{x:Bind}的默认模式是OneTime(只在页面加载时赋值一次)
原本 Header 属性在这里负责显示上方的静态标题“新建笔记”,就像这样:

修改后标题动态显示当前笔记文件的创建日期
小结:MVVM
控件树就是字面意思,它们的关系像一棵树一样
Page (爷爷) └── Grid (爸爸) ├── TextBox (大儿子) └── Button (小儿子)这种 一层套一层、单线继承 的结构,在计算机里就叫 树(Tree)
回顾一下数据绑定相关知识,我们用到了 Model-View-ViewModel(MVVM)这种 UI 体系结构设计模式,简单来说就是前后端分离。在 Web 开发中,看到的界面就是前端,在服务器的计算就是后端;而在 WinUI3 开发中,我们只做客户端,所以本文中所提到的前后端便是控件树(视图)与控件的状态与行为(逻辑)
// 吃完饭见bye


Comments NOTHING