WPF 基础
深入理解 WPF 的核心概念和基础功能
样式系统
WPF 的样式系统允许你定义可重用的 UI 外观,类似于 CSS 在 Web 开发中的作用。样式可以应用于单个控件、特定类型的所有控件或整个应用程序。
基本样式定义
<Window.Resources>
<Style TargetType="Button" x:Key="ModernButton">
<Setter Property="Background" Value="#2196F3" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="14" />
<Setter Property="Padding" Value="15,8" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Cursor" Value="Hand" />
</Style>
</Window.Resources>
<!-- 使用样式 -->
<Button Content="点击我" Style="{StaticResource ModernButton}" />
隐式样式
<!-- 不指定 x:Key,样式将应用于所有目标类型 -->
<Style TargetType="Button">
<Setter Property="Padding" Value="10,5" />
<Setter Property="Margin" Value="5" />
</Style>
<!-- 所有 Button 都会自动应用此样式 -->
<Button Content="按钮1" />
<Button Content="按钮2" />
样式继承
<Style TargetType="ButtonBase" x:Key="BaseButtonStyle">
<Setter Property="Padding" Value="10,5" />
<Setter Property="FontSize" Value="14" />
</Style>
<Style TargetType="Button"
BasedOn="{StaticResource BaseButtonStyle}"
x:Key="PrimaryButton">
<Setter Property="Background" Value="#2196F3" />
<Setter Property="Foreground" Value="White" />
</Style>
样式触发器
<Style TargetType="Button">
<Setter Property="Background" Value="#2196F3" />
<Setter Property="Foreground" Value="White" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#1976D2" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#0D47A1" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.5" />
</Trigger>
</Style.Triggers>
</Style>
💡 样式最佳实践
- 将样式定义在资源字典中,便于复用和管理
- 使用命名约定区分不同用途的样式
- 合理使用 BasedOn 实现样式继承
- 避免在样式中硬编码颜色值,使用资源引用
- 为样式添加注释说明其用途和使用场景
模板系统
模板系统允许你完全自定义控件的外观和行为,是 WPF 最强大的功能之一。
ControlTemplate
ControlTemplate 定义控件的整体视觉结构。
<Style TargetType="Button" x:Key="RoundedButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="10"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
DataTemplate
DataTemplate 定义数据对象的显示方式。
<!-- 定义数据模板 -->
<DataTemplate x:Key="PersonTemplate">
<StackPanel Orientation="Horizontal" Margin="5">
<Ellipse Width="40" Height="40"
Fill="LightBlue" Margin="0,0,10,0" />
<StackPanel>
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Text="{Binding Email}" FontSize="12"
Foreground="Gray" />
</StackPanel>
</StackPanel>
</DataTemplate>
<!-- 使用数据模板 -->
<ListBox ItemsSource="{Binding People}"
ItemTemplate="{StaticResource PersonTemplate}" />
ItemsPanelTemplate
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
HierarchicalDataTemplate
<HierarchicalDataTemplate
DataType="{x:Type local:Category}"
ItemsSource="{Binding SubCategories}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="📁" Margin="0,0,5,0" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</HierarchicalDataTemplate>
<TreeView ItemsSource="{Binding Categories}" />
⚠️ 模板注意事项
- 模板中必须使用 TemplateBinding 引用控件属性
- ContentPresenter 用于显示控件内容
- 复杂模板可能影响性能,注意优化
- 使用 VisualStateManager 管理视觉状态
资源管理
WPF 的资源系统允许你在不同层级定义和共享对象,如样式、模板、画刷、字符串等。
资源层级
<!-- 1. 应用程序级资源 (App.xaml) -->
<Application.Resources>
<SolidColorBrush x:Key="PrimaryColor" Color="#2196F3" />
</Application.Resources>
<!-- 2. 窗口级资源 (Window.xaml) -->
<Window.Resources>
<SolidColorBrush x:Key="WindowBackground" Color="White" />
</Window.Resources>
<!-- 3. 控件级资源 -->
<Grid.Resources>
<Style TargetType="Button">
<Setter Property="Margin" Value="5" />
</Style>
</Grid.Resources>
资源字典
<!-- Themes/Generic.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="#2196F3" />
<SolidColorBrush x:Key="SecondaryBrush" Color="#FF5722" />
<SolidColorBrush x:Key="TextBrush" Color="#333333" />
<Style TargetType="Button" x:Key="PrimaryButton">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}" />
<Setter Property="Foreground" Value="White" />
</Style>
</ResourceDictionary>
<!-- 在 App.xaml 中合并资源字典 -->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Themes/Generic.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
动态资源 vs 静态资源
<!-- 静态资源:在加载时解析,性能更好 -->
<Button Background="{StaticResource PrimaryBrush}" />
<!-- 动态资源:运行时更新,支持资源变更 -->
<Button Background="{DynamicResource PrimaryBrush}" />
<!-- C# 中更新动态资源 -->
this.Resources["PrimaryBrush"] = new SolidColorBrush(Colors.Red);
💡 资源管理建议
- 优先使用静态资源,只在需要动态更新时使用动态资源
- 将常用资源提取到独立的资源字典文件中
- 使用合理的命名约定,如 {Type}{Purpose}
- 对于大型应用,考虑使用主题系统
动画系统
WPF 提供了强大的动画系统,可以创建流畅的用户体验和交互效果。
Storyboard 动画
<Window.Resources>
<Storyboard x:Key="FadeInAnimation">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0" To="1" Duration="0:0:0.5" />
</Storyboard>
<Storyboard x:Key="SlideInAnimation">
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"
From="-100" To="0" Duration="0:0:0.3">
<DoubleAnimation.EasingFunction>
<BackEase EasingMode="EaseOut" Amplitude="0.3" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Window.Resources>
触发器动画
<Style TargetType="Button">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Width"
To="150" Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Width"
Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
关键帧动画
<Storyboard>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetProperty="Opacity"
Duration="0:0:2">
<LinearDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<LinearDoubleKeyFrame KeyTime="0:0:0.5" Value="1" />
<LinearDoubleKeyFrame KeyTime="0:0:1.5" Value="1" />
<LinearDoubleKeyFrame KeyTime="0:0:2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
C# 中控制动画
private void Button_Click(object sender, RoutedEventArgs e)
{
var button = sender as Button;
var storyboard = new Storyboard();
var widthAnimation = new DoubleAnimation
{
From = button.ActualWidth,
To = 200,
Duration = TimeSpan.FromSeconds(0.3),
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
};
Storyboard.SetTarget(widthAnimation, button);
Storyboard.SetTargetProperty(widthAnimation, new PropertyPath("Width"));
storyboard.Children.Add(widthAnimation);
storyboard.Begin();
}
💡 动画性能优化
- 优先使用 DoubleAnimation,避免使用 ThicknessAnimation
- 使用 RenderTransform 而不是 LayoutTransform
- 对于简单动画,考虑使用 Composition API
- 合理设置动画的 FillBehavior 属性
触发器
触发器允许你在特定条件下自动更改属性值,无需编写后台代码。
Property Trigger
<Style TargetType="Button">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#1976D2" />
<Setter Property="Cursor" Value="Hand" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#0D47A1" />
</Trigger>
</Style.Triggers>
</Style>
Data Trigger
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding IsOnline}" Value="True">
<Setter Property="Foreground" Value="Green" />
<Setter Property="Text" Value="在线" />
</DataTrigger>
<DataTrigger Binding="{Binding IsOnline}" Value="False">
<Setter Property="Foreground" Value="Gray" />
<Setter Property="Text" Value="离线" />
</DataTrigger>
</Style.Triggers>
</Style>
MultiTrigger
<Style TargetType="Button">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsEnabled" Value="True" />
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="Background" Value="#1976D2" />
</MultiTrigger.Setters>
</MultiTrigger>
</Style.Triggers>
</Style>
Event Trigger
<Style TargetType="Button">
<Style.Triggers>
<EventTrigger RoutedEvent="Button.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0" To="1" Duration="0:0:0.5" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
命令模式
命令模式实现了 UI 与业务逻辑的解耦,是 WPF 开发的重要模式。
ICommand 接口
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public bool CanExecute(object parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object parameter) => _execute(parameter);
}
使用命令
public class MainViewModel
{
public ICommand SaveCommand { get; }
public ICommand DeleteCommand { get; }
private string _text;
public string Text
{
get => _text;
set
{
_text = value;
OnPropertyChanged(nameof(Text));
}
}
public MainViewModel()
{
SaveCommand = new RelayCommand(
_ => Save(),
_ => !string.IsNullOrWhiteSpace(Text)
);
DeleteCommand = new RelayCommand(
_ => Delete(),
_ => !string.IsNullOrWhiteSpace(Text)
);
}
private void Save()
{
// 保存逻辑
}
private void Delete()
{
// 删除逻辑
}
}
XAML 中绑定命令
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<StackPanel>
<TextBox Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="保存" Command="{Binding SaveCommand}" />
<Button Content="删除" Command="{Binding DeleteCommand}" />
</StackPanel>
内置命令
<!-- ApplicationCommands -->
<Button Command="ApplicationCommands.New" />
<Button Command="ApplicationCommands.Open" />
<Button Command="ApplicationCommands.Save" />
<Button Command="ApplicationCommands.Copy" />
<Button Command="ApplicationCommands.Paste" />
<!-- NavigationCommands -->
<Button Command="NavigationCommands.BrowseBack" />
<Button Command="NavigationCommands.BrowseForward" />
<!-- ComponentCommands -->
<Button Command="ComponentCommands.MoveUp" />
<Button Command="ComponentCommands.MoveDown" />
💡 命令最佳实践
- 始终使用命令而不是事件处理程序
- 实现 CanExecute 方法提供命令可用性反馈
- 使用 CommandManager.InvalidateRequerySuggested() 手动刷新命令状态
- 考虑使用 CommunityToolkit.Mvvm 的 RelayCommand