65.9K
CodeProject 正在变化。 阅读更多。
Home

C# WPF 动画图像按钮(无按钮)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (4投票s)

2016年10月11日

CPOL

3分钟阅读

viewsIcon

17158

一个可重用的类似按钮的控件, 带有点击时播放的动画图像

引言

为 Windows 应用程序改善用户交互的一种常用方法是在按钮中放置图像,以可视化它们执行的操作,尤其是在操作比“提交”操作更抽象的情况下。 实现此目的最简单的方法是将简单的位图图像用作 Button UIElement 的背景画笔。 然而,有时按钮点击动画的默认行为是不可取的。 在本文中,我希望概述一个可重用的 Rectangle,以模拟 Button 的一般行为,但具有简单的图像过渡,并显示在 Grid “快速工具栏”中。

简单来说,我试图创建一个控件,在用户点击它时闪烁图像负片,而无需使用按钮。

启动时图像缓存

为了消除显示动画时的任何延迟,我发现将所有图像加载到应用程序启动时的一个 Dictionary 集合中会更好。

这些是我在构建此控件时使用的图像。
我将这些图像作为项目中的资源添加。 它们都是 29x29 像素。

// the image dictionary
private Dictionary<string, ImageSource> _imageCache;

// helper function to convert resource bitmaps into a BitmapSource
// since resource images are loaded as plain bitmaps
private BitmapSource CreateSourceFromBitmap(System.Drawing.Bitmap bitmap)
{
  return System.Windows.Interop.Imaging.CreateBitmapSourceFrom HBitmap(
      bitmap.GetHbitmap(),
      IntPtr.Zero,
      Int32Rect.Empty,
      BitmapSizeOptions.FromEmptyOptions()
    );
}

// loading the images into memory
private void LoadImages()
{
  _imageCache = new Dictionary<string, ImageSource>();
  _imageCache.Add("Search", CreateSourceFromBitmap(Properties.Resources.Search));
  _imageCache.Add("SearchInverted", CreateSourceFromBitmap(Properties.Resources.SearchInverted));
}

这样,我们的图像就预加载好了,可以进行快速过渡动画。

快速工具栏

QuickBar 是一个简单的网格,用于包含按钮。 我在 Grid 中定义了按钮的大部分属性,以获得统一的外观。 此外,所有按钮事件都连接到单个事件,用于所有按钮。 我稍后会讨论如何处理它。 我选择这种方法,以便不需要为每个按钮处理事件处理程序。

<Grid x:Name="_grid_Content_QuickBar">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="38">
        <!-- Add more columns for more buttons -->
        <ColumnDefinition Width="1*">
    </Grid.ColumnDefinitions>
    <Grid.Resources>
        <Style TargetType="Rectangle">
            <Setter Property="VerticalAlignment" Value="Center" />
            <Setter Property="HorizontalAlignment" Value="Center" />
            <Setter Property="Margin" Value="3" />
            <Setter Property="Width" Value="32" />
            <Setter Property="Height" Value="32" />
            <Setter Property="RadiusX" Value="2" />
            <Setter Property="RadiusY" Value="2" />
            <Setter Property="Stroke" Value="#858585" />
            <Setter Property="StrokeThickness" Value="1.5" />
            <EventSetter Event="MouseEnter" Handler="QuickMenu_MouseEnter" />
            <EventSetter Event="MouseLeave" Handler="QuickMenu_MouseLeave" />
            <EventSetter Event="MouseDown" Handler="QuickMenu_MouseClick" />
        </Style>
    </Grid.Resources>
    <Rectangle Grid.Column="0" ToolTip="Launch new search" x:Name="rect_Search">
        <Rectangle.Fill>
            <ImageBrush ImageSource=".\Resources\Search.png" Stretch="Uniform"/>
        </Rectangle.Fill>
    </Rectangle>
</Grid>

事件连接

