Painting best practices
Be not afraid… I’m not a very good artist. I remember the first and last time I was recognized for my artistic talent – kindergarten, the squirrel with the red kite. I had managed to color within the lines. For this act of brilliance, my picture was featured amongst all the others in the class who had long ago mastered this fine art. Fortunately, if you’re somewhat reasonable at basic math, you don’t even need that skill to be successful in painting in Windows Forms.Painting 101: How a control decides to paint itselfSay you change the BackColor property of a button. Somehow the button has to tell itself to repaint. The way it does this is by calling Invalidate(), which essentially says to the OS, hey when you’ve got a free minute, I’d like to be painted please. At its next convenience, it serves up a couple window messages, which are translated into events back on the Control. These are WM_ERASEBKGND and WM_PAINT which usually correspond to OnPaintBackground and OnPaint respectively. (More about exceptions to this later.) |
Other resources… |
If you’ve done much ALT+Tabbing around, you’ve likely seen the result of a WM_ERASEBKGND/OnPaintBackground. If you open up lots of copies of notepad and then switch between them, you’re likely to see a flash of white for a brief second, and then the text. In Windows Forms, the background color and the background image are typically painted in OnPaintBackground and foreground effects such as text and image are painted in OnPaint.When the control gets one of these events, it is handed a Graphics object in its PaintEventArgs. Graphics has functions off of it to draw whatever you’d like, images, rectangles, ellipsis, strings, you name it. There are even fill methods for those of us who still have trouble painting between the lines. =) |
Making custom painting look right
It’s really not as hard as you may think. If you want to go old school – enter the Windows UI Guidelines to the rescue! It breaks apart step by step the anatomy of a button, how switching the highlight color makes it look pressed and so on.
Never underestimate the power of taking a screen shot via PrintScreen (or Alt+PrintScreen) and pasting the result into Paint (mspaint.exe). The magnifier program (magnify.exe) is also quite handy for this task.
Helpful drawing functions which aren’t on Graphics
Windows Forms has provided a set of routines for painting common things, which can be found in ControlPaint – these mostly have the 3D look and feel of “classic” windows. In Windows Forms 2.0, you can draw pieces of the current “theme” by using the VisualStyleRenderer, and its friends specifically targeted to particular controls, like the ButtonRenderer. I've talked about this a bit before.
What to put in your paint method
Painting code, that’s it. Sounds silly, but it needs to be said. OnPaint is not the time to fetch stuff out of the database, change the location or size of controls, etc. Changing properties like this can cause side-effects which could get you into an infinite painting loop.
If you need to perform some work after the Form.Load event, consider telling yourself to do it later when you’re not so busy – the easiest way to do this in WindowsForms is to use the BeginInvoke method.
BeginInvoke(new EventHandler(OnLoadCompleted));
The importance of keeping clean
Most of the pieces and parts of the graphics API implement IDisposable. This is because they generally correspond to real operating resources: GDI objects. I’m talking pens, brushes, images, and graphics objects themselves here. If you don’t dispose as you go, or reuse a cached brush, you’re essentially hogging memory for your app. I’ve talked about this a bit in How, When, Where and Why to use Dispose.
Invalidate vs. Update vs. Refresh
Invalidate – marks part or all of a control as invalid, the next time there are no messages in the message queue, windows will send a WM_PAINT message to the control to repaint itself.
Update – specifies a synchronous paint of the part of the control you just called Invalidate on. Has to be used with invalidate, otherwise it is essentially a no-op.
Refresh – calls invalidate over the whole control, then update.
Use Update/Refresh sparingly, as you don’t want to block other important things from getting processed by needlessly repainting. Note that if you call invalidate several times, you may only get one paint event – as the OS will just merge this into one WM_PAINT message. Calling Update negates this perf advantage.
Sometimes calling update is necessary: this is the case where you know it’s going to be quite a while before you return from the function: e.g. a user clicks on a button, the button should appear pressed while the mouse is down – you fire a mouse down event handler, but you don’t know how long it’s going to take before the function returns.
What do the ControlStyles actually mean?
Here are some of the interesting ones:
ResizeRedraw – If you’re syncing SizeChanged or Resize event and calling Invalidate, set this bit instead.
UserPaint – Essentially means whether or not we should call OnPaint and OnPaintBackground when we get a WM_PAINT/WM_ERASEBKGND. By default this is set to true, but it’s set to false for a lot of the OS wrapped controls from ComCtl and User32 which require owner draw to change the look and feel.
AllPaintingInWmPaint - When this flag is specified, the control essentially ignores WM_ERASEBKGND and uses WM_PAINT to call both OnPaintBackground and OnPaint.
The rest are summarized well here.
Flicker-free painting – is it possible?
You’ve assigned a huge image to the background of your form, and as it resizes it just looks like a flicker fest. If you change the BackColor to HotPink, chances are you’re briefly seeing pink, then the background image paint.
Double-buffering to the rescue! When a control is double buffered, it’s first painted offscreen, then it’s copied back to the screen once all the painting has completed. DoubleBuffer is perfect when you have layers of painting going on (the BackColor, then the BackgroundImage for example) and you just want it to appear all at once.
If it’s so great, why not turn it on everywhere by default? It’s all a matter of resources - if you have to paint somewhere else first, you need to allocate a bitmap in order to paint into it. Common dialogs and forms mostly use a solid back color, so this is not typically needed.
To enable double buffering in a control, you need to call the protected SetStyle() method specifying ControlStyles.DoubleBuffer (in WindowsForms 2.0 use OptimizedDoubleBuffer – same thing better algorithm). There are a couple of other flags you’ll want to set: this stems back to the original discussion about WM_ERASEBKGND and WM_PAINT.
If you only call SetStyle(ControlStyles.DoubleBuffer, true), you still may notice flicker. This is because you’re still painting the background in a separate windows message than the foreground. The OnPaintBackground is double buffered, and the OnPaint is double buffered, but it would be more helpful if the background and the foreground were rendered together and then copied out to the screen. Enter ControlStyle.AllPaintingInWmPaint – by specifiying this, all the painting happens together in WM_PAINT, eliminating flicker.
Calling GDI functions
If there’s something in GDI you must call, you can get a handle to the DC by calling Graphics.GetHdc(). At this point, you’ll need to use platform invoke to call into GDI – some of these may already be documented up on https://www.pinvoke.net.
Note that while you have the HDC out, you cannot make any calls to the Graphics object. (This is roughly because Graphics corresponds to the GDI+ library, which cant be called while you’re working with GDI.) You need to call Graphics.ReleaseHdc() once you are done with your GDI calls.
Measuring Text
In Windows Forms 2.0, you can use TextRenderer to draw text with the GDI libraries, no p/invoke needed. Note GDI and GDI+ use dramatically different algorithms for drawing and measuring text, so if you’re using a particular technology to draw text, you should use the same technology to measure it.
High contrast considerations
Windows provides a color scheme for those with impaired eyesight. If you’re concerned about meeting Windows LOGO requirements, this is a requirement for your application. When SystemInformation.HighContrast == true, you should pick colors out of SystemColors to paint with. This will correctly map to the users color scheme.
Instructions for getting into High Contrast Mode.
Windows LOGO requirements.
Here's a mapping of the colors from GetSysColor to SystemColors:
Comments
- Anonymous
August 31, 2005
Custom PaintingPainting best practices ComboBox OwnerDrawLayoutDock layout/Using the Splitter control...