WPF 样式与模板
大约 11 分钟约 3392 字
WPF 样式与模板
简介
样式(Style)和模板(Template)是 WPF 实现界面美观和代码复用的核心机制。样式统一控件的属性,模板完全自定义控件的外观。掌握它们可以打造出专业的、一致的用户界面。
特点
样式(Style)
基本样式
<!-- 定义样式 -->
<Window.Resources>
<!-- 基本按钮样式 -->
<Style x:Key="PrimaryButton" TargetType="Button">
<Setter Property="Background" Value="#4CAF50" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="14" />
<Setter Property="Padding" Value="20,8" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 危险按钮样式 -->
<Style x:Key="DangerButton" TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
<Setter Property="Background" Value="#F44336" />
</Style>
</Window.Resources>
<!-- 使用样式 -->
<Button Style="{StaticResource PrimaryButton}" Content="确定" />
<Button Style="{StaticResource DangerButton}" Content="删除" />全局默认样式(隐式样式)
<!-- 不设置 x:Key — 自动应用到所有该类型控件 -->
<Style TargetType="Button">
<Setter Property="Background" Value="#2196F3" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="14" />
<Setter Property="Margin" Value="5" />
<Setter Property="Padding" Value="15,8" />
</Style>
<!-- 所有 Button 自动应用此样式 -->
<Button Content="按钮1" />
<Button Content="按钮2" />样式继承(BasedOn)
<!-- 基础文本样式 -->
<Style x:Key="BaseText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Microsoft YaHei" />
<Setter Property="FontSize" Value="14" />
<Setter Property="Foreground" Value="#333333" />
</Style>
<!-- 继承并扩展 -->
<Style x:Key="TitleText" TargetType="TextBlock" BasedOn="{StaticResource BaseText}">
<Setter Property="FontSize" Value="24" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style x:Key="SubtitleText" TargetType="TextBlock" BasedOn="{StaticResource BaseText}">
<Setter Property="FontSize" Value="18" />
<Setter Property="Foreground" Value="#666666" />
</Style>控件模板(ControlTemplate)
自定义按钮模板
<!-- 完全自定义按钮外观 -->
<Style x:Key="ModernButton" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1"
CornerRadius="6"
SnapsToDevicePixels="True">
<ContentPresenter x:Name="contentPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
RecognizesAccessKey="True" />
</Border>
<ControlTemplate.Triggers>
<!-- 鼠标悬停 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="#3D8BFD" />
<Setter TargetName="border" Property="BorderBrush" Value="#2D6BDD" />
</Trigger>
<!-- 按下 -->
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Background" Value="#1D5BBD" />
</Trigger>
<!-- 禁用 -->
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="border" Property="Background" Value="#CCCCCC" />
<Setter TargetName="border" Property="BorderBrush" Value="#999999" />
<Setter TargetName="contentPresenter" Property="TextElement.Foreground" Value="#666666" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>自定义 TextBox 模板
<!-- 带水印和边框动画的输入框 -->
<Style x:Key="ModernTextBox" TargetType="TextBox">
<Setter Property="Background" Value="White" />
<Setter Property="BorderBrush" Value="#CCCCCC" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="8,6" />
<Setter Property="FontSize" Value="14" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<Grid>
<TextBlock x:Name="placeholder"
Text="{TemplateBinding Tag}"
Foreground="#AAAAAA"
VerticalAlignment="Center"
Visibility="Collapsed" />
<ScrollViewer x:Name="PART_ContentHost"
VerticalAlignment="Center" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="border" Property="BorderBrush" Value="#2196F3" />
<Setter TargetName="border" Property="BorderThickness" Value="2" />
</Trigger>
<Trigger Property="Text" Value="">
<Setter TargetName="placeholder" Property="Visibility" Value="Visible" />
</Trigger>
<Trigger Property="Text" Value="{x:Null}">
<Setter TargetName="placeholder" Property="Visibility" Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 使用:Tag 作为水印文字 -->
<TextBox Style="{StaticResource ModernTextBox}" Tag="请输入用户名" />
<TextBox Style="{StaticResource ModernTextBox}" Tag="请输入密码" />数据模板(DataTemplate)
列表项模板
<!-- 定义数据模板 -->
<DataTemplate x:Key="UserCardTemplate" DataType="{x:Type local:User}">
<Border Background="White" CornerRadius="8" Padding="15" Margin="5"
BorderBrush="#E0E0E0" BorderThickness="1">
<StackPanel>
<TextBlock Text="{Binding Name}" FontSize="16" FontWeight="Bold" />
<TextBlock Text="{Binding Email}" Foreground="#666" Margin="0,4,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<Border Background="#E3F2FD" CornerRadius="3" Padding="6,2">
<TextBlock Text="{Binding Department}" FontSize="12" Foreground="#1565C0" />
</Border>
<Border Background="{Binding IsActive, Converter={StaticResource StatusColorConverter}}"
CornerRadius="10" Width="8" Height="8" Margin="8,0,0,0"
VerticalAlignment="Center" />
</StackPanel>
</StackPanel>
</Border>
</DataTemplate>
<!-- 使用 -->
<ItemsControl ItemsSource="{Binding Users}"
ItemTemplate="{StaticResource UserCardTemplate}" />自动匹配数据类型
<!-- 不指定 x:Key — 自动匹配 DataType -->
<Window.Resources>
<DataTemplate DataType="{x:Type local:TextMessage}">
<TextBlock Text="{Binding Content}" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:ImageMessage}">
<Image Source="{Binding ImageUrl}" MaxHeight="200" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:SystemMessage}">
<TextBlock Text="{Binding Content}" Foreground="Gray"
HorizontalAlignment="Center" FontStyle="Italic" />
</DataTemplate>
</Window.Resources>
<!-- 根据消息类型自动选择模板 -->
<ItemsControl ItemsSource="{Binding Messages}" />资源字典(ResourceDictionary)
组织样式文件
<!-- Styles/Colors.xaml — 颜色定义 -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Color x:Key="PrimaryColor">#2196F3</Color>
<Color x:Key="DangerColor">#F44336</Color>
<Color x:Key="SuccessColor">#4CAF50</Color>
<Color x:Key="WarningColor">#FF9800</Color>
<Color x:Key="TextColor">#333333</Color>
<Color x:Key="SecondaryTextColor">#666666</Color>
<Color x:Key="BorderColor">#E0E0E0</Color>
<Color x:Key="BackgroundColor">#F5F5F5</Color>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource PrimaryColor}" />
<SolidColorBrush x:Key="DangerBrush" Color="{StaticResource DangerColor}" />
<SolidColorBrush x:Key="TextBrush" Color="{StaticResource TextColor}" />
<SolidColorBrush x:Key="SecondaryTextBrush" Color="{StaticResource SecondaryTextColor}" />
<SolidColorBrush x:Key="BorderBrush" Color="{StaticResource BorderColor}" />
</ResourceDictionary><!-- Styles/ButtonStyles.xaml -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Style x:Key="PrimaryButton" TargetType="Button">
<!-- ... -->
</Style>
<Style x:Key="DangerButton" TargetType="Button">
<!-- ... -->
</Style>
</ResourceDictionary><!-- App.xaml — 合并所有资源字典 -->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Styles/Colors.xaml" />
<ResourceDictionary Source="Styles/ButtonStyles.xaml" />
<ResourceDictionary Source="Styles/TextStyles.xaml" />
<ResourceDictionary Source="Styles/TextBoxStyles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>视觉状态(VisualStateManager)
<!-- WPF 也可以使用 VisualStateManager 管理状态 -->
<Style x:Key="VsmButton" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<Storyboard>
<ColorAnimation Storyboard.TargetName="border"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="#2196F3" Duration="0:0:0.2" />
</Storyboard>
</VisualState>
<VisualState x:Name="MouseOver">
<Storyboard>
<ColorAnimation Storyboard.TargetName="border"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="#3D8BFD" Duration="0:0:0.2" />
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ColorAnimation Storyboard.TargetName="border"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="#1D5BBD" Duration="0:0:0.1" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="border" Background="#2196F3" CornerRadius="6" Padding="20,8">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>样式选择器
根据条件自动应用样式
<!-- DataTrigger — 根据数据值切换样式 -->
<Style x:Key="DeviceStatusCard" TargetType="Border">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#E0E0E0"/>
<Setter Property="BorderThickness" Value="1"/>
<Style.Triggers>
<!-- 根据绑定数据动态切换边框颜色 -->
<DataTrigger Binding="{Binding State}" Value="Online">
<Setter Property="BorderBrush" Value="#4CAF50"/>
<Setter Property="BorderThickness" Value="2"/>
</DataTrigger>
<DataTrigger Binding="{Binding State}" Value="Warning">
<Setter Property="BorderBrush" Value="#FF9800"/>
<Setter Property="BorderThickness" Value="2"/>
</DataTrigger>
<DataTrigger Binding="{Binding State}" Value="Error">
<Setter Property="BorderBrush" Value="#F44336"/>
<Setter Property="BorderThickness" Value="2"/>
</DataTrigger>
</Style.Triggers>
</Style>
<!-- MultiDataTrigger — 多条件组合 -->
<Style x:Key="PriorityTaskStyle" TargetType="ListBoxItem">
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsUrgent}" Value="True"/>
<Condition Binding="{Binding IsCompleted}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Background" Value="#FFF3E0"/>
<Setter Property="FontWeight" Value="Bold"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>多值转换器驱动样式
/// <summary>
/// 多值转换器 — 根据多个参数决定颜色
/// </summary>
public class RangeToColorConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length < 3) return Brushes.Gray;
if (values[0] is double current &&
values[1] is double min &&
values[2] is double max)
{
if (max <= min) return Brushes.Gray;
double ratio = (current - min) / (max - min);
if (ratio < 0.3) return Brushes.Green;
if (ratio < 0.7) return Brushes.Orange;
return Brushes.Red;
}
return Brushes.Gray;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// 布尔值转图标
/// </summary>
public class BoolToIconConverter : IValueConverter
{
public string TrueIcon { get; set; } = "✓";
public string FalseIcon { get; set; } = "✗";
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolVal)
return boolVal ? TrueIcon : FalseIcon;
return FalseIcon;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}<!-- 使用多值转换器 -->
<Border Background="{Binding Value, Converter={StaticResource ValueToColorConverter}}"/>
<Border>
<Border.Background>
<MultiBinding Converter="{StaticResource RangeToColorConverter}">
<Binding Path="CurrentValue"/>
<Binding Path="MinValue"/>
<Binding Path="MaxValue"/>
</MultiBinding>
</Border.Background>
</Border>ItemsControl 样式定制
列表项交替色与选中样式
<!-- ListBox 样式 — 交替行色 + 选中高亮 -->
<Style x:Key="AlternatingListBox" TargetType="ListBox">
<Setter Property="AlternationCount" Value="2"/>
<Setter Property="Background" Value="White"/>
</Style>
<Style x:Key="AlternatingListItem" TargetType="ListBoxItem">
<Style.Triggers>
<!-- 偶数行 -->
<Trigger Property="ItemsControl.AlternationIndex" Value="0">
<Setter Property="Background" Value="White"/>
</Trigger>
<!-- 奇数行 -->
<Trigger Property="ItemsControl.AlternationIndex" Value="1">
<Setter Property="Background" Value="#F5F5F5"/>
</Trigger>
<!-- 悬停 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#E3F2FD"/>
</Trigger>
<!-- 选中 -->
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#BBDEFB"/>
<Setter Property="Foreground" Value="#1565C0"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Trigger>
</Style.Triggers>
</Style>
<ListBox Style="{StaticResource AlternatingListBox}"
ItemContainerStyle="{StaticResource AlternatingListItem}"
ItemsSource="{Binding Devices}"/>DataGrid 样式定制
<!-- DataGrid 整体样式 -->
<Style x:Key="ModernDataGrid" TargetType="DataGrid">
<Setter Property="AutoGenerateColumns" Value="False"/>
<Setter Property="CanUserAddRows" Value="False"/>
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="GridLinesVisibility" Value="Horizontal"/>
<Setter Property="HeadersVisibility" Value="Column"/>
<Setter Property="RowHeight" Value="32"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="AlternatingRowBackground" Value="#FAFAFA"/>
</Style>
<!-- 表头样式 -->
<Style x:Key="DataGridColumnHeaderStyle" TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#F0F0F0"/>
<Setter Property="Foreground" Value="#333333"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="8,6"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Setter Property="BorderBrush" Value="#DDDDDD"/>
</Style>
<!-- 单元格样式 -->
<Style x:Key="DataGridCellStyle" TargetType="DataGridCell">
<Setter Property="Padding" Value="8,4"/>
<Setter Property="BorderThickness" Value="0"/>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#BBDEFB"/>
<Setter Property="Foreground" Value="#1565C0"/>
</Trigger>
</Style.Triggers>
</Style>
<DataGrid Style="{StaticResource ModernDataGrid}"
ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}"
CellStyle="{StaticResource DataGridCellStyle}"
ItemsSource="{Binding Products}">
<DataGrid.Columns>
<DataGridTextColumn Header="产品名称" Binding="{Binding Name}" Width="*"/>
<DataGridTextColumn Header="价格" Binding="{Binding Price, StringFormat='{}{0:C}'}" Width="100"/>
<DataGridTextColumn Header="库存" Binding="{Binding Stock}" Width="80"/>
</DataGrid.Columns>
</DataGrid>样式优先级与覆盖
样式覆盖规则
WPF 样式优先级(从高到低):
1. 直接设置属性(如 Button Background="Red")
2. 模板内的 Trigger
3. 样式中的 Trigger
4. Style 中的 Setter
5. 隐式样式(TargetType 无 Key)
6. 主题样式(Themes/Generic.xaml)
了解优先级有助于排查"样式不生效"的问题<!-- 样式覆盖示例 -->
<!-- 底层样式 -->
<Style x:Key="BaseCard" TargetType="Border">
<Setter Property="Background" Value="White"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="16"/>
<Setter Property="Margin" Value="4"/>
</Style>
<!-- 继承并覆盖 -->
<Style x:Key="WarningCard" TargetType="Border" BasedOn="{StaticResource BaseCard}">
<Setter Property="Background" Value="#FFF3E0"/>
<Setter Property="BorderBrush" Value="#FF9800"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<!-- 使用时再次覆盖 -->
<Border Style="{StaticResource WarningCard}"
Background="#FFF8E1" Margin="8"/>
<!-- 最终 Background 为 #FFF8E1(直接属性 > Style Setter) -->性能优化
样式性能注意事项
/// <summary>
/// 样式性能优化建议
/// </summary>
/*
* 1. 减少 DataTemplate 中的控件嵌套层数
* 每增加一层嵌套,就多一个可视化树节点
* 推荐:DataTemplate 内部不超过 5 层
*
* 2. 避免在 DataTemplate 中使用复杂 Converter
* 列表项模板中的 Converter 会在每个项上执行
* 高频刷新时考虑用 TextBlock 的 StringFormat 替代
*
* 3. 合理使用 VirtualizingStackPanel
* 列表超过 50 项时必须启用虚拟化
* <VirtualizingStackPanel.IsVirtualizing="True"/>
*
* 4. 避免不必要的样式 Trigger
* Trigger 会在每次属性变更时检查
* 列表项模板中的 Trigger 数量控制在 3 个以内
*
* 5. 使用 x:Shared="False" 处理需要独立状态的模板
* 如动画播放状态需要独立的控件
*/优点
缺点
总结
样式和模板是 WPF 的核心竞争力。样式(Style)统一属性设置,控件模板(ControlTemplate)自定义控件外观,数据模板(DataTemplate)定义数据展示方式。通过资源字典(ResourceDictionary)组织样式文件,打造一致且美观的界面。
关键知识点
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
- WPF 主题往往要同时理解依赖属性、绑定、可视树和命令系统。
项目落地视角
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
- 优先确认 DataContext、绑定路径、命令触发点和资源引用来源。
常见误区
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
- 在 code-behind 塞太多状态与业务逻辑。
进阶路线
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
- 继续补齐控件库、主题系统、诊断工具和启动性能优化。
适用场景
- 当你准备把《WPF 样式与模板》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
落地建议
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
排错清单
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
复盘问题
- 如果把《WPF 样式与模板》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《WPF 样式与模板》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《WPF 样式与模板》最大的收益和代价分别是什么?
