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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 16:06:00