Friday 18 February 2011

.net Graphics in Windows Forms – Part 1: ControlStyles

One of the areas of .net development that doesn't get the attention I think it deserves is graphics. At the UI level, a lot of software is effectively just a bolting together of existing components that all draw themselves and so this topic is often considered a sideline. Maybe it is beyond the scope of a lot of programs, but occasionally it's very useful to know about the default drawing behaviour and how it can be altered.

Although it isn't hard to draw to a control's surface in Forms applications, the default configuration for the GDI+ system can limit the quality of the result – mainly for reasons of safety (speed, making sure something gets drawn and previously painted stuff gets erased, etc.). This is the first of three posts on graphics in Windows Forms applications. In this post, I'll discuss the ControlStyles flags held internally in objects derived from Control and how they can be set to help controls that draw themselves by overriding OnPaint(). In the next post I'll talk about painting lines and shapes (and in particular about smoothing) and in the last one, the discussion will move on to drawing text directly to the screen. On to the first topic…


System.Windows.Forms.Control has a private enum variable called controlSyle, which is of type System.Windows.Forms.ControlStyles. Amongst other things, the flags contained in this enum can alter the way the control is painted in several respects. In particular, you can use it to:
  • Inform the Window Manager that you want to be in charge of drawing a control if you don't need it to do anything (which removes pointless and potentially time-consuming calls during painting)
  • Use Double-buffering for complicated (i.e. time-consuming) drawing
  • Ask for painting to be redone any time the control is resized
The really good news about these settings (even the relatively complicated double-buffering) is that all you need to know is what the settings are for: any heavy lifting is done for you by the framework and you just have to ask.


As I said, ControlStyles is a flag-style enum (i.e. the possible values are independent of one another and several can be set at once). The variable itself is private, but it's exposed using the getter / setter methods GetStyle() and SetStyle(). However, these are protected, so deriving from an object in the Control hierarchy is the only way to access them. This isn't a problem because deriving a new control from an existing one (or simply from Control itself) is really what you want to do any time you're changing behaviour like painting.


Don't forget when you're dealing with a derived control that the recommended way to access events is not to subscribe to them, but override the OnEvent() methods of the base class (OnPaint(), OnClick(), etc.). Whether you then call the base method is up to you, but as the base method actually raises the event, make sure there are no subscribers if you want to miss this out.
 

The flags that are of interest to this discussion are detailed in the table below:

Flag
Description
UserPaint
Cause the Paint event to be raised when repainting is necessary
AllPaintingInWmPaint
Prevent the control from being erased by the OS before Paint is raised
OptimizedDoubleBuffer
Construct control in buffer before outputting to screen
ResizeRedraw
Repaint the control automatically if resized

UserPaint causes the Paint event to be raised when redraw is necessary. This flag will be set to false on controls the OS draws without input from the program. See the note below about this flag in combination with some of the others, but in general, if you're painting, you'll need this set to true.
 

AllPaintingInWmPaint simply says that the control wants to be responsible for the entire painting operation. What this means in practice is that the PaintBackground event is not raised when a WM_ERASEBKGND message is received from the Windows message pump. Running the (potentially lengthy) implementation of OnPaintBackground in Control is pointless if you are just going to replace the entire area with something else (a picture or some other graphic that fills the control). This flag can greatly reduce flickering on controls where OnPaint() always redraws every pixel of the invalidated region (or can easily be made to).
 

In fact, MSDN suggests that when this flag is true, PaintBackground is raised instead from the WM_PAINT Windows event, and you sometimes see a recommendation to override OnPaintBackground with an empty method specifically so the base method is never called. When I experimented with this, I found OnPaintBackground was never called if AllPaintingInWmPaint was set to true. Your mileage may vary, of course, and I'd be interested to hear any counter-examples. However, I can't see the point of making it possible to not call it from one Windows message just to call it from a subsequent one instead.
 

OptimizedDoubleBuffer means the PaintEventArgs passed to OnPaint() references the Graphics object of an off-screen surface and swaps that onto the display only when painting has been completed. This is another flicker-reducer and works especially well if the painting takes any length of time and / or is made up of layers (which the user can often see being constructed). Obviously, it does introduce an extra step into each Paint event and may not be necessary so should be experimented with rather than simply used all the time.
 

