WPF 触发器
WPF 触发器学习笔记
1. 触发器是什么
触发器(Trigger)用于声明:当某个条件成立时,修改控件的属性或执行动画。
它适合描述界面状态变化,例如:
- 鼠标悬停时改变按钮背景色。
- 设备被禁用时显示灰色状态标签。
- 列表正在加载时显示“正在加载”。
- 查询结果为空时显示空状态页面。
触发器通常写在以下位置:
<Style.Triggers>
<!-- Style 中的触发器 -->
</Style.Triggers>
<ControlTemplate.Triggers>
<!-- ControlTemplate 中的触发器 -->
</ControlTemplate.Triggers>
2. 触发器总览
| 类型 | 条件来源 | 常见位置 | 适用场景 |
| --- | --- | --- | --- |
| Trigger | 控件自身的依赖属性 | Style.Triggers、ControlTemplate.Triggers | 悬停、按下、选中、禁用 |
| DataTrigger | 绑定对象的数据属性 | Style.Triggers、ControlTemplate.Triggers | 设备启用状态、加载状态、权限状态 |
| MultiTrigger | 多个控件自身属性 | Style.Triggers、ControlTemplate.Triggers | 同时悬停且启用 |
| MultiDataTrigger | 多个绑定数据属性 | Style.Triggers、ControlTemplate.Triggers | 有数据但无筛选结果 |
| EventTrigger | 路由事件或生命周期事件 | 元素触发器、样式或模板 | 加载时播放动画 |
| 代码/行为触发 | 自定义事件或复杂条件 | ViewModel、Behavior | 不适合纯 XAML 的复杂交互 |
3. Trigger:监听控件自身属性
Trigger 不需要写 Binding。它监听的是当前控件或模板控件的属性。
例如,按钮被鼠标悬停时改变背景色:
<Style TargetType="Button">
<Setter Property="Background" Value="{DynamicResource PrimaryBrush}" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource PrimaryHoverBrush}" />
</Trigger>
</Style.Triggers>
</Style>
逻辑可以理解为:
if (button.IsMouseOver)
{
button.Background = PrimaryHoverBrush;
}
当前项目中,按钮的 IsMouseOver、IsPressed 和 IsEnabled 触发器位于 App.xaml 的按钮模板中。
常见属性
| 属性 | 典型控件 | 作用 |
| --- | --- | --- |
| IsMouseOver | 大多数控件 | 鼠标是否停留在控件上 |
| IsPressed | Button | 按钮是否正在按下 |
| IsEnabled | 大多数控件 | 控件是否可用 |
| IsSelected | ListBoxItem、DataGridRow | 项是否被选中 |
| IsKeyboardFocused | 输入控件 | 是否获得键盘焦点 |
4. DataTrigger:监听绑定数据
DataTrigger 通过 Binding 监听 DataContext 中的属性,因此特别适合 MVVM。
你刚才看到的设备状态背景色就是这个类型:
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background"
Value="{DynamicResource StatusEnabledBackgroundBrush}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
<Setter Property="Background"
Value="{DynamicResource StatusDisabledBackgroundBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
执行顺序是:
- 普通
Setter先把背景设为启用颜色。 - WPF 读取当前行设备的
IsEnabled。 - 当值为
false时,DataTrigger中的 Setter 覆盖默认背景色。 - 值恢复为
true时,触发器失效,背景回到普通 Setter 的值。
当前项目中,这个例子位于 DeviceListView.xaml。
常见用途
<!-- 正在加载时显示文字 -->
<DataTrigger Binding="{Binding IsLoading}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
<!-- 请求失败时显示错误区域 -->
<DataTrigger Binding="{Binding HasLoadError}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
注意 Value 的类型
Value="False" 看起来是字符串,但 XAML 会依据绑定属性的类型转换为 bool。如果绑定的属性是枚举或数值,也应使用该类型能转换的值。
<DataTrigger Binding="{Binding StatusFilter}" Value="Disabled">
<!-- StatusFilter 是枚举时,匹配 Disabled 值 -->
</DataTrigger>
5. MultiTrigger:多个控件属性同时成立
MultiTrigger 是多个 Trigger 条件的“并且”关系。它的条件都来自控件自身属性,不能使用 Binding。
<Style TargetType="Button">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsEnabled" Value="True" />
</MultiTrigger.Conditions>
<Setter Property="Cursor" Value="Hand" />
</MultiTrigger>
</Style.Triggers>
</Style>
只有按钮既可用、又被鼠标悬停时,才会设置手型光标。
6. MultiDataTrigger:多个数据条件同时成立
MultiDataTrigger 是多个绑定条件的“并且”关系。每个条件都要写 Binding。
下面是设备列表的“无匹配结果”场景:源数据存在,但当前筛选后没有任何可见设备,并且没有处于加载或错误状态。
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding HasDevices}" Value="True" />
<Condition Binding="{Binding HasVisibleDevices}" Value="False" />
<Condition Binding="{Binding IsLoading}" Value="False" />
<Condition Binding="{Binding HasLoadError}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible" />
</MultiDataTrigger>
可以把它读成:
if (HasDevices && !HasVisibleDevices && !IsLoading && !HasLoadError)
{
visibility = Visibility.Visible;
}
当前项目已经在 DeviceListView.xaml 中使用这种写法区分空数据和无筛选结果。
MultiTrigger 与 MultiDataTrigger 的选择
| 条件是什么 | 选择 |
| --- | --- |
| IsMouseOver、IsPressed、IsEnabled 等控件属性 | MultiTrigger |
| IsLoading、HasDevices、IsEnabled 等 ViewModel 或行数据属性 | MultiDataTrigger |
7. EventTrigger:事件发生时执行动作
EventTrigger 不判断一个持续存在的值,而是响应一次事件。它最常见的用途是启动动画。
<Border Opacity="0">
<Border.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
这段代码会在 Border 加载完成时,将透明度从 0 动画过渡到 1。
EventTrigger 的限制
- 它适合“发生一次就执行一次”的动作,例如加载、鼠标进入、单击。
- 它不能像
DataTrigger一样持续观察 ViewModel 属性。 - 复杂交互、取消动画、异步操作通常更适合放在 ViewModel 或 Behavior 中。
8. Style.Triggers 与 ControlTemplate.Triggers
两者都能使用属性、数据和多条件触发器,但监听对象不同。
Style.Triggers
用于直接修改控件自身可公开设置的属性。
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter Property="BorderBrush" Value="{DynamicResource PrimaryBrush}" />
</Trigger>
</Style.Triggers>
</Style>
ControlTemplate.Triggers
用于修改模板内部命名元素的属性。当前项目按钮模板中的 Root Border 就属于这种场景。
<ControlTemplate TargetType="Button">
<Border x:Name="Root" Background="{TemplateBinding Background}">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Root"
Property="Background"
Value="{DynamicResource PrimaryHoverBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
TargetName="Root" 的意思是:不要改 Button 自身,而是改模板中名为 Root 的 Border。
9. Setter、Trigger 与属性优先级
下面的代码中,禁用状态背景色会覆盖默认背景色:
<Style TargetType="Border">
<Setter Property="Background" Value="Green" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
<Setter Property="Background" Value="Gray" />
</DataTrigger>
</Style.Triggers>
</Style>
因为同一个样式内,激活的 Trigger 通常比普通 Setter 优先级更高。
但还要注意:如果直接在元素上设置了本地值,往往会压过样式触发器。
<!-- Background 是本地值,可能导致 Style 中的 Trigger 看起来“不生效” -->
<Border Background="Red" Style="{StaticResource DeviceStatusStyle}" />
排查触发器不生效时,优先检查:
Binding的路径是否正确,DataContext 是否如预期。- 绑定属性是否会触发
PropertyChanged。 Value的类型是否能正确转换。- 同一个属性是否被本地值、动画、模板或更高优先级样式覆盖。
- 对于模板内部元素,是否应该使用
ControlTemplate.Triggers和TargetName。
10. Trigger、Converter 和 ViewModel 属性怎么选
| 情况 | 推荐方式 | 原因 |
| --- | --- | --- |
| 一个布尔条件改变颜色或可见性 | DataTrigger | 声明式、直观 |
| 两到四个简单布尔条件同时判断 | MultiDataTrigger | 直接表达“并且”关系 |
| 数据值转换为显示文本或颜色 | IValueConverter | 适合一进一出的转换 |
| 条件很复杂、需要复用、包含业务含义 | ViewModel 派生属性 | 便于测试,不让 XAML 变复杂 |
| 按钮悬停、按下、禁用 | Trigger | 条件来自控件本身 |
| 启动淡入、放大等视觉动画 | EventTrigger + Storyboard | 适合一次性事件 |
例如,HasVisibleDevices 是一个很好的 ViewModel 派生属性。它把“当前筛选后是否仍有数据”这个业务判断放在 C# 中,XAML 只根据 true 或 false 决定是否显示空状态。