Windows Forms provides many mechanisms for building professional custom UI controls that match the operating system style; by combining visual style renderers, system colours/brushes, the ControlPaint class and more, it’s possible to reproduce most of the standard Windows controls in user code.
There is, however, one aspect of the built-in controls that can be difficult to recreate in managed code: Starting with Windows Vista, fade animations are used for many controls (e.g. Button, ComboBox, TextBox, etc) when transitioning between states such as focus, mouse-over and button pressing. Internally, these animations are handled by the buffered paint API (part of uxtheme.dll, the library responsible for visual styles).
Most developers would be content with instantaneous visual state changes, but to the trained eye, the lack of smooth transitions can really make a custom control stand out from a built-in one. The good news is that, although there is no managed API for buffered painting, it’s relative easy to harness using PInvoke.
Buffered Paint API – The Basics
Imports
[DllImport("uxtheme")] static extern IntPtr BufferedPaintInit(); [DllImport("uxtheme")] static extern IntPtr BufferedPaintUnInit(); [DllImport("uxtheme")] static extern IntPtr BeginBufferedAnimation( IntPtr hwnd, IntPtr hdcTarget, ref Rectangle rcTarget, BP_BUFFERFORMAT dwFormat, IntPtr pPaintParams, ref BP_ANIMATIONPARAMS pAnimationParams, out IntPtr phdcFrom, out IntPtr phdcTo ); [DllImport("uxtheme")] static extern IntPtr EndBufferedAnimation(IntPtr hbpAnimation, bool fUpdateTarget); [DllImport("uxtheme")] [return: MarshalAs(UnmanagedType.Bool)] static extern bool BufferedPaintRenderAnimation(IntPtr hwnd, IntPtr hdcTarget); [DllImport("uxtheme")] static extern IntPtr BufferedPaintStopAllAnimations(IntPtr hwnd);
Usage
- You initialise/end a session (typically lasting for the life of the control) using BufferedPaintInit/BufferedPaintUnInit.
- You begin a fade animation with BeginBufferedAnimation, passing in a BP_ANIMATIONPARAMS structure to describe the transition. A handle to the animation and two empty bitmaps are returned.
- You paint the start and end frames onto the respective bitmaps (using GDI+, as if you were painting the control normally) and call EndBufferedAnimation to start playing it.
- While the animation is playing, the control’s Paint event will be fired multiple times. Check to see whether it was triggered by the buffered paint animation by checking the value returned by BufferedPaintRenderAnimation; if so, don’t paint the control yourself.
void Control_Paint(object sender, PaintEventArgs e) { IntPtr hdc = e.Graphics.GetHdc(); if (hdc != IntPtr.Zero) { // see if this paint was generated by a soft-fade animation if (!Interop.BufferedPaintRenderAnimation(Control.Handle, hdc)) { BP_ANIMATIONPARAMS animParams = new BP_ANIMATIONPARAMS(); animParams.cbSize = Marshal.SizeOf(animParams); animParams.style = BP_ANIMATIONSTYLE.BPAS_LINEAR; // set duration according to state transition animParams.dwDuration = 125; // begin the animation Rectangle rc = Control.ClientRectangle; IntPtr hdcFrom, hdcTo; IntPtr hbpAnimation = Interop.BeginBufferedAnimation( Control.Handle, hdc, ref rc, BP_BUFFERFORMAT.BPBF_COMPATIBLEBITMAP, IntPtr.Zero, ref animParams, out hdcFrom, out hdcTo ); if (hbpAnimation != IntPtr.Zero) { if (hdcFrom != IntPtr.Zero) /* paint start frame to hdcFrom */; if (hdcTo != IntPtr.Zero) /* paint end frame to hdcTo */; Interop.EndBufferedAnimation(hbpAnimation, true); } else { /* paint control normally */ } } e.Graphics.ReleaseHdc(hdc); } }
Some further notes about using the buffered paint API on Windows Forms controls:
- Animations are not supported if the control is double-buffered (DoubleBuffered property set to true or OptimizedDoubleBuffer style flag set).
- To reduce flickering, override the control’s OnPaintBackground method and don’t call the base class method. You can paint the control’s background manually when you paint the rest of it.
- Whenever the control is resized, all running animations should be stopped using BufferedPaintStopAllAnimations.
BufferedPainter – A Managed Class to Simplify Buffered Painting
Buffered painting involves a certain amount of boilerplate code. You need to add code to the control’s creation/disposal events, override the Paint event using a particular pattern and then provide an alternative method for painting the control (either to the screen or to a bitmap).
One way to eliminate this boilerplate code would be to write a base class, derived from Control, which provided this functionality. This is somewhat limiting, however, as all custom controls using buffered painting would have to inherit from this class. In reality, you are likely to want to use buffered painting both in completely new controls and when subclassing existing controls (e.g. ComboBox). For this reason, i’ve written a class which sits in isolation and attaches to any type of control.
BufferedPainter is a generic class which allows any type to be used to represent a control’s visual state; this may be an enumeration, an integer or even a more complex type. As long as the type provides an Equals method (or has a suitable default implementation), it can be used to track state transitions. A simple button control might have three states; Normal, Hot and Pushed. BufferedPainter holds information about state changes and the duration of animations between states (where desired). It stores the current visual state of the control, overrides the control’s Paint event and provides a PaintVisualState event to be handled by user code.
It also provides a mechanism to simplify the process of triggering changes in the control’s visual state; in addition to manually setting the state of the control (using the State property), you can add a trigger which changes to a particular state in response to a condition (such as the mouse being over the control). This further reduces the amount of code necessary in the control. Conditions can be specific to a region within the control’s bounds, and anchoring can be used to automatically update the region when the control is resized.
Adding buffered paint support to a control is as simple as:
// using an enum type 'MyVisualStates' to describe the control's visual state BufferedPainter<MyVisualStates> painter = new BufferedPainter<MyVisualStates>(/* control instance */); painter.PaintVisualState += /* event handler which paints the control in a particular state */; // describe the state transitions we want to animate painter.AddTransition(MyVisualStates.Normal, MyVisualStates.Hot, 125); // fade in painter.AddTransition(MyVisualStates.Hot, MyVisualStates.Pushed, 75); painter.AddTransition(MyVisualStates.Hot, MyVisualStates.Normal, 250); // fade out // describe what causes the control to change its visual state painter.AddTrigger(VisualStateTriggerTypes.Hot, MyVisualStates.Hot); // mouse over painter.AddTrigger(VisualStateTriggerTypes.Pushed, MyVisualStates.Pushed); // mouse down
Download
BufferedPainting.zip (includes example control)
Final Words
The buffered paint API closes one of the final gaps in writing custom controls in managed code which match the OS style, both in terms of static appearance and animation. I hope you find my code useful in implementing smooth transitions in your own custom controls.
Great post. I wish more developers took the time to polish their custom/owner-drawn controls.
Regarding the transition times in milliseconds: did you come up with those values yourself, or do they come from somewhere else? I can’t find any documentation on transition times for the standard controls – I don’t think those values are exposed as system metrics. The values in this sample are a little different, but the end result seems almost perfect: http://www.codeproject.com/KB/vista/vistathemebuttons.aspx
Frankly, it’s disappointing that the OS doesn’t make this easier.
The transition times used in my example are not based on any real Win32 control; in fact, they slightly exaggerate the effect for demonstration purposes. When simulating buttons, combo boxes, etc, I have just used trial and error to refine the animation times.
To follow up my own question, there is a Win32 function GetThemeTransitionDuration: http://msdn.microsoft.com/en-us/library/windows/desktop/bb759804(v=VS.85).aspx
You have a quite litle memory leak there..
if (hdcFrom != IntPtr.Zero) OnPaintVisualState(new BufferedPaintEventArgs(_currentState, Graphics.FromHdc(hdcFrom)));
if (hdcTo != IntPtr.Zero) OnPaintVisualState(new BufferedPaintEventArgs(_newState, Graphics.FromHdc(hdcTo)));
You create a new Graphics object to use the native HDC but you are not disposing it, Eventually leading to an OutOfMemoryException.
if (hdcFrom != IntPtr.Zero)
{
using (Graphics gfxFrom = Graphics.FromHdc(hdcFrom))
OnPaintVisualState(new BufferedPaintEventArgs(_currentState, gfxFrom));
}
if (hdcTo != IntPtr.Zero)
{
using (Graphics gfxTo = Graphics.FromHdc(hdcTo))
OnPaintVisualState(new BufferedPaintEventArgs(_newState, gfxTo));
}
Thanks for pointing this out. I will eventually get around to updating the code.