Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

C# WPF Animated Image Button Without the Button

4.69/5 (4 votes)
11 Oct 2016CPOL3 min read 16.7K  
A reusable button-like control with an on-click image animation

Introduction

A well known way to improve user interaction for a Windows application is to put an image in your buttons to visualize what action they perform when the action is more abstract then a 'Submit' operation. The easiest way to accomplish this has been to use a simple bitmap image as the background brush of a Button UIElement. However, there are times when the default behavior of the button's click animation is undesirable. What I desire to do in this article is outline a reusable Rectangle to mimic the general behavior of the Button but with a simple image transition and displayed in a Grid 'Quick Bar'.

Simply, I set out to create a control that would flash a image negative so the user knows they clicked it without using a Button.

On-Start Image Caching

In order to eliminate any delays when showing animations, I found it better to load all the images into a Dictionary collection when the application starts.

These are the images that I used when building this.
I added these images as resources in the project. Both are 29x29 pixels.

C#
// 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));
}

With this, our images are pre-loaded and ready to go to make a fast-transition animation.

The QuickBar

The QuickBar is a simple grid used to contain the buttons. I defined much of the button's properties in the Grid to get a uniform appearance. Additionally, all the buttons events are wired to single events for all buttons. I'll discuss how to handle that shortly. I chose this method so that event handlers for each button were unnecessary.

XML
<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>

Wiring Events

Since I decided to handle all events through a single event handler, there's a little code behind necessary to handle them. While I used the method of handling the MouseDown event in a single handler, it is just as easy to add that event handler to each button.

C#
// 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();
    }
}
A better way to handle the MouseLeave event is to define a default SolidColorBrush in your start up code to squeeze out a bit of performance instead of creating a new brush with each click.

Animating the Button

To ensure that each button handles its images correctly, I add this bit of code that is called in the Action function mapped to the button's click event. The animation is accomplished with the ObjectAnimationUsingKeyFrames class. The ObjectAnimationUsingKeyFrames differs from other KeyFrameAnimation classes in that there is no transition smoothing between frames.

C#
// 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
}

Here, I used the DiscreteObjectKeyFrame class to define each frame of the animation. Since there are only two frames, it is quite simple. There are two important parts, the frame Value, which is set to the image that will be displayed, and the KeyTime, which indicates when in the animation timeline to move to the next frame. With only two frames, I set the second KeyTime to one-half the duration.

It is noteworthy that while I used a simple animation switching quickly between two images in response to mouse clicks, it should pose little difficulty to use this with much more complex image animations with dozens of KeyFrames.

In Conclusion

I am still learning the power of WPF animations in order to create a better user experience. Since I had a bit of difficulty figuring this one out, I wanted to share the lessons learned.

History

  • 10/11/2016: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)