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

How to Paint on Top of a ProgressBar using C#

5.00/5 (7 votes)
6 Mar 2016CPOL7 min read 39.1K   3.2K  
A simple explanation of how to synchronize custom painting with a ProgressBar's default painting.

Image 1

Introduction

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.

Background

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.

C#
private void WmPaint(ref Message m)
{
    // Create a Handle wrapper
    HandleRef myHandle = new HandleRef(this, Handle);

    // Prepare the window for painting and retrieve a device context
    NativeMethods.PAINTSTRUCT pAINTSTRUCT = new NativeMethods.PAINTSTRUCT();
    IntPtr hDC = UnsafeNativeMethods.BeginPaint(myHandle, ref pAINTSTRUCT);

    try
    {
        // Apply hDC to message
        m.WParam = hDC;

        // Let Windows paint
        base.WndProc(ref m);

        // Create a Graphics object for the device context
        using (Graphics graphics = Graphics.FromHdc(hDC))
        {
            Rectangle rect = ClientRectangle;

            // Paint a translucent white layer on top, to fade the colors
            using (SolidBrush fadeBrush = new SolidBrush(Color.FromArgb(150, Color.White)))
            {
                graphics.FillRectangle(fadeBrush, rect);
            }

            // Draw text: the Value and a percent sign
            TextRenderer.DrawText(graphics, Value.ToString() + "%", Font, rect, ForeColor);
        }
    }
    finally
    {
        // Release the device context that BeginPaint retrieved
        UnsafeNativeMethods.EndPaint(myHandle, ref pAINTSTRUCT);
    }
}
VB.NET
Private Sub WmPaint(ByRef m As Message)
    ' Create a Handle wrapper
    Dim myHandle As New HandleRef(Me, Handle)

    ' Prepare the window for painting and retrieve a device context
    Dim pAINTSTRUCT As New NativeMethods.PAINTSTRUCT()
    Dim hDC As IntPtr = UnsafeNativeMethods.BeginPaint(myHandle, pAINTSTRUCT)

    Try
        ' Apply hDC to message
        m.WParam = hDC

        ' Let Windows paint
        MyBase.WndProc(m)

        ' Create a Graphics object for the device context
        Using g As Graphics = Graphics.FromHdc(hDC)
            Dim rect As Rectangle = ClientRectangle

            ' Paint a translucent white layer on top, to fade the colors a bit
            Using fadeBrush As New SolidBrush(Color.FromArgb(Fade, Color.White))
                g.FillRectangle(fadeBrush, rect)
            End Using

            ' Draw text: the Value and a percent sign
            TextRenderer.DrawText(g, Value.ToString() + "%", Font, rect, ForeColor)
        End Using

    Finally
        ' Release the device context that BeginPaint retrieved
        UnsafeNativeMethods.EndPaint(myHandle, pAINTSTRUCT)
    End Try
End Sub

The essential part of this code is the below code fragment:

C#
// Apply hDC to message
m.WParam = hDC;

// Let Windows paint
base.WndProc(ref m);
VB.NET
' Apply hDC to message
m.WParam = hDC

' Let Windows paint
MyBase.WndProc(m)

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.

Design-Time Support

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.

Image 2

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 DoubleBuffered property.

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.

Documentation

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.

History

  • March 1, 2016 - Initial post

License

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