写在前面
版本说明:基于 .NET 8 LTS。
WPF 最让人惊艳的特性之一——同一个 Button,可以看起来像 Material 按钮、像 iOS 按钮、像 Office 按钮、甚至像一颗星星。所有这些"换肤"都靠本篇讲的三层机制:Style、ControlTemplate、DataTemplate。
本篇要把它们各自的语义、适用场景、相互关系讲清楚。理解之后,你就能从"用别人写好的控件"升级到"自己定义控件外观"。
本文要回答:
Style / ControlTemplate / DataTemplate 三者到底有什么区别?StaticResource 和 DynamicResource 选哪个?Trigger 为什么"不用写代码就能改外观"?
一、资源系统:ResourceDictionary
样式、模板、画刷都存在"资源"里。
1.1 资源的定义和使用
1
2
3
4
5
6
7
8
| <Window.Resources>
<SolidColorBrush x:Key="AccentBrush" Color="DodgerBlue"/>
<Style x:Key="MyButton" TargetType="Button">
<Setter Property="Background" Value="{StaticResource AccentBrush}"/>
</Style>
</Window.Resources>
<Button Content="OK" Style="{StaticResource MyButton}"/>
|
1
2
3
4
5
6
| 资源定义:放在 .Resources 字典里
- x:Key 是资源名(必须)
- 任意对象都可以是资源(Brush / Style / Template / 数字 / 字符串)
资源使用:{StaticResource key} 或 {DynamicResource key}
- 通过 key 查找
|
1.2 资源查找链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| WPF 资源查找顺序(从内到外):
元素自身.Resources
↓(找不到)
父元素.Resources
↓
Window.Resources
↓
App.Resources
↓
Theme(系统主题样式)
↓
抛 XamlParseException
类似变量作用域链
|
1
2
3
4
5
6
7
8
9
10
| <Window.Resources>
<SolidColorBrush x:Key="bg" Color="Red"/> <!-- 全局 -->
</Window.Resources>
<Grid>
<Grid.Resources>
<SolidColorBrush x:Key="bg" Color="Blue"/> <!-- 局部覆盖 -->
</Grid.Resources>
<Button Background="{StaticResource bg}"/> <!-- 用蓝色(局部优先) -->
</Grid>
|
1.3 ResourceDictionary 合并
1
2
3
4
5
6
7
8
9
10
11
12
13
| <!-- App.xaml 合并多个资源字典 -->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Styles/Colors.xaml"/>
<ResourceDictionary Source="Styles/Buttons.xaml"/>
<ResourceDictionary Source="Styles/TextBoxes.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 这里仍可定义本地资源 -->
<SolidColorBrush x:Key="bg" Color="White"/>
</ResourceDictionary>
</Application.Resources>
|
1
2
3
4
5
6
7
8
| 组织方式:
- Colors.xaml:颜色/画刷
- Sizes.xaml:尺寸
- Buttons.xaml:按钮样式
- Brushes.xaml:渐变、画刷
- ThemeDark.xaml / ThemeLight.xaml:主题切换
按需合并,可切换
|
二、StaticResource vs DynamicResource
2.1 区别
1
2
3
4
5
6
7
8
9
10
11
| StaticResource:
- 编译时(XAML 加载时)解析一次
- 之后不再查
- 性能好(一次性)
- 改资源字典不会更新引用
DynamicResource:
- 运行时解析
- 资源变化时自动更新引用
- 性能稍差
- 支持主题切换、运行时换肤
|
2.2 何时用 DynamicResource
1
2
3
4
5
| <!-- 主题切换:换主题时所有引用自动更新 -->
<Button Background="{DynamicResource AccentBrush}"/>
<!-- 系统颜色:用户改系统主题时跟随 -->
<TextBlock Foreground="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/>
|
1
2
3
4
5
6
| 常见 DynamicResource 场景:
- 系统颜色 / 字体(用户改系统设置时跟随)
- 主题切换(运行时换 ResourceDictionary)
- 跨程序集的资源引用
其他场景用 StaticResource 即可(性能更好)
|
2.3 主题切换实现
1
2
3
4
5
6
7
8
9
10
11
| private void SwitchTheme(string themeName)
{
var newDict = new ResourceDictionary
{
Source = new Uri($"Themes/{themeName}.xaml", UriKind.Relative)
};
// 替换合并字典
Application.Current.Resources.MergedDictionaries.Clear();
Application.Current.Resources.MergedDictionaries.Add(newDict);
}
|
1
| 所有 {DynamicResource ...} 引用都会自动更新
|
三、Style(样式)
3.1 基本用法
1
2
3
4
5
6
7
8
9
10
11
12
| <Window.Resources>
<Style x:Key="PrimaryButton" TargetType="Button">
<Setter Property="Background" Value="DodgerBlue"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Padding" Value="20,8"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
</Window.Resources>
<Button Content="保存" Style="{StaticResource PrimaryButton}"/>
<Button Content="取消" Style="{StaticResource PrimaryButton}"/>
|
3.2 隐式样式(不写 x:Key)
1
2
3
4
5
6
7
8
9
10
| <Window.Resources>
<Style TargetType="Button"> <!-- 注意没 x:Key -->
<Setter Property="Background" Value="DodgerBlue"/>
<Setter Property="Foreground" Value="White"/>
</Style>
</Window.Resources>
<!-- 所有 Button 自动应用 -->
<Button Content="保存"/>
<Button Content="取消"/>
|
1
2
3
4
5
6
7
8
9
| 隐式样式的规则:
- TargetType 必填,x:Key 不写
- 自动应用到该类型的所有元素
- 但只对"声明样式的作用域内"生效
- 派生类不自动应用(RepeatButton 不会用 Button 的隐式样式)
适用:
- 全局按钮风格统一
- 全局文本样式
|
3.3 样式继承(BasedOn)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| <Style x:Key="BaseButton" TargetType="Button">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Padding" Value="20,8"/>
</Style>
<Style x:Key="PrimaryButton" TargetType="Button" BasedOn="{StaticResource BaseButton}">
<Setter Property="Background" Value="DodgerBlue"/>
<Setter Property="Foreground" Value="White"/>
</Style>
<Style x:Key="DangerButton" TargetType="Button" BasedOn="{StaticResource BaseButton}">
<Setter Property="Background" Value="Red"/>
<Setter Property="Foreground" Value="White"/>
</Style>
|
1
2
| BasedOn 类似 CSS 的继承
子样式继承父样式的所有 Setter,可覆盖
|
四、Trigger(触发器)
不用写代码,“条件改变时改属性”。
4.1 PropertyTrigger(属性触发器)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| <Style TargetType="Button">
<Setter Property="Background" Value="DodgerBlue"/>
<Setter Property="Foreground" Value="White"/>
<Style.Triggers>
<!-- 鼠标悬停 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="LightBlue"/>
</Trigger>
<!-- 禁用 -->
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="Gray"/>
<Setter Property="Foreground" Value="LightGray"/>
</Trigger>
</Style.Triggers>
</Style>
|
1
2
3
4
5
| 效果:
- 默认蓝色
- 鼠标悬停浅蓝(IsMouseOver=true 时)
- 禁用灰色(IsEnabled=false 时)
- 离开/恢复时自动回退(不用写代码!)
|
4.2 MultiTrigger(多条件)
1
2
3
4
5
6
7
8
9
10
11
| <Style TargetType="Button">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsEnabled" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" Value="LightGreen"/>
</MultiTrigger>
</Style.Triggers>
</Style>
|
4.3 EventTrigger(事件触发器)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| <Style TargetType="Button">
<Style.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="0.7" Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="1.0" Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
|
1
2
| EventTrigger 触发动画(详见第 10 篇)
不是简单改属性
|
4.4 DataTrigger(数据触发器)
1
2
3
4
5
6
7
8
| <Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding IsUrgent}" Value="True">
<Setter Property="Foreground" Value="Red"/>
<Setter Property="FontWeight" Value="Bold"/>
</DataTrigger>
</Style.Triggers>
</Style>
|
1
2
| DataTrigger 监听"任意绑定的数据"
比 PropertyTrigger(只能监听自身属性)更强大
|
4.5 MultiDataTrigger
1
2
3
4
5
6
7
| <MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsUrgent}" Value="True"/>
<Condition Binding="{Binding IsOverdue}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter Property="Background" Value="DarkRed"/>
</MultiDataTrigger>
|
五、ControlTemplate(控件模板)
Style 改的是"属性",ControlTemplate 改的是"外观结构"。
1
2
3
4
| Button(默认模板展开)
└─ ButtonChrome(主题边框)
└─ ContentPresenter(内容容器)
└─ (你的 Content)
|
1
2
3
| ControlTemplate 的作用:
完全替换上面的视觉树
→ 你可以让 Button 看起来像任何东西
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| <Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<!-- 边框 -->
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6">
<!-- 内容 -->
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding Content}"/>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Opacity" Value="0.8"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Opacity" Value="0.6"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
|
5.3 TemplateBinding
1
2
3
4
5
6
7
8
9
10
| TemplateBinding = 在 ControlTemplate 里"接通"原控件的属性
Background="{TemplateBinding Background}"
→ 把模板内 Border.Background 接到 Button.Background
→ 你设置 Button.Background="Red",模板里的 Border 也是红
等价于:
Background="{Binding Background, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
TemplateBinding 是它的语法糖(且 OneWay)
|
5.4 ContentPresenter
1
2
3
4
5
6
7
| ContentPresenter = "原控件的内容放这里"
<ContentPresenter Content="{TemplateBinding Content}"/>
→ Button.Content 会被渲染到这个位置
类似 ASP.NET Razor 的 RenderBody()
→ 模板留个洞,外面填充
|
六、DataTemplate(数据模板)
ControlTemplate 决定"控件长什么样"
DataTemplate 决定"数据长什么样"
6.1 问题引入
1
2
3
4
| <ListBox ItemsSource="{Binding Items}"/>
<!-- Items 是 List<TodoItem> -->
<!-- ListBox 不知道怎么显示 TodoItem -->
<!-- 默认调用 .ToString(),丑爆 -->
|
6.2 用 DataTemplate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| <ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0" IsChecked="{Binding Completed}"/>
<TextBlock Grid.Column="1" Text="{Binding Title}" Margin="8,0"/>
<TextBlock Grid.Column="2" Text="{Binding CreatedAt, StringFormat=yyyy-MM-dd}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
|
1
2
3
4
5
| 效果:
每个 TodoItem 不再是 ToString
而是按 DataTemplate 渲染(复选框 + 标题 + 日期)
DataContext 自动是当前 Item
|
6.3 隐式 DataTemplate(推荐)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <!-- App.xaml 全局注册 -->
<Application.Resources>
<DataTemplate DataType="{x:Type models:TodoItem}">
<Grid Margin="4">
<TextBlock Text="{Binding Title}"/>
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type models:User}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Avatar}" Width="32"/>
<TextBlock Text="{Binding Name}" VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>
</Application.Resources>
|
1
2
3
| <!-- 任意 ItemsControl 自动按数据类型选模板 -->
<ListBox ItemsSource="{Binding Items}"/>
<!-- 不同类型的 Item 显示不同样式 -->
|
1
2
3
4
5
6
7
8
| 隐式 DataTemplate 的优势:
- 不需要在每个 ItemsControl 写 ItemTemplate
- 同一类型在全应用一致显示
- 异构列表(多种类型混合)自动选模板
后端类比:
DataTemplate ≈ Razor 的 DisplayTemplate
隐式 DataTemplate ≈ EditorTemplates/DisplayTemplates 按类型选
|
七、ControlTemplate vs DataTemplate 对比
| 维度 | ControlTemplate | DataTemplate |
|---|
| 应用对象 | 控件(Control) | 数据(任意对象) |
| 决定什么 | 控件外观结构 | 数据呈现方式 |
| 触发位置 | Style.Template | ItemTemplate / ContentTemplate |
| 内部绑定 | TemplateBinding(绑控件属性) | 普通绑定(绑数据) |
| 隐式 | 主题样式(Generic.xaml) | DataType 隐式模板 |
1
2
3
4
5
6
7
| 记忆点:
ControlTemplate 改"控件"(如 Button 怎么画)
DataTemplate 改"数据"(如 TodoItem 怎么显示)
一个控件同时可有两者:
<ListBox ItemTemplate="..."/> <!-- 数据呈现 -->
<ListBox.Template="..."/> <!-- 控件结构 -->
|
八、VisualStateManager(VSM)
Trigger 的"现代替代品"。
8.1 VSM 概念
1
2
3
4
5
6
7
| VisualStateManager:
- 把控件状态抽象成"组"
- 每个状态对应一组动画/属性
- 状态间过渡用动画
例:Button 有 CommonStates 组
Normal / MouseOver / Pressed / Disabled
|
8.2 用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| <ControlTemplate TargetType="Button">
<Grid>
<Border x:Name="border" Background="DodgerBlue" CornerRadius="6"/>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver">
<Storyboard>
<ColorAnimation Storyboard.TargetName="border"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="LightBlue" Duration="0:0:0.2"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ColorAnimation Storyboard.TargetName="border"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="DarkBlue" Duration="0:0:0.1"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
|
8.3 Trigger vs VSM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| Trigger(属性触发器):
- 简单属性变化
- 立即生效
- 在 ControlTemplate.Triggers 里
- 适合"非动画"的简单切换
VSM:
- 状态机模型
- 内置动画过渡
- 适合 CustomControl(自定义控件的标准做法)
- 微软推荐用 VSM
实践:
- 简单 Style(如颜色变化)用 Trigger
- 自定义控件用 VSM
- 大量动画用 VSM
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| <!-- App.xaml -->
<Application.Resources>
<Style x:Key="MaterialButton" TargetType="Button">
<Setter Property="Background" Value="#2196F3"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="Medium"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="24,12"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="root"
Background="{TemplateBinding Background}"
CornerRadius="4">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="{TemplateBinding Padding}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="root" Property="Background" Value="#1976D2"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="root" Property="Background" Value="#0D47A1"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="root" Property="Background" Value="#BDBDBD"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 危险变体(继承) -->
<Style x:Key="MaterialDangerButton" TargetType="Button"
BasedOn="{StaticResource MaterialButton}">
<Setter Property="Background" Value="#F44336"/>
</Style>
</Application.Resources>
|
9.2 卡片 ListBox
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| <Application.Resources>
<Style x:Key="CardListBox" TargetType="ListBox">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 卡片 DataTemplate -->
<DataTemplate x:Key="CardTemplate" DataType="{x:Type models:TodoItem}">
<Border Background="White" CornerRadius="6" Padding="16" Margin="8"
Width="240">
<Border.Effect>
<DropShadowEffect BlurRadius="8" ShadowDepth="2" Opacity="0.2"/>
</Border.Effect>
<StackPanel>
<Grid>
<TextBlock Text="{Binding Title}" FontWeight="Bold" FontSize="16"/>
<Border HorizontalAlignment="Right"
Background="{Binding Completed,
Converter={StaticResource StatusToColor}}"
CornerRadius="8" Padding="6,2">
<TextBlock Text="{Binding Completed,
Converter={StaticResource StatusToText}}"
Foreground="White" FontSize="11"/>
</Border>
</Grid>
<TextBlock Text="{Binding Description}"
Foreground="Gray" Margin="0,8,0,0"
TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding CreatedAt, StringFormat=yyyy-MM-dd HH:mm}"
Foreground="LightGray" FontSize="11" Margin="0,8,0,0"/>
</StackPanel>
</Border>
</DataTemplate>
</Application.Resources>
|
1
2
3
4
| <!-- 使用 -->
<ListBox Style="{StaticResource CardListBox}"
ItemTemplate="{StaticResource CardTemplate}"
ItemsSource="{Binding Todos}"/>
|
9.3 知识点覆盖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| ✅ Style + Setter + 隐式触发器
✅ ControlTemplate + TemplateBinding + ContentPresenter
✅ ControlTemplate.Triggers
✅ Style 继承(BasedOn)
✅ DataTemplate 隐式 + 显式
✅ ItemTemplate 应用
✅ ItemsPanel 改变布局(WrapPanel)
✅ DropShadowEffect(特效)
✅ Converter 在模板里(StatusToColor / StatusToText)
观察:
- 改 Style 不改业务逻辑
- 同一个 ViewModel + 不同 Style = 完全不同 UI
- 这是 WPF "设计 vs 开发"分离的体现
|
十、Theme 与 Generic.xaml
10.1 主题样式
1
2
3
4
5
6
| 每个 WPF 控件都有"默认主题样式"
- 来自 PresentationFramework.Aero2.dll(Windows 10 默认主题)
- 包含 ControlTemplate、Trigger
- 是"没写 Style 时的样子"
你写 Style TargetType="Button"(隐式)会覆盖主题样式
|
10.2 Generic.xaml(自定义控件的主题)
1
2
3
4
5
6
7
8
9
10
| 自定义 CustomControl 时:
- 创建 Themes/Generic.xaml
- 里面写控件的默认 ControlTemplate
- WPF 自动加载(约定)
文件结构:
MyControlLibrary/
├── MyButton.cs
└── Themes/
└── Generic.xaml ← 约定路径
|
1
2
3
4
5
6
7
8
9
10
11
12
| <!-- Generic.xaml -->
<ResourceDictionary xmlns="...">
<Style TargetType="{x:Type local:MyButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyButton}">
<!-- 模板 -->
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
|
十一、常见陷阱
11.1 StaticResource 引用顺序
1
2
3
4
5
6
7
8
| <!-- ❌ 资源定义在使用之后 -->
<Button Background="{StaticResource bg}"/>
<SolidColorBrush x:Key="bg" Color="Red"/>
<!-- 报错:找不到 bg -->
<!-- ✅ 先定义后使用 -->
<SolidColorBrush x:Key="bg" Color="Red"/>
<Button Background="{StaticResource bg}"/>
|
1
2
3
4
5
| StaticResource 是"前向查找",定义必须在前面
但同一 Resources 字典内不分先后(解析时全收集)
跨字典才要求顺序
DynamicResource 没此限制(运行时查)
|
11.2 隐式样式对派生类不生效
1
2
3
4
5
6
7
| <Style TargetType="Button">
<Setter Property="Background" Value="Red"/>
</Style>
<RepeatButton Content="..."/>
<!-- RepeatButton 继承自 ButtonBase,不是 Button -->
<!-- 不会应用上面的隐式样式 -->
|
11.3 ControlTemplate 缺 ContentPresenter
1
2
3
4
5
| <ControlTemplate TargetType="Button">
<Border Background="Red"/>
<!-- ❌ 没 ContentPresenter -->
<!-- Button.Content 没地方放,不显示 -->
</ControlTemplate>
|
11.4 Trigger 和本地值冲突
1
2
3
4
5
6
7
8
9
10
11
| <Style TargetType="Button">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- 本地值(XAML 直接设置)覆盖 Style.Trigger -->
<Button Background="Blue"/>
<!-- 鼠标悬停不会变红! -->
|
1
2
3
4
5
6
| 原因:
本地值 > Style.Trigger(详见第 2 篇优先级)
Trigger 永远赢不了本地值
解决:
把 Background="Blue" 也写到 Style 里
|
11.5 大量 Trigger 性能问题
1
2
3
4
5
| <!-- ❌ 每个 Item 都有自己的 Trigger + 复杂 ControlTemplate -->
<ListBox ItemsSource="{Binding Items}"/>
<!-- 1000 个 Item × 复杂模板 = 慢 -->
<!-- ✅ 简化模板 + 虚拟化 -->
|
十二、与 CSS / Razor 的对比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| CSS WPF Style
──────────────────────────────────────────
.class { color: red } <Style TargetType><Setter Property="Foreground" Value="Red"/>
:hover <Trigger Property="IsMouseOver" Value="True">
@media (theme) DynamicResource + 切换 ResourceDictionary
inheritance BasedOn
Razor Layout WPF ControlTemplate
──────────────────────────────────────────
_Layout.cshtml ControlTemplate
@RenderBody() <ContentPresenter/>
@RenderSection() ItemPresenter / ContentPresenter
Razor DisplayTemplate WPF DataTemplate
──────────────────────────────────────────
DisplayTemplates/ <DataTemplate DataType=...>
按类型选模板 隐式 DataTemplate
@Html.DisplayForModel() ItemsControl 自动用 DataTemplate
|
十三、小结
本文深入 WPF 的样式与模板系统:
- ResourceDictionary 资源系统与查找链
- StaticResource vs DynamicResource(编译期 vs 运行期)
- Style(属性批处理)+ 隐式样式 + BasedOn 继承
- Trigger 全家桶(Property / Multi / Event / Data / MultiData)
- ControlTemplate(彻底重画控件外观)
- TemplateBinding + ContentPresenter
- DataTemplate(决定数据呈现)
- 显式 ItemTemplate + 隐式 DataType
- VisualStateManager(VSM)vs Trigger
- 实战:Material Button + 卡片 ListBox
- Theme 与 Generic.xaml(自定义控件主题)
1
2
3
4
| 记住三句话:
1. Style 改属性,ControlTemplate 改结构,DataTemplate 改数据呈现
2. ContentPresenter 是 ControlTemplate 的"洞",TemplateBinding 是"接原控件属性"
3. DynamicResource + 切换 ResourceDictionary = 一键换肤
|
下一篇进入 WPF 的多线程模型——Dispatcher 与 UI 线程。这是后端工程师最容易踩坑的地方(async/await 行为和 ASP.NET Core 不同)。