You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在WPF模板中实现仅响应鼠标悬停(忽略点击)的动画

问题

我正在使用WPF结合DevExpress,为DockLayoutManager的标签头创建自定义模板。目标是实现仅在鼠标进入或离开标签头边界时播放的动画,要求忽略点击操作,只关注鼠标位置。

根据WPF文档,UIElement的IsMouseOver属性应该能满足需求:

Gets a value indicating whether the mouse pointer is located over this element (including child elements in the visual tree). This is a dependency property.

但实际使用中发现,按下鼠标左键时IsMouseOver会变为False,导致动画错误触发。以下是我的模板代码:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:cs="colorscheme_ignorable_uri"
        xmlns:dx="http://schemas.devexpress.com/winfx/2008/xaml/core"
        xmlns:dxdo="http://schemas.devexpress.com/winfx/2008/xaml/docking"
        xmlns:dxdop="http://schemas.devexpress.com/winfx/2008/xaml/docking/platform"
        xmlns:dxdot="http://schemas.devexpress.com/winfx/2008/xaml/docking/themekeys"
        xmlns:dxdove="http://schemas.devexpress.com/winfx/2008/xaml/docking/visualelements"
        xmlns:dxi="http://schemas.devexpress.com/winfx/2008/xaml/core/internal"
        xmlns:dxt="http://schemas.devexpress.com/winfx/2008/xaml/core/themekeys"
        xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
    <ControlTemplate x:Key="{dxdot:DocumentPaneElementsThemeKey ResourceKey=PageHeader}" TargetType="dxdove:DocumentPaneItem">
        <Grid x:Name="Root"
              dxdove:DockPane.HitTestType="Header"
              Visibility="{Binding Visibility}">
            <dxdove:TabHeaderContainer BottomMargin="0,1,0,0"
                                       BottomSelectedMargin="0,-1,0,-1"
                                       CaptionLocation="{TemplateBinding CaptionLocation}"
                                       IsSelected="{TemplateBinding IsSelected}"
                                       LeftMargin="0,0,1,0"
                                       LeftSelectedMargin="-1,0,-1,0"
                                       RightMargin="1,0,0,0"
                                       RightSelectedMargin="-1,0,-1,0"
                                       TopMargin="0,0,0,1"
                                       TopSelectedMargin="0,0,0,1">
                <dxdove:TabHeaderBackgroundPanel CaptionLocation="{TemplateBinding CaptionLocation}" CaptionOrientation="{TemplateBinding CaptionOrientation}">
                    <Grid>
                        <Border x:Name="IsDefault"
                                Background="Transparent"
                                BorderBrush="Transparent"
                                BorderThickness="1,1,1,0"
                                CornerRadius="0,0,0,0"
                                Visibility="{Binding HasTabColor, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BoolToVisibilityInverseConverter}}"
                                cs:id="Border_0001"/>
                        <Border x:Name="IsDefaultColored"
                                Background="Transparent"
                                BorderBrush="Transparent"
                                BorderThickness="{TemplateBinding ActualBorderThickness}"
                                CornerRadius="0,0,0,0"
                                Visibility="{Binding HasTabColor, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BooleanToVisibilityConverter}}"
                                cs:id="Border_0002"/>
                    </Grid>
                </dxdove:TabHeaderBackgroundPanel>
            </dxdove:TabHeaderContainer>
            <dxdove:TabHeaderTransformPanel CaptionLocation="{TemplateBinding CaptionLocation}" CaptionOrientation="{TemplateBinding CaptionOrientation}">
                <Grid x:Name="PressSensor"
                      Background="Transparent"
                      Cursor="Arrow"
                      Focusable="False"
                      HorizontalAlignment="Stretch"
                      VerticalAlignment="Stretch"
                      cs:id="Grid_0000">
                    <Grid x:Name="Content">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        <dxdove:TemplatedTabCaptionControl x:Name="PART_CaptionControlPresenter"
                                                           Foreground="$Foreground.Primary"
                                                           LayoutItem="{Binding}"
                                                           Margin="12,3,12,3"
                                                           cs:id="TemplatedTabCaptionControl_0000"/>
                        <Border x:Name="HoverIndicator"
                                Background="$Button.SimpleButtonBorder"
                                CornerRadius="2"
                                Height="3"
                                HorizontalAlignment="Center"
                                Margin="0,30,0,5"
                                RenderTransformOrigin="0.5,0.5"
                                VerticalAlignment="Bottom"
                                Visibility="Hidden"
                                Width="{Binding ActualWidth, ElementName=PART_CaptionControlPresenter}"
                                cs:id="Border_0000"/>
                        <Border x:Name="SelectionIndicator"
                                Background="$Accent"
                                CornerRadius="2"
                                Height="3"
                                HorizontalAlignment="Center"
                                Margin="0,30,0,5"
                                RenderTransformOrigin="0.5,0.5"
                                VerticalAlignment="Bottom"
                                Visibility="Hidden"
                                Width="{Binding ActualWidth, ElementName=PART_CaptionControlPresenter}"
                                cs:id="Border_0003">
                            <dx:DXBorder.RenderTransform>
                                <ScaleTransform ScaleX="1"/>
                            </dx:DXBorder.RenderTransform>
                        </Border>
                        <dxdove:DocumentTabHeaderControlBoxControl x:Name="PART_ControlBox"
                                                                   ButtonHeight="16"
                                                                   ButtonStyle="{DynamicResource {dxdot:DocumentElementsThemeKey ResourceKey=DocumentPageButtonStyle}}"
                                                                   ButtonWidth="16"
                                                                   CloseButtonTemplate="{DynamicResource {dxdot:DocumentElementsThemeKey ResourceKey=ClosePageButton}}"
                                                                   Grid.Column="1"
                                                                   LayoutItem="{Binding}"
                                                                   Margin="4,3,4,3"
                                                                   PinButtonTemplate="{DynamicResource {dxdot:DocumentElementsThemeKey ResourceKey=PinPageButton}}"/>
                    </Grid>
                </Grid>
            </dxdove:TabHeaderTransformPanel>
            <VisualStateManager.VisualStateGroups>
                <VisualStateGroup x:Name="CommonStates">
                    <VisualState x:Name="MouseOver">
                        <Storyboard>
                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="HoverIndicator" Storyboard.TargetProperty="Visibility">
                                <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}"/>
                            </ObjectAnimationUsingKeyFrames>
                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="HoverIndicator" Storyboard.TargetProperty="Background">
                                <DiscreteObjectKeyFrame KeyTime="0">
                                    <DiscreteObjectKeyFrame.Value>
                                        <SolidColorBrush Color="$Button.SimpleButtonBorder" cs:id="SolidColorBrush_0000"/>
                                    </DiscreteObjectKeyFrame.Value>
                                </DiscreteObjectKeyFrame>
                            </ObjectAnimationUsingKeyFrames>
                        </Storyboard>
                    </VisualState>
                    <VisualState x:Name="Normal"/>
                </VisualStateGroup>
                <VisualStateGroup x:Name="SelectionStates">
                    <VisualState x:Name="EmptySelectionState"/>
                    <VisualState x:Name="Selected">
                        <Storyboard>
                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="SelectionIndicator" Storyboard.TargetProperty="Visibility">
                                <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}"/>
                            </ObjectAnimationUsingKeyFrames>
                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_CaptionControlPresenter" Storyboard.TargetProperty="FontWeight">
                                <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static FontWeights.Bold}"/>
                            </ObjectAnimationUsingKeyFrames>
                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="IsDefault" Storyboard.TargetProperty="Background">
                                <DiscreteObjectKeyFrame KeyTime="0">
                                    <DiscreteObjectKeyFrame.Value>
                                        <SolidColorBrush Color="Transparent" cs:id="SolidColorBrush_0005"/>
                                    </DiscreteObjectKeyFrame.Value>
                                </DiscreteObjectKeyFrame>
                            </ObjectAnimationUsingKeyFrames>
                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="IsDefault" Storyboard.TargetProperty="BorderBrush">
                                <DiscreteObjectKeyFrame KeyTime="0">
                                    <DiscreteObjectKeyFrame.Value>
                                        <SolidColorBrush Color="Transparent" cs:id="SolidColorBrush_0001"/>
                                    </DiscreteObjectKeyFrame.Value>
                                </DiscreteObjectKeyFrame>
                            </ObjectAnimationUsingKeyFrames>
                        </Storyboard>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>
        </Grid>
        <ControlTemplate.Triggers>
            <MultiTrigger>
                <MultiTrigger.Conditions>
                    <Condition Property="IsMouseOver"
                               SourceName="PressSensor"
                               Value="True"/>
                </MultiTrigger.Conditions>
                <MultiTrigger.EnterActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Duration="0:0:0.1"
                                             Storyboard.TargetName="SelectionIndicator"
                                             Storyboard.TargetProperty="RenderTransform.(ScaleTransform.ScaleX)"
                                             To="1.0"/>
                        </Storyboard>
                    </BeginStoryboard>
                </MultiTrigger.EnterActions>
                <MultiTrigger.ExitActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Duration="0:0:0.1"
                                             Storyboard.TargetName="SelectionIndicator"
                                             Storyboard.TargetProperty="RenderTransform.(ScaleTransform.ScaleX)"
                                             To="1.3"/>
                        </Storyboard>
                    </BeginStoryboard>
                </MultiTrigger.ExitActions>
            </MultiTrigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
