田敏
返回博客列表
WPF

WPF 触发器

田敏
2026-07-133 分钟阅读
WPF,C#

WPF 触发器学习笔记

1. 触发器是什么

触发器(Trigger)用于声明:当某个条件成立时,修改控件的属性或执行动画。

它适合描述界面状态变化,例如:

  • 鼠标悬停时改变按钮背景色。
  • 设备被禁用时显示灰色状态标签。
  • 列表正在加载时显示“正在加载”。
  • 查询结果为空时显示空状态页面。

触发器通常写在以下位置:

<Style.Triggers>
    <!-- Style 中的触发器 -->
</Style.Triggers>

<ControlTemplate.Triggers>
    <!-- ControlTemplate 中的触发器 -->
</ControlTemplate.Triggers>

2. 触发器总览

| 类型 | 条件来源 | 常见位置 | 适用场景 | | --- | --- | --- | --- | | Trigger | 控件自身的依赖属性 | Style.TriggersControlTemplate.Triggers | 悬停、按下、选中、禁用 | | DataTrigger | 绑定对象的数据属性 | Style.TriggersControlTemplate.Triggers | 设备启用状态、加载状态、权限状态 | | MultiTrigger | 多个控件自身属性 | Style.TriggersControlTemplate.Triggers | 同时悬停且启用 | | MultiDataTrigger | 多个绑定数据属性 | Style.TriggersControlTemplate.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;
}

当前项目中,按钮的 IsMouseOverIsPressedIsEnabled 触发器位于 App.xaml 的按钮模板中。

常见属性

| 属性 | 典型控件 | 作用 | | --- | --- | --- | | IsMouseOver | 大多数控件 | 鼠标是否停留在控件上 | | IsPressed | Button | 按钮是否正在按下 | | IsEnabled | 大多数控件 | 控件是否可用 | | IsSelected | ListBoxItemDataGridRow | 项是否被选中 | | 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>

执行顺序是:

  1. 普通 Setter 先把背景设为启用颜色。
  2. WPF 读取当前行设备的 IsEnabled
  3. 当值为 false 时,DataTrigger 中的 Setter 覆盖默认背景色。
  4. 值恢复为 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 中使用这种写法区分空数据和无筛选结果。

MultiTriggerMultiDataTrigger 的选择

| 条件是什么 | 选择 | | --- | --- | | IsMouseOverIsPressedIsEnabled 等控件属性 | MultiTrigger | | IsLoadingHasDevicesIsEnabled 等 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.TriggersControlTemplate.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}" />

排查触发器不生效时,优先检查:

  1. Binding 的路径是否正确,DataContext 是否如预期。
  2. 绑定属性是否会触发 PropertyChanged
  3. Value 的类型是否能正确转换。
  4. 同一个属性是否被本地值、动画、模板或更高优先级样式覆盖。
  5. 对于模板内部元素,是否应该使用 ControlTemplate.TriggersTargetName

10. Trigger、Converter 和 ViewModel 属性怎么选

| 情况 | 推荐方式 | 原因 | | --- | --- | --- | | 一个布尔条件改变颜色或可见性 | DataTrigger | 声明式、直观 | | 两到四个简单布尔条件同时判断 | MultiDataTrigger | 直接表达“并且”关系 | | 数据值转换为显示文本或颜色 | IValueConverter | 适合一进一出的转换 | | 条件很复杂、需要复用、包含业务含义 | ViewModel 派生属性 | 便于测试,不让 XAML 变复杂 | | 按钮悬停、按下、禁用 | Trigger | 条件来自控件本身 | | 启动淡入、放大等视觉动画 | EventTrigger + Storyboard | 适合一次性事件 |

例如,HasVisibleDevices 是一个很好的 ViewModel 派生属性。它把“当前筛选后是否仍有数据”这个业务判断放在 C# 中,XAML 只根据 truefalse 决定是否显示空状态。

版权协议:MIT返回列表