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.csForm1.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>

注意两点:

  1. x:Name给控件起了名字,编译后它就是代码隐藏类中的成员变量名
  2. 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路由事件机制的体现,后文章节会详细介绍。

点击运行,效果如下。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。