There is another member of the enum, "DoubleBuffer" which you may come across (previous to .net 2.0, it was all that existed). In current versions of the framework, it is hidden from Intellisense so you won't see it in the IDE. Despite this, there is a (presumably erroneous) note on the MSDN ControlStyles entry saying it is preferred over OptimizedDoubleBuffer.
 

MSDN says that UserPaint must be true for either of AllPaintingInWmPaint and OptimizedDoubleBuffer to work and that in order for OptimizedDoubleBuffer to work fully, AllPaintingInWmPaint should also be set to true when double buffering is desired.
 

ResizeRedraw is not really a way of improving display, but does provide a handy method for forcing the Paint event to be raised whenever a control's size has been changed. I've mentioned it here because you would normally set it together with the other flags if it is useful for your control.
 

I've seen a few contradicting examples and explanations in this area and the problems are exacerbated by the current (as of Feb 2011) MSDN literature. As well as contradiction of my findings with the PaintBackground event and the surprising recommendation of DoubleBuffer (i.e. the older flag, currently hidden from Intellisense in the IDE) over OptimizedDoubleBuffer, the ControlStyles page is ambiguous about UserPaint and DoubleBuffer (there is a statement that it should be set for the DoubleBuffer flag to work and another that says it is implied by the DoubleBuffer flag).
 

I thought it would be interesting to see which flags are set by default on some of the familiar controls. Here is the full set of flags, the numeric value of each within the enum and those set by default ('*' in the 'T' column) for a control derived from Panel:

Hex      Binary                            T Name
=========================================================================
00000001 00000000000000000000000000000001: * ContainerControl
00000002 00000000000000000000000000000010: * UserPaint
00000004 00000000000000000000000000000100:   Opaque
00000010 00000000000000000000000000010000:   ResizeRedraw
00000020 00000000000000000000000000100000:   FixedWidth
00000040 00000000000000000000000001000000:   FixedHeight
00000100 00000000000000000000000100000000: * StandardClick
00000200 00000000000000000000001000000000:   Selectable
00000400 00000000000000000000010000000000:   UserMouse
00000800 00000000000000000000100000000000: * SupportsTransparentBackColor
00001000 00000000000000000001000000000000: * StandardDoubleClick
00002000 00000000000000000010000000000000:   AllPaintingInWmPaint
00004000 00000000000000000100000000000000:   CacheText
00008000 00000000000000001000000000000000:   EnableNotifyMessage
00010000 00000000000000010000000000000000:   DoubleBuffer
00020000 00000000000000100000000000000000:   OptimizedDoubleBuffer
00040000 00000000000001000000000000000000: * UseTextForAccessibility
 

I looked at the default flags for several different types of control and found varying results:

                        Name Panel Form Button TextBox Control
==============================================================
            ContainerControl   *     *                        
                   UserPaint   *     *    *               *   
                      Opaque              *                   
                ResizeRedraw              *                   
                  FixedWidth                                  
                 FixedHeight                      *           
               StandardClick   *     *                    *   
                  Selectable         *    *       *       *   
                   UserMouse              *                   
SupportsTransparentBackColor   *          *                   
         StandardDoubleClick   *     *                    *   
        AllPaintingInWmPaint              *       *       *   
                   CacheText              *                   
         EnableNotifyMessage                                  
                DoubleBuffer                                  
       OptimizedDoubleBuffer              *                   
     UseTextForAccessibility   *     *    *               *   

Lastly, it's worth just making a note about the best way of calling SetStyle(). The method declaration is:


protected void SetStyle(ControlStyles flag, bool value)

which at first glance might lead you to suspect that only a single flag can be set at once and you may see discussions of setting these flags which list several calls to this method. However, because of the way the SetStyle method is implemented, you can combine flag values as you normally would (i.e. with the '|' OR operator) and set several at once, providing you need to set each to the same Boolean value:

SetStyle (ControlStyles.UserPaint |
ControlStyles.AllPaintingInWmPaint |
ControlStyles.OptimizedDoubleBuffer,
true);

Clearly, a single call is the more efficient way to change several flags at once.

No comments:

Post a Comment