C#实现按钮悬停缩放动画出现异常行为的技术求助
按钮悬停动画异常问题
我用C#实现了按钮的悬停动画:MouseEnter触发放大,MouseLeave触发缩小。正常等待缩放完成后移开按钮,动画没问题;但快速多次悬停同一按钮或切换悬停不同按钮时,动画出现异常。我猜是MouseEnter和MouseLeave并发执行导致的,试过记录按钮名称规避但没用。相关代码如下:
XAML代码
<Button Name="Redo" Click="ButtonPressed" Canvas.Left="148" VerticalAlignment="Center" MouseEnter="MouseEnter" MouseLeave="MouseLeave">Redo</Button>
C#代码
DispatcherTimer timer; int elapsedTime = 0; int interval = 1; int totalTime = 5; string name = null; public void MouseEnter(object sender, MouseEventArgs e) { if (e.OriginalSource is not Button) { return; } //if (name != null) // return; Button hoveredButton = e.OriginalSource as Button; name = hoveredButton.Name; elapsedTime = 0; timer = new DispatcherTimer(); timer.IsEnabled = true; timer.Interval = TimeSpan.FromMilliseconds(interval); timer.Tick += delegate { MouseAnimation(1, hoveredButton); }; } public void MouseAnimation(int resize, Button button) { timer.Stop(); if (elapsedTime > totalTime) { elapsedTime = 0; return; } timer.IsEnabled = true; button.Height = button.ActualHeight + resize; button.Width = button.ActualWidth + resize; elapsedTime += interval; } public void MouseLeave(object sender, MouseEventArgs e) { //if (elapsedTime > 0) // return; if (e.OriginalSource is not Button) { return; } Button hoveredButton = e.OriginalSource as Button; //if (hoveredButton.Name != name) // return; //if (name == null) // return; Button tempButton = (Button)e.OriginalSource; //if(e.OriginalSource != hoveredButton) //{ // return; //} elapsedTime = 0; timer = new DispatcherTimer(); timer.IsEnabled = true; timer.Interval = TimeSpan.FromMilliseconds(interval); timer.Tick += delegate { MouseAnimation(-1, hoveredButton); }; timer.IsEnabled = true; name = null; }
问题根源
你的代码存在几个核心问题:
- 全局Timer复用冲突:所有按钮共享同一个
timer变量,快速切换悬停时,新Timer会覆盖旧的,但旧Timer的Tick事件可能还在执行,导致多个动画逻辑同时操作按钮尺寸。 - 状态变量全局共享:
elapsedTime和name是全局变量,多个按钮的动画会互相干扰,无法独立跟踪每个按钮的动画状态。 - 动画未正确终止:触发新动画时,旧的Timer没有被彻底停止和释放,残留的Tick回调会继续修改按钮尺寸。
修复方案
方案1:使用WPF原生DoubleAnimation(推荐)
WPF自带的动画系统会自动处理动画中断、并发冲突等问题,比手动用Timer实现更可靠简洁:
XAML实现(无需后台代码):
<Button Name="Redo" Click="ButtonPressed" Canvas.Left="148" VerticalAlignment="Center"> Redo <Button.Style> <Style TargetType="Button"> <!-- 设置缩放原点为按钮中心 --> <Setter Property="RenderTransformOrigin" Value="0.5,0.5"/> <Setter Property="RenderTransform"> <Setter.Value> <ScaleTransform ScaleX="1" ScaleY="1"/> </Setter.Value> </Setter> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Trigger.EnterActions> <BeginStoryboard> <Storyboard> <!-- 放大动画,时长50ms --> <DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleX" To="1.1" Duration="0:0:0.05"/> <DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleY" To="1.1" Duration="0:0:0.05"/> </Storyboard> </BeginStoryboard> </Trigger.EnterActions> <Trigger.ExitActions> <BeginStoryboard> <Storyboard> <!-- 恢复原尺寸动画 --> <DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleX" To="1" Duration="0:0:0.05"/> <DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleY" To="1" Duration="0:0:0.05"/> </Storyboard> </BeginStoryboard> </Trigger.ExitActions> </Trigger> </Style.Triggers> </Style> </Button.Style> </Button>
后台代码实现(支持动态控制):
private void MouseEnter(object sender, MouseEventArgs e) { if (sender is Button button) { StartScaleAnimation(button, 1.1); } } private void MouseLeave(object sender, MouseEventArgs e) { if (sender is Button button) { StartScaleAnimation(button, 1.0); } } private void StartScaleAnimation(Button button, double targetScale) { // 确保按钮绑定了ScaleTransform if (button.RenderTransform is not ScaleTransform scaleTransform) { scaleTransform = new ScaleTransform(1, 1); button.RenderTransform = scaleTransform; button.RenderTransformOrigin = new Point(0.5, 0.5); } // 停止当前动画,避免冲突 scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, null); scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, null); // 创建并启动新动画 var animX = new DoubleAnimation(targetScale, TimeSpan.FromMilliseconds(50)); var animY = new DoubleAnimation(targetScale, TimeSpan.FromMilliseconds(50)); scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, animX); scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, animY); }
方案2:修复原有Timer方案(适合理解原理)
为每个按钮维护独立的Timer和状态,用字典存储每个按钮的动画信息:
// 存储每个按钮的动画状态:Timer+已流逝时间 private Dictionary<string, (DispatcherTimer Timer, int ElapsedTime)> _buttonAnimations = new(); private int _interval = 1; private int _totalTime = 5; public void MouseEnter(object sender, MouseEventArgs e) { if (sender is not Button button) return; // 停止该按钮当前的动画(如果存在) if (_buttonAnimations.TryGetValue(button.Name, out var state)) { state.Timer.Stop(); state.Timer.Tick -= (s, args) => UpdateButtonSize(button, 1); _buttonAnimations.Remove(button.Name); } var timer = new DispatcherTimer(); timer.Interval = TimeSpan.FromMilliseconds(_interval); int elapsedTime = 0; timer.Tick += (s, args) => UpdateButtonSize(button, 1, ref elapsedTime, timer); _buttonAnimations[button.Name] = (timer, elapsedTime); timer.Start(); } public void MouseLeave(object sender, MouseEventArgs e) { if (sender is not Button button) return; // 停止该按钮当前的动画(如果存在) if (_buttonAnimations.TryGetValue(button.Name, out var state)) { state.Timer.Stop(); state.Timer.Tick -= (s, args) => UpdateButtonSize(button, -1); _buttonAnimations.Remove(button.Name); } var timer = new DispatcherTimer(); timer.Interval = TimeSpan.FromMilliseconds(_interval); int elapsedTime = 0; timer.Tick += (s, args) => UpdateButtonSize(button, -1, ref elapsedTime, timer); _buttonAnimations[button.Name] = (timer, elapsedTime); timer.Start(); } private void UpdateButtonSize(Button button, int resize, ref int elapsedTime, DispatcherTimer timer) { if (elapsedTime > _totalTime) { timer.Stop(); _buttonAnimations.Remove(button.Name); return; } button.Height += resize; button.Width += resize; elapsedTime += _interval; }
为什么原有方案失败?
- 全局
timer变量被多次覆盖,旧Timer的回调仍持有按钮引用,会继续执行动画逻辑,导致多个动画同时修改同一按钮尺寸。 elapsedTime是全局变量,多个动画会互相覆盖该值,导致动画提前终止或无法正确完成。- 触发新动画时没有彻底停止并释放旧Timer,残留的回调会干扰新动画的执行。
内容的提问来源于stack exchange,提问作者Heliophobia
相关产品推荐
相关产品推荐