由于我决定通过单个事件处理程序处理所有事件,因此需要一些代码来处理它们。 虽然我使用了在单个处理程序中处理 MouseDown 事件的方法,但将该事件处理程序添加到每个按钮同样容易。

// event function mapper
private Dictionary<string, Action> _buttonEventHandlers;

// a method to register events. Called during app initialization 
private void RegisterEventHandlers()
{
    _buttonEventHandlers = new Dictionary<string, Action>();
    _buttonEventHandlers.Add(rect_Search.Name, SearchClick);
}

// button event handlers
// this one changes the button outline to highlight which button the mouse is over
private void QuickMenu_MouseEnter(object sender, RoutedEventArgs e)
{
    Rectangle rect = sender as Rectangle;
    
    if (rect != null)
    {
        rest.Stroke = Brushes.Thistle; // change the color of the rectangle border on mouseover
    }
}
// this one removes the highlight when the mouse leaves the button
private void QuickMenu_MouseLeave(object sender, RoutedEventArgs e)
{
    Rectangle rect = sender as Rectangle;
    
    if (rect != null)
    {
        rect.Stroke = new SolidColorBrush
         (Color.FromArgb(255, 85, 85, 85); // change the color back to the original on mouseleave
    }
}
// this one handles the user clicks
private void QuickMenu_MouseClick(object sender, MouseButtonEventArgs e)
{
    Rectangle rect = sender as Rectangle;
    Action action;
    
    if (rect != null)
    {
        action = _buttonEventHandlers[rect.Name];
        action?.Invoke();
    }
}
处理 MouseLeave 事件的一个更好的方法是在启动代码中定义默认的 SolidColorBrush,以提高一点性能,而不是每次点击时都创建一个新的画笔。

动画按钮

为了确保每个按钮正确处理其图像,我添加了这段代码,它在映射到按钮点击事件的 Action 函数中调用。 动画是通过 ObjectAnimationUsingKeyFrames 类完成的。 ObjectAnimationUsingKeyFrames 与其他 KeyFrameAnimation 类不同之处在于,帧之间没有过渡平滑处理。

// the animation code
private void QuickButtonClicked(Rectange rect, ImageSource originalImg, ImageSource alternateImg)
{
    ObjectAnimationUsingKeyFrames animation = new ObjectAnimationUsingKeyFrames()
        {
            AutoReverse = true,
            Duration = new Duration(TimeSpan.FromSeconds(0.125)), // duration is very important here
            RepeatBehavior = new RepeatBehavior(2D) // flash twice
        };
        
    var startFrame = new DiscreteObjectKeyFrame();
    startFrame.Value = originalImg;
    startFrame.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.0));
    animation.KeyFrames.Add(startFrame);
    
    var altFrame = new DiscreteObjectKeyFrame();
    altFrame.Value = alternateImg;
    altFrame.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.0625));
    animation.KeyFrame.Add(altFrame);
    
    rect.Fill.BeginAnimation(ImageBrush.ImageSourceProperty, animation);
}

// the button click handler
private void SearchClick()
{
    QuickButtonClicked(rect_Search, _imageCache["Search"], _imageCache["SearchInverted"]);
    
    // ToDo: respond to the click
}

在这里,我使用了 DiscreteObjectKeyFrame 类来定义动画的每个帧。 由于只有两个帧,所以很简单。 有两个重要部分,帧 Value,设置为将显示的图像,以及 KeyTime,指示在动画时间轴中移动到下一个帧的时间。 对于只有两个帧的情况,我将第二个 KeyTime 设置为持续时间的一半。

值得注意的是,虽然我使用了一个简单的动画,该动画在鼠标点击时快速切换两个图像,但使用更多的复杂图像动画,包含数十个 KeyFrame,应该不会有太大的困难。

结论

我仍在学习 WPF 动画的力量,以创造更好的用户体验。 由于我对此遇到了一些困难,我想分享我学到的经验。

历史

  • 2016/11/10:初始发布
© . All rights reserved.