WPF简介和环境搭建

WPF(Windows Presentation Foundation)是微软在2006年推出的新一代Windows桌面图形用户界面开发技术,它以DirectX硬件加速渲染、XAML声明式界面描述和数据驱动为核心,彻底改变了传统Windows桌面程序的开发方式。与Winform相比,WPF在界面表现力、样式定制能力和数据绑定方面有着质的飞跃,至今仍是Windows下桌面软件开发的主流框架之一。
WPF最早随.NET Framework 3.0发布,原生支持Windows Vista并能向下兼容支持.NET Framework 3.0的Windows操作系统版本,不过随着时代的发展,.NET Core 3.0及后续版本.NET 6/8/9对WPF的正式支持,WPF目前主要面向Windows 10/11进行现代UI样式的迭代与优化。本系列笔记基于.NET 8和Windows 11操作系统对WPF框架和XAML进行介绍。
WPF的核心特性
WPF并不是Winform的简单升级,而是一套全新的开发范式。WPF的核心特征可以用三个关键词概括,DirectX渲染、XAML声明式UI和数据驱动。
DirectX渲染:众所周知,Winform基于古老的GDI+绘制界面,它本质上是基于CPU的对控件逐像素绘制,而WPF基于DirectX做硬件加速渲染,界面以矢量方式描述,天然支持任意缩放不失真、透明度、动画和3D效果,高DPI屏幕上的表现也远好于Winform。
XAML声明式UI:Winform的界面是设计器生成的大段C#代码.Designer.cs,这些代码可读性差,且一旦手动修改就容易造成设计器崩溃,WPF则使用XML格式的XAML语言声明式地描述界面,界面结构和外观一目了然,且XAML界面代码与业务逻辑天然解耦,更符合工程直觉。
数据驱动:WPF把数据绑定、样式、模板作为框架的一等公民,界面可以围绕数据自动组织,这也是MVVM等现代架构模式在WPF中流行的原因。
创建第一个WPF工程
打开Visual Studio创建新项目,这里我们打算基于最新的.NET 8来学习WPF,因此选择「WPF 应用程序」。如果你打算使用旧版.NET Framework,则需要选择「WPF 应用(.NET Framework)」项目模板。

项目创建完成后,Visual Studio会打开MainWindow.xaml的设计器界面。WPF设计器分为上下两部分:上半部分是可视化预览,下半部分是XAML代码编辑区,两边是双向同步的,我们既可以从工具箱拖拽控件,也可以直接手写XAML,实际开发中手写XAML往往更高效。

Visual Studio为我们生成的工程模板大致会是如下目录结构。
Gacfox.Demo
|_ bin # 编译输出目录
|_ obj # 编译中间文件目录
│_ App.xaml # 应用程序入口配置,用于定义应用程序级别的共享资源、全局启动设置等
│_ App.xaml.cs # 应用程序后台逻辑,常用于处理应用程序生命周期事件
│_ AssemblyInfo.cs # 程序集信息文件,用于配置生成程序集的元数据
│_ MainWindow.xaml # 主窗口界面XAML设计文件,定义主界面的UI布局和控件
│_ MainWindow.xaml.cs # 主窗口交互逻辑
│_ Gacfox.Demo.csproj # MSBuild项目配置文件
│_ Gacfox.Demo.csproj.user # 用户个性化配置,不要将其提交到版本库
│_ Gacfox.Demo.sln # Visual Studio解决方案文件
WPF中,通常每个窗口由一对文件组成,类似MainWindow.xaml(界面)和MainWindow.xaml.cs(逻辑),后者被称为“代码隐藏类”,它们两个的关系类似Winform中Form1.Designer.cs和Form1.cs的关系,只不过设计器生成的代码不再以C#源文件形式出现在工程里,而是在编译时由XAML编译生成。
此外,我们还可以发现工程中竟然没有Program.cs入口文件,这是因为WPF程序比较特别,它的入口由App.xaml承担,编译时SDK会根据它自动生成Main()函数。
App.xaml是整个应用程序的入口定义,默认内容如下。
App.xaml
<Application x:Class="Gacfox.Demo.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Gacfox.Demo"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
代码中,最关键的是StartupUri="MainWindow.xaml",它指定程序启动时自动创建并显示哪个窗口,这相当于Winform程序中Application.Run(new Form1())的作用。App类继承自Application,WPF运行时内部同样维护着一个消息循环,持续分发用户输入和系统消息,这一点与Winform的消息循环在本质上是相通的,只是我们一般不需要直接接触它。
如果需要在程序启动时执行一些初始化逻辑,我们也可以重写App类的OnStartup方法,此时通常会把StartupUri删掉,改为自己控制窗口的创建。
App.xaml.cs
using System.Windows;
namespace Gacfox.Demo;
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
MainWindow mainWindow = new();
mainWindow.Show();
}
}
MainWindow.xaml描述窗口的界面,默认只有一个空白的Grid面板。
MainWindow.xaml
<Window x:Class="Gacfox.Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Gacfox.Demo"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
</Grid>
</Window>
根元素Window上的x:Class="Gacfox.Demo.MainWindow"非常关键,它指明这份XAML与哪个类对应。编译时,XAML编译器会为这份XAML生成一个partial class,其中包含InitializeComponent()方法以及所有带名字的控件对应的字段,它与MainWindow.xaml.cs中我们自己写的partial类合并成一个完整的MainWindow类。
MainWindow.xaml.cs
using System.Windows;
namespace Gacfox.Demo;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
这个机制和Winform的Form1.cs + Form1.Designer.cs是完全相同的思路,InitializeComponent()负责把界面构建出来,我们的事件处理函数写在partial类中,区别只在于Winform的设计器代码是一个看得见摸得着的C#文件,而WPF的这部分代码由XAML编译器在背后生成,我们要修改界面时直接编辑XAML即可。
最后我们看看.csproj文件,新版.NET的项目描述文件非常简洁,其中<UseWPF>true</UseWPF>声明这是个WPF应用程序或类库,添加这个标识后,MSBuild才会触发WPF的专有编译流程,包括启动XAML编译器、引入WPF专用代码生成与类型支持等功能。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
</Project>
第一个小例子
现在我们在XAML中写一个简单界面,包含输入框和一个按钮,点击按钮弹出问候语。我们编辑MainWindow.xaml,写入以下内容。
<Window x:Class="Gacfox.Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Gacfox.Demo"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel Margin="20">
<TextBox x:Name="nameTextBox" Height="30" />
<Button Content="问好" Height="30" Margin="0,10,0,0"
Click="helloButton_Click" />
</StackPanel>
</Window>
注意两点:
x:Name给控件起了名字,编译后它就是代码隐藏类中的成员变量名Click="helloButton_Click"把事件挂接到了C#代码中的方法上
然后我们在MainWindow.xaml.cs中编写事件处理逻辑。
using System.Windows;
namespace Gacfox.Demo;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void helloButton_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show($"你好,{nameTextBox.Text}!");
}
}
可以看到事件处理函数的签名与Winform略有不同,第二个参数是RoutedEventArgs而非EventArgs,这是WPF路由事件机制的体现,后文章节会详细介绍。
点击运行,效果如下。
