DekGenius.com
[ Team LiB ] Previous Section Next Section

7.1 Drawing and Controls

Every control owns an area of the screen, and it is responsible for drawing its visual representation onto that part of the display. So far we have not had to deal with this—we have relied on the fact that the built-in controls all draw themselves. But to customize the appearance of our applications, we must first understand the mechanism by which user interface elements are displayed.

The approach used by Windows Forms (and indeed by Windows itself) is that every time any part of a control becomes visible, it is asked to redraw itself. This usually happens for one of two reasons: either the control is being displayed for the first time (e.g., it is on a window that has just appeared), or it was behind some other window that has just been moved out of its way.

In either case, the framework will call the control's OnPaint method. All custom controls will need to override this to draw themselves. This is why Visual Studio .NET automatically supplies an override of the OnPaint method when you add a new custom control to your project. Its signature looks like this:

// C#
protected override void OnPaint(PaintEventArgs pe)

' VB
Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)

Your OnPaint method must always call the base class's OnPaint method as the last thing it does, so Visual Studio .NET adds this call for you. But you must supply the code to draw your control, using the facilities supplied in the PaintEventArgs parameter. The PaintEventArgs object contains two properties. The first, ClipRectangle, tells you which part of your control needs redrawing. If your control is partially obscured by a window, when that window closes or moves, only the parts of your control that were hidden need to be redrawn; the ClipRectangle property provides a Rectangle that indicates which part that is. The other property is Graphics, which returns an object that allows us to draw things—this is our entry point into GDI+. The use of Graphics objects is the subject of the majority of this chapter.

7.1.1 Forcing a Redraw

It is possible to get the framework to call OnPaint even when a control is already visible. This is useful because you might want to change your control's appearance due to some change in status. (For example, if your control displays any text, it will need to make sure that it is redrawn whenever the Text or Font properties are changed.) The Control class provides a method to do this: Invalidate. The Invalidate method is overloaded—if you pass no parameters, the entire control will be redrawn, but you can also specify smaller areas to be updated.

Invalidate does not call OnPaint directly—it simply tells the framework that all or part of the control is invalid (i.e., it should be redrawn at some point). If the area in question happens not to be visible, it won't be redrawn until it becomes visible, so you might not see a call to OnPaint for every call to Invalidate. The framework may also aggregate multiple calls to Invalidate into a single OnPaint. If you call Invalidate inside some event handler, the framework will typically not bother to call OnPaint until you have returned from that handler, so if you call it more than once, these calls will be summarized into a single call to OnPaint after your handler returns. (If you need the framework to update the control before you return, calling the Control class's Update method will cause any invalidated areas to be painted immediately. You can also call the Refresh method, which has the same effect as calling Invalidate followed by Update. However, it does not let you specify the area to be invalidated, so if you need to be selective, use Invalidate instead.)

7.1.2 Painting Other Controls

Methods whose names begin with On are usually associated with an event, and OnPaint is no exception—the Control class provides a corresponding Paint event. Its delegate type is PaintEventHandler, and as you would expect, handlers of this type are provided with the same PaintEventArgs object as the OnPaint method. Client code that handles a control's Paint event gets to draw whatever it likes into the control on top of what the control itself paints.

You might have only limited success using the Paint event with certain built-in controls, because some rely on the underlying Windows operating system to do some of their drawing instead of the OnPaint method.


7.1.3 Flicker-Free Drawing