</ResourceDictionary>

我试过用透明按钮,想基于IsPressed || IsMouseOver触发动画,但DevExpress的某些机制似乎拦截了点击事件。我对DevExpress、WPF和UI编程都不太熟悉,不知道该怎么解决。

核心问题:在此模板中,如何实现仅在鼠标进入或离开元素时触发的动画,而非鼠标按下或释放时?


解决方案

原因分析

按下鼠标左键时IsMouseOver变为False,是因为WPF中当元素没有捕获鼠标时,按下左键后鼠标输入会被其他元素(比如DevExpress内部的交互控件)接管,导致原元素的IsMouseOver状态丢失。

解决方法

1. 使用MouseEnter和MouseLeave事件直接触发动画

放弃依赖IsMouseOver触发器,改用事件触发器绑定MouseEnter和MouseLeave事件,这样只响应鼠标进出动作,不受点击状态影响。

修改模板中的ControlTemplate.Triggers部分:

<ControlTemplate.Triggers>
    <!-- 替换原来的MultiTrigger -->
    <EventTrigger RoutedEvent="MouseEnter">
        <BeginStoryboard>
            <Storyboard>
                <DoubleAnimation Duration="0:0:0.1"
                                 Storyboard.TargetName="SelectionIndicator"
                                 Storyboard.TargetProperty="RenderTransform.(ScaleTransform.ScaleX)"
                                 To="1.0"/>
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
    <EventTrigger RoutedEvent="MouseLeave">
        <BeginStoryboard>
            <Storyboard>
                <DoubleAnimation Duration="0:0:0.1"
                                 Storyboard.TargetName="SelectionIndicator"
                                 Storyboard.TargetProperty="RenderTransform.(ScaleTransform.ScaleX)"
                                 To="1.3"/>
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</ControlTemplate.Triggers>

如果需要指定仅在PressSensor网格上触发事件,也可以给事件触发器指定SourceName:

<EventTrigger RoutedEvent="MouseEnter" SourceName="PressSensor">
    <!-- 动画内容 -->
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave" SourceName="PressSensor">
    <!-- 动画内容 -->
</EventTrigger>

2. 优化VisualState的MouseOver状态(可选)

原模板中CommonStates的MouseOver状态同样会受点击影响,可以添加对IsPressed的排除条件,确保仅在未点击时触发:

<VisualStateGroup x:Name="CommonStates">
    <VisualState x:Name="MouseOver">
        <VisualState.StateTriggers>
            <MultiStateTrigger>
                <MultiStateTrigger.Conditions>
                    <Condition Property="IsMouseOver" Value="True"/>
                    <Condition Property="IsPressed" Value="False"/>
                </MultiStateTrigger.Conditions>
            </MultiStateTrigger>
        </VisualState.StateTriggers>
        <!-- 原有Storyboard -->
    </VisualState>
    <VisualState x:Name="Normal"/>
</VisualStateGroup>

内容的提问来源于stack exchange,提问作者Brendan Mitchell

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 05:24:49