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.
private Dictionary<string, ImageSource> _imageCache;
private BitmapSource CreateSourceFromBitmap(System.Drawing.Bitmap bitmap)
{
return System.Windows.Interop.Imaging.CreateBitmapSourceFrom HBitmap(
bitmap.GetHbitmap(),
IntPtr.Zero,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions()
);
}
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.
<Grid x:Name="_grid_Content_QuickBar">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="38">
<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.
private Dictionary<string, Action> _buttonEventHandlers;
private void RegisterEventHandlers()
{
_buttonEventHandlers = new Dictionary<string, Action>();
_buttonEventHandlers.Add(rect_Search.Name, SearchClick);
}
private void QuickMenu_MouseEnter(object sender, RoutedEventArgs e)
{
Rectangle rect = sender as Rectangle;
if (rect != null)
{
rest.Stroke = Brushes.Thistle;
}
}
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);
}
}
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.
private void QuickButtonClicked(Rectange rect, ImageSource originalImg, ImageSource alternateImg)
{
ObjectAnimationUsingKeyFrames animation = new ObjectAnimationUsingKeyFrames()
{
AutoReverse = true,
Duration = new Duration(TimeSpan.FromSeconds(0.125)),
RepeatBehavior = new RepeatBehavior(2D)
};
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);
}
private void SearchClick()
{
QuickButtonClicked(rect_Search, _imageCache["Search"], _imageCache["SearchInverted"]);
}
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 KeyFrame
s.
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