Whenever a control is redrawn, either as a result of normal window activity, or an explicit call to Invalidate, the OnPaint method is effectively starting from a blank canvas. This is because the control always clears its background before calling OnPaint. (If it didn't, then unless OnPaint happened to paint the entire control area itself, whatever was on the screen before the control appeared would still be visible, leading to a mangled display.)

The control background is painted in the Control class's OnPaintBackground method, which is called directly before OnPaint. If you want something other than the default control colored background, you can override OnPaintBackground. Note that OnPaintBackground is unusual in that there is no corresponding public PaintBackground event.


Although it is useful that the control's background is automatically cleared, it does cause one problem. The OnPaint method takes a certain amount of time to run. This means that there is a short period when the control will be blank, rather than showing its contents. The OnPaint method usually runs quickly enough for this to be unobtrusive most of the time. However, if you redraw a control frequently (e.g., you update it regularly to indicate the status of your program), this two-stage redrawing will cause the control to flicker occasionally. This can range from barely noticeable to highly intrusive—it depends on various factors such as the frequency of updates, the complexity of the OnPaint method, and the speed of the computer.

Ideally, we would like to prevent this flickering. But there is only one way to achieve this: the redrawing process must paint the control in one fell swoop. The flickering is caused by the fact that the drawing process does not happen instantaneously—it is the result of catching a glimpse of the control in a partially drawn state. So we must draw the control atomically to avoid flicker.[1]

[1] Game authors and television engineers will point out that there is another way—synchronizing your redraw code with the refresh of the monitor. Unfortunately, Windows has only ever exposed such support through DirectX, which is not yet directly supported by .NET. Even then, it is difficult to guarantee the absence of flicker, because this is a real-time technique, and Windows is not a real-time operating system. So in practice, we must aim for an atomic redraw.

There is a well-known technique for achieving a single-step redraw, known as double buffering. It involves drawing everything into an off-screen bitmap and only transferring the results onto the screen once drawing is complete. When you invalidate a control that uses such a technique, the screen never contains any intermediate contents—the very first change to hit the screen will be the fully drawn end result as it is copied from the off-screen buffer. At no time does the screen contain the initial background color, nor does it ever contain half-drawn results. It either contains the previous version or the new version, thus avoiding flicker.[2]

[2] Pedants will observe that you might catch the occasional glimpse in which half of the control shows the old image and half contains the new image. This won't cause flicker, but if you are animating the control, it can cause a different artifact known as tearing. To fix this, you will need to use some rather more exotic techniques involving DirectX. GDI+ is not designed to display broadcast-quality moving pictures.

With classic Win32 programming, the only way to use this technique was to write code to do it yourself. Fortunately, Windows Forms can do all the work for you. All you need to do is tell it that you would like double buffering switched on for your control, and the framework will take care of it. You don't need to make any changes to the way your OnPaint method works; just add the following code to your constructor:

// C#
SetStyle(ControlStyles.DoubleBuffer |
    ControlStyles.AllPaintingInWmPaint |
    ControlStyles.UserPaint,
    true);

' VB
SetStyle(ControlStyles.DoubleBuffer Or
    ControlStyles.AllPaintingInWmPaint Or
    ControlStyles.UserPaint,
    true);

The first style, DoubleBuffer, is self-explanatory. The other two must be set for double buffering to work. If you're curious as to what they actually do, AllPaintingInWmPaint makes sure that when the control erases the background before calling OnPaint, it does that erasing in the off-screen buffer, not on the screen. UserPaint indicates that this control manages its own painting, which discourages the operating system from trying to help with the drawing; this is important because the whole point is to try to do the drawing in a single step.

Because double buffering is so simple to use in .NET, you might be wondering why it isn't always on. The main reason is that it's not free—memory must be allocated for the off-screen buffer. The cost of this depends on the size of the control. For something the size of a button, the control occupies about 1500 pixels. Most modern PCs have a 32-bit color display, which means that 1500 pixels occupy about 6 KB of memory. On a current PC, 6 KB is as close to nothing as makes no difference, so the double buffering cost here is very low. But some controls are larger—a control representing a document may even be the size of the screen if the user maximizes the application's windows. For a 1600 x 1200 32-bit color display, the screen occupies over 7 MB of memory. Memory may be cheap, but even with current technology, 7 MB is large enough to make you think twice.

The memory is only allocated temporarily. This means that a large control does not require memory for the whole time that it is visible, only while it is being redrawn. So the working set implications are not as bad as they could be, but double buffering does make the garbage collector work a lot harder.


So double buffering should only be used on controls for which the difference it makes is worth the memory it requires. This is not always an easy judgment to make. For controls that are never updated, it is probably not worth using double buffering—its purpose is to avoid flicker, which is something that only afflicts controls that change their appearance from time to time. If your control never calls Invalidate, it probably doesn't need double buffering.[3] For large controls, the frequency of update is probably the single most significant factor. Drawing programs almost certainly want double buffering turned on because they can update the display tens of times a second when items are being dragged around with the mouse. But for a large control that only updates its display a few times a day, it would be hard to justify the overhead.

[3] The exception here is if you have set the ResizeRedraw control style—this will cause the control to be redrawn every time the control is resized.

It may also be possible to get a flicker-free display without needing to turn on double buffering. This happens in the fairly unusual case when your OnPaint method only needs to draw one thing that covers the entire control. In this case, you already have an atomic redraw, so there is no need for double buffering. This usually only happens when your control just shows a bitmap or draws a filled rectangle the size of the control. (As we will see later, you can get some interesting visual effects by drawing a single rectangle with some exotic GDI+ options turned on, so this is not as pointless as it sounds.) If your control fits into this category, you can tell Windows Forms by putting the following code into your constructor:

SetStyle(ControlStyles.Opaque, true);

This tells the framework that your control completely covers its whole area when it redraws itself. This causes the framework not to bother filling the control with the default background before calling your OnPaint method, thus eliminating flicker without the overhead of double buffering. But your control must fill its entire area if you set this flag. If it doesn't, the control will look rather strange, with garbage appearing in the parts that you leave blank.

    [ Team LiB ] Previous Section Next Section