Whilst working on an experimental composite control which contains a progress bar in the background, I decided it would be nice to fade the coloring of the standard ProgressBar a little bit, to make any text, icons or controls drawn on top of it more distinguished and readable. As a simple solution, painting a translucent white layer on top of the control seemed the way to go, so I created a Class which would implement the code to do the painting required.
This is one of those programming tasks that seem trivial, until you actually start writing your code to implement the desired functionality, as I soon discovered. Read on to follow my quest along some of the pages I have visited and the solution I managed to find.
I sincerely hope that my solution may provide an easy answer to any programmer out there still working on Windows Forms projects, encountering this problem without the option to get a third party control that does this straight out of the box.
The issue with the standard ProgressBar is that it does not support the Paint event. I have searched the web for a while, hoping to find some sample code that might provide easy answers, but to no avail. There are a few solutions out there, such as this one and this one but none of the proposed solutions fitted well with my particular project, mainly because there the progress bar is implemented in a class that derives the NativeWindow class, which does not expose the CreateGraphics method like the Control class does.
As ProgressBar is in fact a wrapper for the Windows Progress Bar common control, I then looked into the MSDN documentation on messages and styles supported for the
msctls_progress32 class. I expected to find some explanation of how to implement custom drawing using the
NM_CUSTOMDRAW notification but, much to my disappointment, I discovered that this scenario is not supported. This had me burning the midnight oil without finding much of an answer as to how it should be done, while I was convinced there must be a simple way to make this work with a minimum of code.
Then I found the About Progress Bar Controls page on MSDN, containing an explanation of a few standard Windows messages and how they are handled by the Windows Progress Bar. About the WM_PAINT message it states: "Draws the progress bar. If the wParam parameter is non-NULL, the control assumes that the value is an HDC and paints using that device context."
In other words, you can create your own device context and feed it to the control by assigning it to the
WParam parameter of the
WM_PAINT message prior to calling base! Never before had I seen such functionality with any other common control and initially I was reading the text with
NM_CUSTOMDRAW notifications in mind, so only after reading the page a few times did it sink in with me what exactly the possibilities might be of taking this approach. When it did, I decided to give it a try and as it turns out, it works like a charm!
Using the code
The source code with this article comes in the shape of a solution which contains two projects; a library and a test project. You will need VS2015 to open this solution, but the code will work in previous versions of Visual Studio.
The library contains a class named
CustomProgressBar which derives the
System.Windows.Forms.ProgressBar class and overrides the WndProc method to handle the
WM_PAINT and WM_PRINTCLIENT messages. The
WM_PRINTCLIENT message is important to handle, because the control receives it occasionally, whenever a static image of the control is required. For example, when the control is being dragged to another location in the designer, the
WM_PRINTCLIENT message is sent to create the drag image. When a control does not implement the
WM_PRINTCLIENT message, the image used when dragging differs from the appearance of the control just prior to starting the drag operation, which may seem odd to a developer using the control.
The following code snippet handles the
WM_PAINT message, which does not supply a device context with its parameters. This code creates a device context by calling the
BeginPaint windows function and then uses this device context for painting operations, before calling the
EndPaint function to release the device context.
private void WmPaint(ref Message m)
HandleRef myHandle = new HandleRef(this, Handle);
NativeMethods.PAINTSTRUCT pAINTSTRUCT = new NativeMethods.PAINTSTRUCT();
IntPtr hDC = UnsafeNativeMethods.BeginPaint(myHandle, ref pAINTSTRUCT);
m.WParam = hDC;
using (Graphics graphics = Graphics.FromHdc(hDC))
Rectangle rect = ClientRectangle;
using (SolidBrush fadeBrush = new SolidBrush(Color.FromArgb(150, Color.White)))
TextRenderer.DrawText(graphics, Value.ToString() + "%", Font, rect, ForeColor);
UnsafeNativeMethods.EndPaint(myHandle, ref pAINTSTRUCT);
Private Sub WmPaint(ByRef m As Message)
Dim myHandle As New HandleRef(Me, Handle)
Dim pAINTSTRUCT As New NativeMethods.PAINTSTRUCT()
Dim hDC As IntPtr = UnsafeNativeMethods.BeginPaint(myHandle, pAINTSTRUCT)
m.WParam = hDC
Using g As Graphics = Graphics.FromHdc(hDC)
Dim rect As Rectangle = ClientRectangle
Using fadeBrush As New SolidBrush(Color.FromArgb(Fade, Color.White))
TextRenderer.DrawText(g, Value.ToString() + "%", Font, rect, ForeColor)
The essential part of this code is the below code fragment:
m.WParam = hDC;
m.WParam = hDC
This is where our device context is being assigned to the message before calling base to have Windows do its default painting, using this provided device context. This allows the default painting to be properly synchronized with any custom drawing being done afterwards. When Windows has finished the default painting of the control, a Graphics object is created for the device context, to easily paint the translucent white layer and the text representing the progress value displayed as a percentage.
When handling the
WM_PAINT message, it is important to note that Windows sends this message profusely to create the ripple animation effects when the control is active, so any code running in response had better be fast and simple. The code in the sample project is actually a bit more elaborate than the sample above, which is intended to show the essential flow at a glance for the sake of simplicity.
As the control is now capable of displaying text, it should play nicely with its neighbors and therefore must support alignment with its displayed text in the same fashion as for example a standard Label will do in the design environment. Therefore I have added a simple
ControlDesigner class named
CustomProgressBarDesigner which overrides the SnapLines property of its base class, adding the Baseline for center-aligned text to the collection which it returns.
Points of Interest
I have spent a lot of time investigating functions like FillRect and TextOut which are reputed to be very quick and can act on a device context directly, so there would be no need to acquire a Graphics object. But unfortunately these functions appear not to support transparency. For example, the
FillRect function ignores the alpha channel of the overlay color. Any attempts made to get around this problem using nothing but native code, quickly resulted in bulky code without achieving all design goals. This is why I chose to use the Graphics object which exposes many methods that do support the use of alpha blending, allowing for a few simple lines of code to achieve the desired results. However, if you happen to know how to do this in pure native code I’d be much obliged if you would drop me a line explaining the trick.
To avoid any flickering effects, the control needs to use double-buffering. To achieve this, I override the CreateParams property to apply the
WS_EX_COMPOSITED extended control style which, as MSDN explains, "Paints all descendants of a window in bottom-to-top painting order using double-buffering". This works well with most, if not all, of the Common Controls, which ignore the
When handling the
WM_PRINTCLIENT message, there’s no need to explicitly call the BeginPaint and EndPaint functions because with the
WM_PRINTCLIENT message, a device context is already supplied in the
WParam parameter of the message.
In the source code with this article, all native method calls pass any Handle values neatly wrapped using the HandleRef Structure which, as MSDN explains: "guarantees that the managed object is not garbage collected until the platform invoke call completes" which might add to the robustness of the code.
The sample project with this article comes with proper documentation in the shape of a compiled help file which explains the public interface of this rather simple control. The help file has been compiled using Sandcastle which is a legacy Microsoft open-source help compiler that can compile documentation in several formats, based on the documentation tags provided within the source code as you can see in the sample project with this article.
- March 1, 2016 - Initial Post.