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