云计算百科
云计算领域专业知识百科平台

WPF 详细入门教程

WPF 详细入门教程(基于 .NET 8)

面向零基础或从 WinForms 转过来的开发者,内容由浅入深,每个知识点都配可运行的小案例。

版本说明:本文全部示例基于 .NET 8 + WPF 编写,使用 C# 12,可空引用类型(Nullable)已开启。文中涉及 string? 标注等语法均以 .NET 8 为准。

版本边界提示:

  • 本文不使用 Grid.ColumnDefinitions="…" / RowDefinitions="…" 字符串简写、StackPanel.Spacing 等语法,因为它们是 .NET 9 才加入 WPF 的新特性,在 .NET 8 上会编译/解析失败。
  • 如果你使用 .NET 9+,可以额外了解这些简写(见 5.7 节)。

目录

  • WPF 是什么
  • 环境准备与第一个程序
  • 项目结构解析
  • XAML 基础语法
  • 布局系统
  • 常用控件
  • 事件处理
  • 数据绑定
  • 样式、触发器与模板
  • 资源字典
  • MVVM 模式
  • 依赖属性与附加属性
  • 综合案例:待办事项
  • 动画入门
  • 学习建议与常见坑

  • 一、WPF 是什么

    WPF(Windows Presentation Foundation)是微软推出的桌面 UI 框架,运行在 .NET 之上。

    与 WinForms 的核心区别

    对比项WinFormsWPF
    UI 描述 C# 代码拖控件 XAML 声明式描述
    布局 绝对坐标(Left/Top) 布局容器自动排列
    渲染 GDI+ DirectX(矢量、硬件加速)
    数据展示 手动赋值 label.Text = … 数据绑定自动同步
    外观定制 困难 样式/模板任意重写
    分辨率 缩放易模糊 矢量缩放清晰

    一句话理解 WPF 的核心思想:UI 和数据分离,用 XAML 描述“长什么样”,用 C# 描述“做什么事”,两者通过数据绑定连接。


    二、环境准备与第一个程序

    1. 安装

    • 下载 Visual Studio 2022(17.8 或更高版本,社区版免费)
    • 安装时勾选工作负载:.NET 桌面开发
    • 确认已安装 .NET 8 SDK

    2. 创建项目

  • 打开 VS → 创建新项目
  • 搜索并选择 WPF 应用程序(描述里会写 “C# / .NET”)
  • 项目名:WpfDemo → 下一步
  • 框架选择 .NET 8.0 (长期支持) → 创建
  • ⚠️ 不要选 “WPF 应用 (.NET Framework)”,那是旧的 .NET Framework 版本,语法和项目结构略有不同。

    3. 运行

    直接按 F5,会看到一个空白窗口。恭喜,第一个 WPF 程序跑起来了。

    4. 添加一个按钮

    打开 MainWindow.xaml,把 <Grid> … </Grid> 部分改成:

    <Grid>
    <Button Content="点我" Width="120" Height="40"
    HorizontalAlignment="Center" VerticalAlignment="Center"
    Click="Button_Click"/>

    </Grid>

    然后在 MainWindow.xaml.cs 中添加事件方法:

    private void Button_Click(object sender, RoutedEventArgs e)
    {
    MessageBox.Show("你好,WPF!");
    }

    再按 F5,点击按钮就会弹窗。

    小技巧:在 XAML 中写 Click=" 后按 Tab 键,VS 会自动生成事件方法。


    三、项目结构解析

    创建项目后你会看到这些文件:

    WpfDemo/
    ├── App.xaml 程序入口,配置启动窗口和全局资源
    ├── App.xaml.cs App 的代码后置
    ├── MainWindow.xaml 主窗口的 UI 描述
    └── MainWindow.xaml.cs 主窗口的逻辑代码

    App.xaml

    <Application x:Class="WpfDemo.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="MainWindow.xaml">

    <Application.Resources>
    <!– 全局资源放这里 –>
    </Application.Resources>
    </Application>

    StartupUri 指定程序启动时显示哪个窗口。

    MainWindow.xaml

    <Window x:Class="WpfDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="450" Width="800">

    <Grid>
    <!– 内容区 –>
    </Grid>
    </Window>

    命名空间说明

    命名空间作用
    xmlns="…presentation" WPF 控件(Button、Grid 等)
    xmlns:x="…xaml" XAML 语言特性(x:Name、x:Class 等)
    xmlns:local="clr-namespace:WpfDemo" 当前项目中的自定义类型(自定义控件、转换器等),命名空间按实际项目名替换

    代码后置(Code-Behind):x:Class="WpfDemo.MainWindow" 把 XAML 和 MainWindow.xaml.cs 中的类关联起来,XAML 里 x:Name 定义的控件可以在 C# 中直接访问。

    .csproj 关键配置

    .NET 8 WPF 项目文件大致如下,Nullable 默认开启:

    <Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <UseWPF>true</UseWPF>
    </PropertyGroup>
    </Project>

    因为 Nullable 是开启的,后面写代码时字符串、委托等类型需要加 ? 标注,否则会有编译器警告。


    四、XAML 基础语法

    XAML 本质是 XML,用标签描述对象,用属性描述特征。

    4.1 属性赋值

    简单属性(字符串、数字):

    <Button Content="确定" Width="100"/>

    复杂属性(对象、集合)用“属性元素语法”:

    <Button Width="100">
    <Button.Content>
    <StackPanel Orientation="Horizontal">
    <TextBlock Text="图标"/>
    <TextBlock Text="文字"/>
    </StackPanel>
    </Button.Content>
    </Button>

    4.2 特殊字符转义

    字符转义
    < &lt;
    > &gt;
    & &amp;
    " &quot;

    4.3 x:Name 与 x:Class

    <TextBox x:Name="txtName" />

    // C# 中可直接使用
    string text = txtName.Text;

    4.4 常用标记扩展(Markup Extension)

    标记扩展用 { } 包裹,是 XAML 里“动态取值”的语法糖。

    标记扩展用途
    {Binding …} 数据绑定
    {StaticResource Key} 引用资源(加载时一次性查找)
    {DynamicResource Key} 引用资源(每次使用查找,可换肤)
    {x:Static Namespace.Type.Member} 引用静态字段/属性/常量
    {x:Type Namespace.Type} 引用类型对象
    {x:Null} 显式设置为 null
    {x:Array Type=…} 声明数组

    示例:

    <TextBlock Text="{x:Static System:Environment.MachineName}"
    Foreground="{StaticResource PrimaryBrush}"/>

    4.5 注释

    <!– 这是注释 –>


    五、布局系统

    WPF 布局的核心思想:父容器负责安排子元素的位置和大小,而不是子元素自己定坐标。

    5.1 Grid(最常用)

    用行和列划分区域:

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

    <Button Grid.Row="0" Grid.Column="0" Content="左上"/>
    <Button Grid.Row="0" Grid.Column="1" Content="右上"/>
    <Button Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Content="跨两列"/>
    <Button Grid.Row="1" Grid.Column="0" Grid.RowSpan="2" Content="跨两行"/>
    </Grid>

    Height/Width 的三种取值:

    • Auto:根据内容自动
    • *:按比例分配剩余空间(2* 表示占两份)
    • 固定值:如 100

    ⚠️ 注意:Grid.Row、Grid.Column、Grid.RowSpan、Grid.ColumnSpan 都是从 0 开始的索引。Grid.RowSpan 不要超出实际行数,否则会静默失效或布局异常。上面示例中 Grid.Row="1" Grid.RowSpan="2" 表示从第 1 行起跨 2 行(即第 1、2 行),共 3 行,正好用满。

    5.2 StackPanel

    按顺序堆叠,不换行:

    <StackPanel Orientation="Vertical">
    <Button Content="按钮1" Margin="0,0,0,8"/>
    <Button Content="按钮2" Margin="0,0,0,8"/>
    <Button Content="按钮3"/>
    </StackPanel>

    Orientation 可选 Vertical(默认)或 Horizontal。

    💡 关于 Spacing 属性:.NET 9 起 WPF 的 StackPanel 增加了 Spacing 属性,用于统一设置子元素间距。但 .NET 8 不支持,请使用 Margin(或用 Grid 的行列间距)实现相同效果。.NET 9+ 的新特性见 5.7 节。

    5.3 DockPanel

    贴边停靠,最后一个元素填满剩余空间:

    <DockPanel>
    <Button DockPanel.Dock="Top" Content="顶部"/>
    <Button DockPanel.Dock="Left" Content="左侧"/>
    <Button DockPanel.Dock="Bottom" Content="底部"/>
    <Button Content="填充剩余"/>
    </DockPanel>

    设置 LastChildFill="False" 可让最后一个也不填充。

    5.4 WrapPanel

    自动换行的流式布局,适合标签墙、图片列表:

    <WrapPanel>
    <Button Content="标签1" Margin="4"/>
    <Button Content="标签2" Margin="4"/>
    <!– 超出宽度会自动换行 –>
    </WrapPanel>

    5.5 Canvas

    绝对坐标定位,类似 WinForms:

    <Canvas>
    <Button Canvas.Left="50" Canvas.Top="30" Content="坐标(50,30)"/>
    </Canvas>

    5.6 布局选择建议

    需求推荐
    表单、复杂分区 Grid
    垂直/水平排列 StackPanel
    页面框架(头/侧/底/中) DockPanel
    自动换行列表 WrapPanel
    绘图、精确定位 Canvas

    5.7 .NET 9+ 新特性(选读)

    以下语法 .NET 8 不支持,仅在 .NET 9 及以上可用。如果你的项目已升级到 .NET 9,可以使用它们让 XAML 更简洁。

    Grid 行列简写:

    <Grid ColumnDefinitions="100,*" RowDefinitions="Auto,*,2*">
    <Button Grid.Row="0" Grid.Column="0" Content="左上"/>
    <Button Grid.Row="0" Grid.Column="1" Content="右上"/>
    </Grid>

    StackPanel.Spacing:

    <StackPanel Orientation="Vertical" Spacing="8">
    <Button Content="按钮1"/>
    <Button Content="按钮2"/>
    </StackPanel>

    .NET 8 项目请使用本文其他章节的完整写法。


    六、常用控件

    6.1 文本类

    <TextBlock Text="只读文本" FontSize="16" Foreground="Gray"/>
    <TextBox Text="可编辑文本" Width="200"/>
    <PasswordBox Width="200"/>
    <RichTextBox Height="100"/>

    6.2 按钮类

    <Button Content="普通按钮" Click="Button_Click"/>
    <RepeatButton Content="长按重复"/>
    <ToggleButton Content="开关"/>
    <CheckBox Content="复选框" IsChecked="True"/>
    <RadioButton Content="单选A" GroupName="g1" IsChecked="True"/>
    <RadioButton Content="单选B" GroupName="g1"/>

    6.3 列表类

    <ListBox x:Name="listBox" Height="150">
    <ListBoxItem>选项1</ListBoxItem>
    <ListBoxItem>选项2</ListBoxItem>
    </ListBox>

    <ComboBox x:Name="comboBox" Width="150">
    <ComboBoxItem>北京</ComboBoxItem>
    <ComboBoxItem>上海</ComboBoxItem>
    </ComboBox>

    6.4 数值与进度

    <Slider Minimum="0" Maximum="100" Value="30" Width="200"/>
    <ProgressBar Value="60" Maximum="100" Width="200" Height="20"/>

    6.5 图片

    <Image Source="/Images/logo.png" Width="100" Height="100" Stretch="Uniform"/>

    图片需在项目中设置“生成操作”为 Resource。等价的 pack URI 写法为:

    <Image Source="pack://application:,,,/Images/logo.png"/>

    6.6 常用通用属性

    属性说明
    Width / Height 宽高
    Margin 外边距(左,上,右,下)
    Padding 内边距
    HorizontalAlignment Left / Center / Right / Stretch
    VerticalAlignment Top / Center / Bottom / Stretch
    Background / Foreground 背景 / 前景色
    FontSize / FontFamily 字体
    IsEnabled 是否可用
    Visibility Visible / Hidden / Collapsed
    ToolTip 悬停提示

    Hidden 与 Collapsed 的区别:Hidden 占位但不可见,Collapsed 不占位。


    七、事件处理

    7.1 在 XAML 中绑定事件

    <Button Content="点击" Click="MyButton_Click"/>

    private void MyButton_Click(object sender, RoutedEventArgs e)
    {
    var btn = (Button)sender;
    btn.Content = "已点击";
    }

    7.2 在代码中绑定事件

    var btn = new Button { Content = "动态按钮" };
    btn.Click += (s, e) => MessageBox.Show("动态创建并绑定");
    rootPanel.Children.Add(btn);

    7.3 常用事件

    控件常用事件
    Button Click
    TextBox TextChanged、GotFocus、LostFocus、KeyDown
    Window Loaded、Closing、Closed
    ListBox SelectionChanged
    CheckBox Checked、Unchecked
    通用 MouseEnter、MouseLeave、MouseDown

    7.4 路由事件(简要了解)

    WPF 事件是路由事件,可以在父容器上统一处理子元素的事件:

    <StackPanel Button.Click="AnyButton_Click">
    <Button Content="A"/>
    <Button Content="B"/>
    </StackPanel>

    private void AnyButton_Click(object sender, RoutedEventArgs e)
    {
    // sender 是 StackPanel,e.OriginalSource 才是真正被点的按钮
    var btn = e.OriginalSource as Button;
    MessageBox.Show($"点击了 {btn?.Content}");

    // 若不想让事件继续冒泡到更上层,可设置:
    // e.Handled = true;
    }


    八、数据绑定(重点)

    数据绑定是 WPF 的灵魂,掌握它就掌握了 WPF。

    8.1 最简单的绑定:元素到元素

    <StackPanel Margin="20">
    <Slider x:Name="slider" Minimum="0" Maximum="100" Value="30"/>
    <TextBlock Text="{Binding ElementName=slider, Path=Value}"
    FontSize="24" Margin="0,10"/>

    </StackPanel>

    拖动滑块,文本自动更新。

    语法解析:

    • ElementName=slider:绑定源是名为 slider 的元素
    • Path=Value:取它的 Value 属性

    小提示:不同控件属性的 UpdateSourceTrigger 默认值不同,直接影响绑定回写源的时机:

    目标属性默认 UpdateSourceTrigger说明
    TextBox.Text LostFocus 失去焦点才回写源,要实时需显式设为 PropertyChanged
    Slider.Value PropertyChanged 拖动时实时回写
    CheckBox.IsChecked PropertyChanged 勾选时实时回写
    RadioButton.IsChecked PropertyChanged 选中时实时回写
    ComboBox.SelectedItem PropertyChanged 选择时实时回写

    因此,只有 TextBox.Text 这类默认 LostFocus 的属性需要显式加 UpdateSourceTrigger=PropertyChanged;Slider.Value 等默认就是实时更新,无需额外设置。

    8.2 绑定到 DataContext(最常用)

    DataContext 是控件的默认数据源,会沿可视化树向下传递。

    XAML:

    <StackPanel Margin="20">
    <TextBox Text="{Binding Name}" Width="200"/>
    <TextBlock Text="{Binding Name}" FontSize="20"/>
    <TextBlock Text="{Binding Age}"/>
    </StackPanel>

    C# 代码后置:

    public partial class MainWindow : Window
    {
    public MainWindow()
    {
    InitializeComponent();
    DataContext = new Person { Name = "张三", Age = 25 };
    }
    }

    public class Person
    {
    public string Name { get; set; } = "";
    public int Age { get; set; }
    }

    注意 .NET 8 开启了 Nullable,string 属性建议初始化为空字符串或标注为 string?,否则会有 CS8618 警告。

    8.3 双向绑定

    默认 TextBox.Text 是双向的,但更新时机是失去焦点时。改成实时更新:

    <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"/>

    Mode 可选值:

    Mode说明
    OneWay 源 → 目标(默认多数场景)
    TwoWay 双向(TextBox、CheckBox 等默认)
    OneTime 只绑定一次
    OneWayToSource 目标 → 源

    8.4 INotifyPropertyChanged(数据变化通知)

    上面的 Person 如果在代码里改了 Name,界面不会更新。要实现属性变更通知,需实现 INotifyPropertyChanged:

    using System.ComponentModel;
    using System.Runtime.CompilerServices;

    public class Person : INotifyPropertyChanged
    {
    private string _name = "";
    public string Name
    {
    get => _name;
    set
    {
    if (_name == value) return;
    _name = value;
    OnPropertyChanged();
    }
    }

    private int _age;
    public int Age
    {
    get => _age;
    set { _age = value; OnPropertyChanged(); }
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? name = null)
    => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }

    现在代码里 person.Name = "李四" 界面也会跟着变。

    注意:PropertyChanged 事件在 .NET 8 下需要写成 PropertyChangedEventHandler?(可空),OnPropertyChanged 的参数也建议标 string?,以符合 Nullable 约定。

    8.5 集合绑定

    用 ObservableCollection<T> 代替 List<T>,集合增删会自动刷新 UI:

    public ObservableCollection<string> Items { get; }
    = new ObservableCollection<string>();

    <ListBox ItemsSource="{Binding Items}"/>

    Items.Add("新项目"); // ListBox 自动多一行
    Items.RemoveAt(0); // 自动少一行

    8.6 值转换器(Converter)

    当源类型和目标类型不一致时(如 bool → Visibility),需要转换器。

    💡 提示:WPF 自带 BooleanToVisibilityConverter,日常 True→Visible / False→Collapsed 的场景直接用它即可,无需自己写。只有需要反向逻辑(True→Collapsed)或其它复杂规则时才自定义。注意它对 null 也会处理为 Collapsed。

    系统自带用法:

    <Window.Resources>
    <BooleanToVisibilityConverter x:Key="BoolToVis"/>
    </Window.Resources>

    <TextBlock Text="我出现了!"
    Visibility="{Binding IsChecked, ElementName=chk,
    Converter={StaticResource BoolToVis}}"
    />

    自定义转换器(例如反向),注意 .NET 8 Nullable 下接口签名带 ?:

    using System.Globalization;
    using System.Windows;
    using System.Windows.Data;

    public class InverseBoolToVisibilityConverter : IValueConverter
    {
    public object? Convert(object? value, Type targetType,
    object? parameter, CultureInfo culture)
    {
    return (value is bool b && b) ? Visibility.Collapsed : Visibility.Visible;
    }

    public object? ConvertBack(object? value, Type targetType,
    object? parameter, CultureInfo culture)
    {
    return value is Visibility v && v == Visibility.Collapsed;
    }
    }

    使用(注意在根元素声明 xmlns:local="clr-namespace:WpfDemo"):

    <Window x:Class="WpfDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfDemo"
    Title="MainWindow" Height="450" Width="800">

    <Window.Resources>
    <local:InverseBoolToVisibilityConverter x:Key="InvBoolToVis"/>
    </Window.Resources>

    <StackPanel>
    <CheckBox x:Name="chk" Content="隐藏下方文本"/>
    <TextBlock Text="我被隐藏了!"
    Visibility="{Binding ElementName=chk, Path=IsChecked,
    Converter={StaticResource InvBoolToVis}}"
    />

    </StackPanel>
    </Window>

    8.7 常用 Binding 参数速查

    参数说明
    Path 绑定属性路径,支持 A.B.C
    ElementName 绑定到指定元素
    RelativeSource 相对源,如 AncestorType=Window
    Source 直接指定源对象
    Mode 绑定方向
    UpdateSourceTrigger 更新时机(Default/PropertyChanged/LostFocus)
    Converter 值转换器
    StringFormat 格式化
    FallbackValue 绑定失败时的默认值
    TargetNullValue 源为 null 时显示的值

    ⚠️ StringFormat 中的花括号必须转义,否则 XAML 会解析失败:

    <!– 方式一:用单引号包裹整个格式串 –>
    <TextBlock Text="{Binding Age, StringFormat='年龄:{0}岁'}"/>

    <!– 方式二:用 {} 前缀转义(格式串以 { 开头时必用) –>
    <TextBlock Text="{Binding Age, StringFormat={}{0}岁}"/>


    九、样式、触发器与模板

    9.1 Style(样式)

    把重复的属性设置抽出来复用:

    <Window.Resources>
    <Style x:Key="PrimaryButton" TargetType="Button">
    <Setter Property="Background" Value="#007ACC"/>
    <Setter Property="Foreground" Value="White"/>
    <Setter Property="Padding" Value="16,8"/>
    <Setter Property="FontSize" Value="14"/>
    <Setter Property="BorderThickness" Value="0"/>
    <Setter Property="Cursor" Value="Hand"/>
    </Style>
    </Window.Resources>

    <StackPanel Margin="20">
    <Button Content="主要按钮" Margin="0,0,0,10"
    Style="{StaticResource PrimaryButton}"/>

    <Button Content="另一个" Style="{StaticResource PrimaryButton}"/>
    </StackPanel>

    隐式样式:不写 x:Key,则该类型的所有控件自动应用:

    <Style TargetType="Button">
    <Setter Property="Margin" Value="4"/>
    </Style>

    样式继承 BasedOn:把公共部分抽成基础样式,再派生出具体样式。

    <Style x:Key="BaseButton" TargetType="Button">
    <Setter Property="Padding" Value="12,6"/>
    <Setter Property="Cursor" Value="Hand"/>
    </Style>

    <Style x:Key="PrimaryButton" TargetType="Button"
    BasedOn="{StaticResource BaseButton}">

    <Setter Property="Background" Value="#007ACC"/>
    <Setter Property="Foreground" Value="White"/>
    </Style>

    9.2 触发器(Trigger)

    根据条件自动改变样式:

    <Style x:Key="HoverButton" TargetType="Button">
    <Setter Property="Background" Value="#007ACC"/>
    <Setter Property="Foreground" Value="White"/>
    <Setter Property="Template">
    <Setter.Value>
    <ControlTemplate TargetType="Button">
    <Border x:Name="border" Background="{TemplateBinding Background}"
    CornerRadius="4" Padding="16,8">
    <ContentPresenter HorizontalAlignment="Center"
    VerticalAlignment="Center"/>
    </Border>
    <ControlTemplate.Triggers>
    <Trigger Property="IsMouseOver" Value="True">
    <Setter TargetName="border" Property="Background" Value="#005A9E"/>
    </Trigger>
    <Trigger Property="IsPressed" Value="True">
    <Setter TargetName="border" Property="Background" Value="#003F6E"/>
    </Trigger>
    <Trigger Property="IsEnabled" Value="False">
    <Setter TargetName="border" Property="Background" Value="#CCCCCC"/>
    </Trigger>
    </ControlTemplate.Triggers>
    </ControlTemplate>
    </Setter.Value>
    </Setter>
    </Style>

    多条件触发器 MultiTrigger:只有多个属性同时满足条件才触发。

    <MultiTrigger>
    <MultiTrigger.Conditions>
    <Condition Property="IsMouseOver" Value="True"/>
    <Condition Property="IsEnabled" Value="True"/>
    </MultiTrigger.Conditions>
    <Setter Property="Background" Value="#005A9E"/>
    </MultiTrigger>

    9.3 DataTrigger(数据触发器)

    根据绑定数据的值改变外观:

    <TextBlock Text="{Binding Name}">
    <TextBlock.Style>
    <Style TargetType="TextBlock">
    <Style.Triggers>
    <DataTrigger Binding="{Binding IsVip}" Value="True">
    <Setter Property="Foreground" Value="Gold"/>
    <Setter Property="FontWeight" Value="Bold"/>
    </DataTrigger>
    </Style.Triggers>
    </Style>
    </TextBlock.Style>
    </TextBlock>

    小提示:在 Style.Triggers 的 DataTrigger 中,Binding 默认会绑定到应用了该 Style 的元素的数据上下文(通常是数据项本身),而不是父级 ViewModel。如果需要绑定到 ViewModel 的属性,需要使用 RelativeSource:

    <DataTrigger Binding="{Binding DataContext.IsVip,
    RelativeSource={RelativeSource AncestorType=Window}}"

    Value="True">

    <Setter Property="Foreground" Value="Gold"/>
    </DataTrigger>

    多数据条件 MultiDataTrigger:

    <MultiDataTrigger>
    <MultiDataTrigger.Conditions>
    <Condition Binding="{Binding IsVip}" Value="True"/>
    <Condition Binding="{Binding Age}" Value="18"/>
    </MultiDataTrigger.Conditions>
    <Setter Property="Foreground" Value="Gold"/>
    </MultiDataTrigger>

    9.4 DataTemplate(数据模板)

    定义“数据对象长什么样”:

    <ListBox ItemsSource="{Binding Persons}">
    <ListBox.ItemTemplate>
    <DataTemplate>
    <StackPanel Orientation="Horizontal" Margin="4">
    <Border Width="36" Height="36" CornerRadius="18"
    Background="#007ACC" Margin="0,0,10,0">

    <TextBlock Text="{Binding Name[0]}"
    FallbackValue="?" Foreground="White"
    HorizontalAlignment="Center"
    VerticalAlignment="Center"/>

    </Border>
    <StackPanel>
    <TextBlock Text="{Binding Name}" FontWeight="Bold"/>
    <TextBlock Text="{Binding Age, StringFormat='年龄:{0}岁'}"
    Foreground="Gray" FontSize="12"/>

    </StackPanel>
    </StackPanel>
    </DataTemplate>
    </ListBox.ItemTemplate>
    </ListBox>

    上面 Name[0] 加了 FallbackValue="?",防止 Name 为空字符串时索引失败。

    9.5 ControlTemplate(控件模板)

    彻底重写控件的外观结构:

    <Button Content="圆角按钮">
    <Button.Template>
    <ControlTemplate TargetType="Button">
    <Border Background="#28A745" CornerRadius="20" Padding="20,10">
    <ContentPresenter HorizontalAlignment="Center"
    VerticalAlignment="Center"/>

    </Border>
    </ControlTemplate>
    </Button.Template>
    </Button>

    Style vs Template

    • Style:设置属性值(颜色、边距等)
    • Template:重写控件的内部结构

    十、资源字典

    10.1 定义与使用

    <Window x:Class="WpfDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sys="clr-namespace:System;assembly=System.Runtime">

    <Window.Resources>
    <SolidColorBrush x:Key="PrimaryBrush" Color="#007ACC"/>
    <sys:Double x:Key="TitleSize">24</sys:Double>
    </Window.Resources>

    <StackPanel Margin="20">
    <TextBlock Foreground="{StaticResource PrimaryBrush}"
    FontSize="{StaticResource TitleSize}"
    Text="标题"/>

    </StackPanel>
    </Window>

    ⚠️ 注意 xmlns:sys 的程序集名:.NET Framework 时代写 mscorlib,.NET 8 应写 System.Runtime。xmlns:sys 习惯放在根元素上统一声明。

    10.2 StaticResource vs DynamicResource

    StaticResourceDynamicResource
    查找时机 加载时一次 每次使用时
    性能 略低
    支持运行时更换

    需要换肤时用 DynamicResource。

    10.3 独立资源字典文件

    新建 Styles/Colors.xaml:

    <ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <SolidColorBrush x:Key="PrimaryBrush" Color="#007ACC"/>
    <SolidColorBrush x:Key="DangerBrush" Color="#DC3545"/>
    </ResourceDictionary>

    在 App.xaml 中合并:

    <Application.Resources>
    <ResourceDictionary>
    <ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="Styles/Colors.xaml"/>
    </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
    </Application.Resources>

    跨程序集 / 类库引用资源时,用 pack URI:

    <ResourceDictionary Source="pack://application:,,,/YourAssembly;component/Styles/Colors.xaml"/>


    十一、MVVM 模式

    MVVM 是 WPF 最推荐的架构:Model – View – ViewModel。

    View (XAML) ←绑定→ ViewModel ←操作→ Model
    界面 状态与逻辑 数据实体

    • View:只负责显示,几乎不写逻辑
    • ViewModel:持有数据、命令,实现 INotifyPropertyChanged
    • Model:纯数据类

    11.1 为什么用 MVVM

    • UI 与逻辑解耦,方便测试
    • 不用在代码后置里写一堆 xxx.Text = …
    • 团队协作:设计师改 XAML,程序员写 VM

    11.2 ViewModel 基类(减少重复)

    using System.ComponentModel;
    using System.Runtime.CompilerServices;

    public abstract class ViewModelBase : INotifyPropertyChanged
    {
    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? name = null)
    => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

    protected bool SetProperty<T>(ref T field, T value,
    [CallerMemberName] string? name = null)
    {
    if (Equals(field, value)) return false;
    field = value;
    OnPropertyChanged(name);
    return true;
    }
    }

    使用:

    private string _name = "";
    public string Name
    {
    get => _name;
    set => SetProperty(ref _name, value);
    }

    11.3 ICommand 与 RelayCommand

    WPF 的 Button.Command 可以绑定到 ICommand,替代 Click 事件。

    .NET 8 下推荐的手动刷新版本:

    using System;
    using System.Windows.Input;

    public class RelayCommand : ICommand
    {
    private readonly Action<object?> _execute;
    private readonly Predicate<object?>? _canExecute;

    public RelayCommand(Action<object?> execute, Predicate<object?>? canExecute = null)
    {
    _execute = execute ?? throw new ArgumentNullException(nameof(execute));
    _canExecute = canExecute;
    }

    public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;

    public void Execute(object? parameter) => _execute(parameter);

    public event EventHandler? CanExecuteChanged;

    public void RaiseCanExecuteChanged()
    => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }

    两种实现方式的对比:

    方案实现方式优点缺点
    CommandManager.RequerySuggested 版本 在 CanExecuteChanged 的 add/remove 中挂钩 CommandManager 自动刷新,代码简单 每次鼠标/键盘等 UI 交互都会触发所有命令的 CanExecute 重查,性能开销大;异步逻辑改了状态后不会主动刷新
    手动 RaiseCanExecuteChanged 版本(本文所用) 显式调用 RaiseCanExecuteChanged() 性能可控,只在需要时刷新 需要在属性 setter 中手动调用,代码稍多

    如果使用 CommandManager.RequerySuggested 版本,在异步操作(如 await Task.Run(…))完成后,即使状态已改变,按钮可能仍保持禁用状态,此时需要手动调用 CommandManager.InvalidateRequerySuggested() 来强制刷新。

    11.4 可选:直接用 CommunityToolkit.Mvvm

    如果嫌手写 INotifyPropertyChanged 和 RelayCommand 太啰嗦,可以在 .NET 8 项目中安装 NuGet 包 CommunityToolkit.Mvvm(微软官方推荐),用源生成器一行搞定:

    using CommunityToolkit.Mvvm.ComponentModel;
    using CommunityToolkit.Mvvm.Input;

    // 注意:使用源生成器特性的类必须是 partial
    public partial class MainViewModel : ObservableObject
    {
    [ObservableProperty]
    private string _name = "";

    [RelayCommand]
    private void SayHello() => System.Windows.MessageBox.Show($"Hello {Name}");

    // 带 CanExecute 的异步命令示例
    [RelayCommand(CanExecute = nameof(CanSave))]
    private async Task SaveAsync()
    {
    await Task.Delay(1000); // 模拟耗时操作
    }

    private bool CanSave() => !string.IsNullOrWhiteSpace(Name);
    }

    生成的 Name 属性、SayHelloCommand、SaveCommand 都会自动处理通知与刷新。生产项目里用得很多。

    命名约定:[ObservableProperty] 特性的源生成器会基于字段名自动生成首字母大写的属性。它支持 _lowerCamel 和 m_lowerCamel 两种命名约定,例如 _name 会生成 Name,m_name 也会生成 Name。请务必把类声明为 partial,否则源生成器无法工作。


    十二、依赖属性与附加属性

    本章对新手偏难,建议先掌握 MVVM,等到需要自己写自定义控件时再回头看。

    12.1 什么是依赖属性

    WPF 控件的属性大多是依赖属性(DependencyProperty)。它的特点是:

    • 支持数据绑定、样式、动画、默认值继承
    • 值不直接存在对象字段中,而是由 WPF 属性系统管理

    12.2 自定义依赖属性

    下面用 UserControl 派生类演示一个带 Title 属性的简单控件。假设 MyCard.xaml 内部有一个名为 TitleTextBlock 的 TextBlock:

    using System.Windows;
    using System.Windows.Controls;

    public partial class MyCard : UserControl
    {
    public MyCard()
    {
    InitializeComponent();
    }

    public static readonly DependencyProperty TitleProperty =
    DependencyProperty.Register(
    nameof(Title),
    typeof(string),
    typeof(MyCard),
    new PropertyMetadata("默认标题", OnTitleChanged));

    public string Title
    {
    get => (string)GetValue(TitleProperty);
    set => SetValue(TitleProperty, value);
    }

    private static void OnTitleChanged(DependencyObject d,
    DependencyPropertyChangedEventArgs e)
    {
    var card = (MyCard)d;
    card.TitleTextBlock.Text = e.NewValue as string ?? "";
    }
    }

    XAML 中使用(同样需要 xmlns:local="clr-namespace:WpfDemo"):

    <local:MyCard Title="我的卡片" Width="200" Height="80"/>

    如果非要 class MyCard : Control,则必须同时在 Themes/Generic.xaml 里提供默认 Template,否则控件不显示任何内容——新手容易踩坑,所以这里用 UserControl 演示。

    12.3 附加属性

    附加属性是“定义在 A 类,却用在 B 对象上”的属性,比如 Grid.Row、Canvas.Left:

    using System.Windows;

    public static class MyAttached
    {
    public static readonly DependencyProperty TagProperty =
    DependencyProperty.RegisterAttached(
    "Tag", typeof(string), typeof(MyAttached),
    new PropertyMetadata(string.Empty));

    public static string? GetTag(DependencyObject obj)
    => (string?)obj.GetValue(TagProperty);

    public static void SetTag(DependencyObject obj, string? value)
    => obj.SetValue(TagProperty, value);
    }

    新手提示:日常开发中,依赖属性更多是“使用”而非“定义”。了解即可,需要做自定义控件时再深入。


    十三、综合案例:待办事项

    把前面的知识串起来,做一个完整的 TodoList。

    13.1 Model

    Models/TodoItem.cs:

    using System.ComponentModel;
    using System.Runtime.CompilerServices;

    namespace WpfDemo.Models
    {
    public class TodoItem : INotifyPropertyChanged
    {
    private string _title = "";
    public string Title
    {
    get => _title;
    set { _title = value; OnPropertyChanged(); }
    }

    private bool _isDone;
    public bool IsDone
    {
    get => _isDone;
    set { _isDone = value; OnPropertyChanged(); }
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? name = null)
    => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
    }

    13.2 Command(复用第 11.3 节的 RelayCommand)

    Commands/RelayCommand.cs:直接使用第 11.3 节的实现。

    13.3 ViewModel

    ViewModels/MainViewModel.cs:

    using System.Collections.ObjectModel;
    using System.Windows.Input;
    using WpfDemo.Models;
    using WpfDemo.Commands;

    namespace WpfDemo.ViewModels
    {
    public class MainViewModel : ViewModelBase
    {
    public ObservableCollection<TodoItem> Todos { get; }
    = new ObservableCollection<TodoItem>();

    private string _newTodoText = "";
    public string NewTodoText
    {
    get => _newTodoText;
    set
    {
    if (SetProperty(ref _newTodoText, value))
    (AddCommand as RelayCommand)?.RaiseCanExecuteChanged();
    }
    }

    public ICommand AddCommand { get; }
    public ICommand DeleteCommand { get; }
    public ICommand ClearCommand { get; }

    public MainViewModel()
    {
    AddCommand = new RelayCommand(
    _ => Add(),
    _ => !string.IsNullOrWhiteSpace(NewTodoText));

    DeleteCommand = new RelayCommand(
    p => Delete(p as TodoItem));

    ClearCommand = new RelayCommand(
    _ => Clear(),
    _ => Todos.Count > 0);

    // 集合内容变化时,主动刷新 ClearCommand 的可用状态。
    // 这是一种常见模式:当集合内容影响命令可用性时,
    // 在 CollectionChanged 事件中触发 RaiseCanExecuteChanged。
    Todos.CollectionChanged += (_, _) =>
    (ClearCommand as RelayCommand)?.RaiseCanExecuteChanged();
    }

    private void Add()
    {
    Todos.Add(new TodoItem { Title = NewTodoText.Trim() });
    NewTodoText = string.Empty;
    }

    private void Delete(TodoItem? item)
    {
    if (item != null) Todos.Remove(item);
    }

    private void Clear()
    {
    Todos.Clear();
    }
    }
    }

    关键点:

    • NewTodoText 变化时手动刷新 AddCommand(因为按钮的可用性依赖输入)
    • Todos 集合变化时刷新 ClearCommand(清空按钮要有内容才可用)

    13.4 View

    MainWindow.xaml(注意全部使用 .NET 8 兼容的完整写法):

    <Window x:Class="WpfDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="待办事项" Height="500" Width="520"
    WindowStartupLocation="CenterScreen">

    <Window.Resources>
    <Style TargetType="Button">
    <Setter Property="Padding" Value="12,6"/>
    <Setter Property="Margin" Value="4,0,0,0"/>
    <Setter Property="Cursor" Value="Hand"/>
    </Style>
    <Style TargetType="TextBox">
    <Setter Property="Padding" Value="6,4"/>
    <Setter Property="VerticalContentAlignment" Value="Center"/>
    </Style>
    </Window.Resources>

    <Grid Margin="16">
    <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
    <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>

    <!– 输入区 –>
    <Grid Grid.Row="0">
    <Grid.ColumnDefinitions>
    <ColumnDefinition Width="*"/>
    <ColumnDefinition Width="Auto"/>
    </Grid.ColumnDefinitions>

    <TextBox Grid.Column="0"
    Text="{Binding NewTodoText, UpdateSourceTrigger=PropertyChanged}"
    FontSize="14"/>

    <Button Grid.Column="1" Content="添加"
    Command="{Binding AddCommand}"
    Background="#007ACC" Foreground="White" BorderThickness="0"/>

    </Grid>

    <!– 列表区 –>
    <ListBox Grid.Row="1" Margin="0,12" ItemsSource="{Binding Todos}"
    HorizontalContentAlignment="Stretch">

    <ListBox.ItemTemplate>
    <DataTemplate>
    <Grid Margin="2">
    <Grid.ColumnDefinitions>
    <ColumnDefinition Width="Auto"/>
    <ColumnDefinition Width="*"/>
    <ColumnDefinition Width="Auto"/>
    </Grid.ColumnDefinitions>

    <CheckBox Grid.Column="0" IsChecked="{Binding IsDone}"
    VerticalAlignment="Center"/>

    <TextBlock Grid.Column="1" Text="{Binding Title}"
    Margin="8,0" FontSize="14"
    VerticalAlignment="Center"
    TextTrimming="CharacterEllipsis">

    <TextBlock.Style>
    <Style TargetType="TextBlock">
    <Style.Triggers>
    <DataTrigger Binding="{Binding IsDone}" Value="True">
    <Setter Property="TextDecorations" Value="Strikethrough"/>
    <Setter Property="Foreground" Value="Gray"/>
    </DataTrigger>
    </Style.Triggers>
    </Style>
    </TextBlock.Style>
    </TextBlock>

    <Button Grid.Column="2" Content="删除"
    Command="{Binding DataContext.DeleteCommand,
    RelativeSource={RelativeSource AncestorType={x:Type ListBox}}}"

    CommandParameter="{Binding}"/>

    </Grid>
    </DataTemplate>
    </ListBox.ItemTemplate>
    </ListBox>

    <!– 底部 –>
    <Button Grid.Row="2" Content="清空全部"
    HorizontalAlignment="Right"
    Command="{Binding ClearCommand}"/>

    </Grid>
    </Window>

    13.5 代码后置

    MainWindow.xaml.cs:

    using System.Windows;
    using WpfDemo.ViewModels;

    namespace WpfDemo
    {
    public partial class MainWindow : Window
    {
    public MainWindow()
    {
    InitializeComponent();
    DataContext = new MainViewModel();
    }
    }
    }

    13.6 关键点解释

    为什么 DeleteCommand 要用 RelativeSource?

    DataTemplate 内部的 DataContext 已经变成了 TodoItem(列表的每一项),而 DeleteCommand 定义在 MainViewModel 上。所以要通过 RelativeSource={RelativeSource AncestorType={x:Type ListBox}} 向上找到 ListBox,再取它的 DataContext(也就是 MainViewModel),从而访问 DeleteCommand。

    这是 WPF 中解决 DataTemplate 内 DataContext 隔离问题的标准模式,在 ListBox、ListView、DataGrid 等控件中会频繁使用,建议读者熟悉。

    为什么 NewTodoText 的 setter 里要手动 RaiseCanExecuteChanged?

    因为按钮的 CanExecute 依赖输入内容的非空性。默认的 RelayCommand 不监听属性变化,只有当输入变化时主动刷新,按钮才会由灰变亮。

    按 F5 运行:输入文字 → 添加 → 勾选划线 → 删除 → 清空。


    十四、动画入门

    ⚠️ 重要前提:WPF 动画只能作用于依赖属性(Opacity、Width、RenderTransform、Margin 等)。依赖属性提供了值表达式、属性失效、默认值、继承、数据绑定、动画和属性变更通知等支持,普通 CLR 属性无法被动画驱动。

    WPF 动画基于属性,用 Storyboard 描述。

    14.1 简单淡入

    <Button Content="淡入按钮" Width="120" Height="40" Opacity="0">
    <Button.Triggers>
    <EventTrigger RoutedEvent="Loaded">
    <BeginStoryboard>
    <Storyboard>
    <DoubleAnimation Storyboard.TargetProperty="Opacity"
    From="0" To="1" Duration="0:0:1"/>

    </Storyboard>
    </BeginStoryboard>
    </EventTrigger>
    </Button.Triggers>
    </Button>

    14.2 鼠标悬停放大

    <Button Content="悬停放大" Width="120" Height="40"
    RenderTransformOrigin="0.5,0.5">

    <Button.RenderTransform>
    <ScaleTransform ScaleX="1" ScaleY="1"/>
    </Button.RenderTransform>
    <Button.Triggers>
    <EventTrigger RoutedEvent="MouseEnter">
    <BeginStoryboard>
    <Storyboard>
    <DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleX"
    To="1.15" Duration="0:0:0.2"/>

    <DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleY"
    To="1.15" Duration="0:0:0.2"/>

    </Storyboard>
    </BeginStoryboard>
    </EventTrigger>
    <EventTrigger RoutedEvent="MouseLeave">
    <BeginStoryboard>
    <Storyboard>
    <DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleX"
    To="1" Duration="0:0:0.2"/>

    <DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleY"
    To="1" Duration="0:0:0.2"/>

    </Storyboard>
    </BeginStoryboard>
    </EventTrigger>
    </Button.Triggers>
    </Button>

    14.3 常用动画类型

    类型用途
    DoubleAnimation 数值属性(Opacity、Width、角度)
    ColorAnimation 颜色
    ThicknessAnimation 边距
    PointAnimation 点坐标
    DoubleAnimationUsingKeyFrames 关键帧动画

    十五、学习建议与常见坑

    学习路线

    XAML 语法 → 布局 → 常用控件 → 事件
    → 数据绑定 → 样式模板 → 资源字典 → MVVM
    → 依赖属性/附加属性 → 动画 → 自定义控件

    建议顺序:

  • 先能写出静态界面(布局 + 控件)
  • 再学数据绑定,理解 DataContext
  • 然后学 INotifyPropertyChanged + ObservableCollection
  • 再学 MVVM + ICommand,把代码后置里的逻辑搬进 ViewModel
  • 最后学依赖属性、动画,做自己的控件
  • 常见坑

    问题原因 / 解决
    绑定不生效,输出窗口有 Binding 错误 打开 VS 的“输出”窗口查看,通常 Path 写错或 DataContext 未设置
    改了数据界面不更新 属性没实现 INotifyPropertyChanged
    集合增删界面不刷新 用了 List<T>,应改用 ObservableCollection<T>
    TextBox 输入时按钮状态不刷新 加 UpdateSourceTrigger=PropertyChanged,并在 setter 里 RaiseCanExecuteChanged
    DataTemplate 里绑定命令失败 DataContext 已变成数据项,需用 RelativeSource 找父级
    DataTrigger 中 Binding 绑定到了数据项而非 ViewModel Style.Triggers 内的 DataTrigger 默认绑定到应用该 Style 的元素的数据上下文;如需绑定 ViewModel 属性,同样需要用 RelativeSource
    异步更新后命令未刷新 如果使用 CommandManager.RequerySuggested 版本,在 await 完成后需手动调用 CommandManager.InvalidateRequerySuggested()
    控件被“吃掉”看不见 多个元素放同一 Grid 单元格会重叠,需设置 Grid.Row/Grid.Column
    窗口缩放时布局乱 少用固定 Width/Height,多用 * 和 Auto
    StaticResource 找不到 资源定义在使用之后,或未合并资源字典;改用 DynamicResource 可延后查找
    图片不显示 图片“生成操作”要设为 Resource
    Grid.ColumnDefinitions="…" / StackPanel.Spacing 报错 这两个是 .NET 9+ 才加入 WPF 的新特性,.NET 8 请用完整写法(<Grid.RowDefinitions> 与 Margin)
    StringFormat={0} 解析失败 花括号需转义:用 '…' 包裹或用 {} 前缀
    xmlns:sys="…mscorlib" 报错 .NET 8 应写 assembly=System.Runtime
    界面卡顿 耗时操作放后台线程(Task.Run),UI 更新回到 UI 线程
    大数据列表卡顿 ListBox/ListView/DataGrid 默认启用 UI 虚拟化;若把它们放进外层 ScrollViewer 或 StackPanel,虚拟化会失效,需改用 Grid 或设置受限高度

    后台线程更新 UI

    在 UI 线程中 await 默认会回到 UI 线程,可以直接更新控件:

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
    var result = await Task.Run(() => Compute());
    MyTextBlock.Text = result; // 回到 UI 线程,可直接更新
    }

    如果在 Task.Run 内直接更新 UI(非 UI 线程),需要切回 UI 线程:

    Application.Current.Dispatcher.Invoke(() =>
    {
    MyTextBlock.Text = "更新";
    });

    // 或异步等待
    await Application.Current.Dispatcher.InvokeAsync(() =>
    {
    MyTextBlock.Text = "更新";
    });

    设计时数据(让 VS 设计器显示绑定效果)

    MVVM 项目里 View 在设计器里常常看不到效果,可加设计时 DataContext:

    <Window x:Class="WpfDemo.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:vm="clr-namespace:WpfDemo.ViewModels"
    mc:Ignorable="d"
    d:DataContext="{d:DesignInstance Type=vm:MainViewModel, IsDesignTimeCreatable=True}"
    Title="待办事项" Height="500" Width="520">

    <!– 内容 –>
    </Window>

    d: 前缀的属性只在设计器生效,运行时会被忽略。

    调试绑定的技巧

    在 VS 中:工具 → 选项 → 调试 → 输出窗口 → WPF 跟踪设置,把“数据绑定”设为“警告”或“所有”,绑定失败会在输出窗口明确提示。

    也可以对单个绑定开启追踪:

    <TextBlock Text="{Binding Name,
    PresentationTraceSources.TraceLevel=High}"
    />

    推荐的进阶方向

    • CollectionViewSource:对集合做排序、筛选、分组,不改动原集合
    • CommunityToolkit.Mvvm:官方推荐的 MVVM 库,省去大量样板代码,支持 AsyncRelayCommand
    • Microsoft.Xaml.Behaviors.Wpf:把交互逻辑变成可复用组件
    • Prism:成熟的模块化 MVVM 框架
    • 自定义控件(ControlTemplate + 依赖属性)
    • 多线程与 async/await 更新 UI

    CollectionViewSource 简单示例(对 TodoList 做过滤):

    <Window.Resources>
    <CollectionViewSource x:Key="FilteredTodos"
    Source="{Binding Todos}"
    Filter="FilteredTodos_Filter"/>

    </Window.Resources>

    <ListBox ItemsSource="{StaticResource FilteredTodos}"/>

    private void FilteredTodos_Filter(object sender, FilterEventArgs e)
    {
    if (e.Item is TodoItem t)
    e.Accepted = string.IsNullOrEmpty(_keyword)
    || t.Title.Contains(_keyword);
    }


    结语

    WPF 的学习曲线主要在数据绑定和 MVVM 上,布局和控件其实很快就能上手。建议的做法是:

  • 把本文的每个案例都亲手敲一遍(注意:本文基于 .NET 8)
  • 把待办事项案例扩展成:加分类、加持久化(存 JSON)、加搜索过滤
  • 遇到问题先看 VS 输出窗口的绑定错误信息

  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » WPF 详细入门教程
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!