如何在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

