Painting techniques using Windows Forms for the Microsoft .NET Framework
Get the samples for this article. Unzip the folder and see the readme.txt file for instructions on running the samples.
This article begins by introducing the basic architecture of
painting within the .Net Framework. After a brief introduction and
sample, a few painting techniques are discussed along with their
advantages, disadvantages, and other considerations.
Introduction
Fortunately, for the typical client application written using
Windows Forms, painting/rendering/drawing is not a foremost
consideration. And why should it be? By using the .Net Framework, a
developer can drag a series of controls onto a Form, write some code
'behind the scenes' to hook up a few events, and hit F5. There you have
it, a full blown application ready for deployment! All the controls
will draw themselves, resize nicely, and scale appropriately. Every so
often there will be an application which demands a little more
attention to rendering. Games, a custom chart control, or even a
screensaver are examples of applications that will require the
programmer to develop code responding to the paint events.
This article will target those Windows Forms developers that will
benefit from employing simple drawing techniques in their applications.
To start, the basic concept of painting will be discussed. Who's
responsible for painting what? How does a Windows Form program know
when to paint? And where should all this fancy painting code exist?
After this, the basic technique of double buffering will be introduced.
You will take a look at how it works and what the trade offs are for
using such a method. Finally, there will be a discussion about
"intelligent invalidation". In other words, redrawing or erasing only
the parts of your application that are absolutely necessary. Hopefully,
these ideas and techniques will lead the reader of this article down a
path towards better (faster & more attractive) Windows Forms
applications.
It should be noted that Windows Forms uses the GDI+ graphics engine.
All of the painting code in this article references the managed
wrappers defined by the .Net Framework to manipulate the GDI+ graphics
engine.
Although this article does start out with a basic introduction of
how painting a Form works it does accelerate quickly towards more
performance-oriented techniques and ideas. Because of this, it is
recommended that the reader have a basic knowledge of the .Net
Framework including event handling in Windows Forms, and simple GDI+
objects such as lines, pens, and brushes. A solid understanding of the
Visual Basic and/or the C# programming language is also necessary.
The Concept Of Painting
Every Windows application is responsible for painting itself. An
application will receive painting information when it is "dirtied". In
other words, when the window is resized, partially covered by other
applications, or even restored from a minimized state. Windows has
named this transition to a dirty state "invalidated". When a Windows
Form application is invalidated, it will receive painting information
from Windows packaged into messages passed into the application. From
here, this information is parsed and passed along to the Form's
PaintBackground and Paint events. These events are the proper locations
for adding your own specialized painting code.
A simple painting exercise:
Imports System
Imports System.Drawing
Imports System.Windows.Forms
Public Class BasicX
Inherits Form
Public Sub New()
InitializeComponent()
End Sub 'New
Private Sub BasicX_Paint(sender As Object, e As PaintEventArgs) Handles MyBase.BasicX_Paint
Dim g As Graphics = e.Graphics
Dim p As New Pen(Color.Red)
Dim width As Integer = ClientRectangle.Width
Dim height As Integer = ClientRectangle.Height
g.DrawLine(p, 0, 0, width, height)
g.DrawLine(p, 0, height, width, 0)
p.Dispose()
End Sub 'BasicX_Paint
Private Sub InitializeComponent()
Me.SetStyle(ControlStyles.ResizeRedraw, True)
Me.ClientSize = New System.Drawing.Size(300, 300)
Me.Text = "BasicX"
End Sub 'InitializeComponent
Public Shared _
Sub Main()
Application.Run(New BasicX())
End Sub 'Main
End Class 'BasicX
The above code can be divided into two basic steps for creating this
sample application. First, within the InitializeComponent method, a few
properties are set and an event handler is added to respond to the
Form's Paint event. Note that a control style is being set in this
method as well. Control styles are a way of customizing some behavior
of a Windows Forms control. For example, the "ResizeRedraw" control
style in this form dictates that every time the Form is resized it is
completely invalidated - meaning that the information passed into the
paint method will always claim that the entire client area of the Form
needs to be redrawn. The "client area" is defined as the body of the
form everything excluding the title bar and borders of the window. As
an interesting exercise, remove this style set in the sample code and
run the application. It should be apparent as to why this style is
often convenient to set - only the invalidated areas of the form are
redrawn.
Now, pay attention to the BasicX_Paint method. As stated earlier,
the Paint event will fire whenever the application is invalidated.
Since the sample Form listens to this Paint event, it can respond
appropriately to the message. Note that the BasicX_Paint method is
invoked with an object (the sender) and a PaintEventArgs class (defined
as 'e' in this case). The PaintEventArgs class contains two important
pieces of information. The first being the Graphics object of the Form.
This Graphics object, obtained from a property on the PaintEventArgs
class, represents the Form's rendering surface or "canvas" for drawing
lines, text, images, etc The second piece of information surfaced from
a property in the PaintEventArgs class is the ClipRectangle. This is a
Rectangle object representing the invalidated area of the Form - or the
area of the Form that needs to be repainted. Keep in mind, that the
"ResizeRedraw" control style will set this Rectangle to always be the
same size as the Form's client area when resized, or the obscured area
if covered by another application. The use of a clipping rectangle will
be discussed in further detail in the intelligent invalidation section
further along in this article.
The BasicX sample application up and running
Double Buffering, Made Nice & Easy
Double buffering is a technique used to make drawing intensive
applications faster and appear smoother by reducing flicker. The basic
idea is to take the drawing operations used to paint your application
and apply them to an off-screen canvas. Once all of the drawing
operations have finished, this off-screen canvas is then 'pushed
forward' to the surface - or drawn as a single image onto the control.
This gives the user the appearance of a much faster application.
The samples included in this section illustrate the concept and
implementation of double buffering. However, in real world use, this
functionality is integrated into Windows Forms. This behavior can be
leveraged by setting a control style, which will be mentioned at the
end of this section.
To see the benefits of this simple double buffering technique, run
the SpiderWeb sample. Once the program is up and running, resize the
Form. Grab the lower right corner of the Form and quickly move the
mouse around the screen. You can see the algorithm used to draw the
lines is not very efficient resulting in a great deal of flicker.
The SpiderWeb sample application, with no double buffering
Upon inspection of the SpiderWeb source code, you can see that
drawing these lines is done through a method called LineDrawRoutine,
which is invoked from the application's paint method. The
LineDrawRoutine method takes two arguments: 1) the Graphics object on
which to draw the lines, and 2) the Pen object (the rendering tool) to
draw the lines with. The code is fairly straightforward: looping
through several iterations, defined by the LINEFREQ constant, it draws
a line from the left side of the Form's surface to the top. Note that
this sample uses the float data type to calculate the drawing positions
onto the Form, so to provide better accuracy when resizing.
Private Sub LineDrawRoutine(g As Graphics, p As Pen)
Dim width As Single = ClientRectangle.Width
Dim height As Single = ClientRectangle.Height
Dim xDelta As Single = width / LINEFREQ
Dim yDelta As Single = height / LINEFREQ
Dim i As Integer
For i = 0 To LINEFREQ - 1
g.DrawLine(p, 0, height - yDelta * i, xDelta * i, 0)
Next i
End Sub 'LineDrawRoutine
The sample code written to respond to the Paint event,
SpiderWeb_Paint, is even simpler. As mentioned in the previous section,
The Concept Of Painting, the Graphics object representing the Form's
surface can be extracted from the PaintEventArgs object. This Graphics
object along with a newly created Pen object are passed along to the
LineDrawRoutine method to render the spider web looking lines. After
this, the Graphics and Pen objects are disposed of, and the painting
work is complete.
Private Sub SpiderWeb_Paint(sender As Object, e As PaintEventArgs)
Dim g As Graphics = e.Graphics
Dim redPen As New Pen(Color.Red)
'call our isolated drawing routing
LineDrawRoutine(g, redPen)
redPen.Dispose()
g.Dispose()
End Sub 'SpiderWeb_Paint
So, what needs to be changed to the above SpiderWeb application to
implement a simple double buffering technique? Instead of the drawing
operations being rendered to the surface of the application, they will
be redirected to an off-screen bitmap. The LineDrawRoutine can be
supplied with the hidden bitmap's Graphics object to do all the
rendering behind the scenes. After this is finished, the off-screen
image can be 'pushed forward' to the surface of the Form by using the
Graphics.DrawImage method. Finally, a few other modifications can be
done to the application which specifically targets the Form's painting
performance.
Compare the code below, defining a double buffered paint event, to the simple SpiderWeb_Paint event mentioned above:
Private Sub SpiderWeb_DblBuff_Paint(sender As Object, e As PaintEventArgs)
Dim g As Graphics = e.Graphics
Dim bluePen As New Pen(Color.Blue)
'create our offscreen bitmap
Dim localBitmap As New Bitmap(ClientRectangle.Width, ClientRectangle.Height)
Dim bitmapGraphics As Graphics = Graphics.FromImage(localBitmap)
'call our isolated drawing routing
LineDrawRoutine(bitmapGraphics, bluePen)
'push our bitmap forward to the screen
g.DrawImage(localBitmap, 0, 0)
bitmapGraphics.Dispose()
bluePen.Dispose()
localBitmap.Dispose()
g.Dispose()
End Sub
The above sample code creates a local bitmap the size of our Form's
client area (the Form's drawing surface). By making a call to
Graphics.FromImage, the Graphics object referencing the local bitmap is
returned. This object now represents the off-screen canvas and will be
used as the drawing surface for the double buffering technique.
Remember that one of the arguments to the LineDrawRoutine method is a
Graphics object. The double buffering technique can nearly be completed
by passing in the localBitmap Graphics object to the drawing routine
method. Now that the spider web looking lines have been rendered to the
localBitmap, the next step is to push this bitmap forward to the
surface of the Form with a standard DrawImage call. This results in the
spider web image being placed instantly onto the Form eliminating the
flicker of the lines being drawn individually.
These series of steps are not quite enough to efficiently complete
the technique. As mentioned before, control styles are a way of
defining generic behaviors of a Windows Forms application. To better
implement double buffering, the Opaque control style can be set. This
style dictates that the Form will not be responsible for drawing it's
background. In other words, if this style bit is set, the painting code
must be written with clearing/redrawing the background as an additional
consideration. The double buffered version of the Spider Web
application will work well with this behavior since on every paint
event, the entire surface of the Form is redrawn. To do this properly,
the surface is cleared with the back color of the From.
Public Sub New()
SetStyle(ControlStyles.ResizeRedraw Or ControlStyles.Opaque, True)
End Sub 'New
Private Sub SpiderWeb_DblBuff_Paint(sender As Object, e As PaintEventArgs)
'create our offscreen bitmap
Dim localBitmap As New Bitmap(ClientRectangle.Width, ClientRectangle.Height)
Dim bitmapGraphics As Graphics = Graphics.FromImage(localBitmap)
bitmapGraphics.Clear(BackColor)
'call our isolated drawing routing
LineDrawRoutine(bitmapGraphics, bluePen)
End Sub
The result? A much smoother rendering application. Since the spider
web lines are completely drawn off screen then pushed forward, there is
essentially no flicker. The delay that can now be perceived is the time
taken to push the image onto the Form's surface. As a finishing touch,
the user can add one additional piece of code to help bring out the
smoothness of the lines.
bitmapGraphics.SmoothingMode = SmoothingMode.AntiAlias
By placing this line of code immediately after extracting the
bitmapGraphics object from the localBitmap, the user is essentially
stating that everything drawn to this canvas should be rendered using
AntiAliasing, to smooth the jagged lines.
The finished result: SpiderWeb_DblBuff sample - A double buffered app. using AntiAliasing
After completing the simple double buffering section of this article
there are a couple of issues for the reader to recognize. Certain
controls within the .Net Framework (Button, PictureBox, Label, even the
PropertyGrid) already take advantage of this technique! These controls
are automatically double buffered by default. This is enabled by
another control style "DoubleBuffer". Any user can create a doubled
buffered control by simply setting this control style. So a user could
effectively develop a Windows Form which hosts a PictureBox that draws
the spider web looking lines. Since the PictureBox control is double
buffered, the result would be very similar to the SpiderWeb_DblBuff
application described above.
A final consideration about the double buffering idea described here
is that this is neither fully optimized or without its disadvantages.
While double buffering is a great way to reduce the flickering of your
Windows Forms painting, it is memory intensive. Essentially, using
twice the effective memory: the application's screen image as well as
the off screen image. Also, dynamically creating a Bitmap object for
every paint event is extremely expensive as is using the
Graphics.DrawImage to push the off-screen image to the surface of the
Form. The DoubleBuffer control style performs all of these
optimizations as is generally the most efficient way to do double
buffering.
The most efficient implementation of an offscreen buffer using GDI+
is implemented using a DIB Section bitmap. The implementation of the
DoubleBuffer control style takes advantage of this. DIB Sections are a
low level Win32 native bitmap that can very efficiently be rendered to
the screen.
Also, it is interesting to note that GDI+, in it's first version, is
only hardware accelerated for calls that can be directly implemented
using GDI primitives and functions. Due to this limitation features
like antialiased or translucent painting can be slow when rendered
directly to the screen. In general, although double buffering consumes
some memory, you can increase performance of operations like these by
always painting on a double buffered control.
Intelligent Invalidation, i.e. Think Before You Paint
The term "intelligent invalidation" implies that you, as a
developer, should recognize and only redraw the areas of your
application that have become invalidated. In turn, drawing only a
fraction of your Form's surface will yield better painting performance.
The idea of painting smaller more efficient sections of a Form can be
expanded to the use of Regions. By implementing Regions you can
exclude, paint, or contort certain areas of a Form to gain painting
performance.
To start, inspect the BasicClip sample application. This application
makes use of the ClipRectangle object stored in the PaintEventArgs
object. As mentioned earlier, whenever the application is resized, its
Paint event will fire. The BasicClip sample fills the clipping
rectangle with alternating colors (red and blue). After resizing the
application several times with varying speeds the reader will note that
the painted rectangles represent the invalidated (the new or dirtied)
areas of the Form.
The code within the sample's Paint event, looks something like this:
Private Sub BasicClip_Paint(sender As Object, e As PaintEventArgs)
Dim g As Graphics = e.Graphics
'swap colors
If currentBrush.Color = Color.Red Then
currentBrush.Color = Color.Blue
Else
currentBrush.Color = Color.Red
End If
g.FillRectangle(currentBrush, e.ClipRectangle)
g.Dispose()
End Sub
The only purpose this application serves is to demonstrate how efficient targeting the clipping rectangles can be.
The BasicClip sample, the colored rectangles represent the
invalidated client area as the Form is resized downward towards the
right.
To move forward with the idea of intelligent invalidation you should
explore the use of Regions as a way to specify the smallest possible
areas in which to paint. A Region is an object that can be used to
define sections of a Windows Form or control. When it comes time to
paint, the application can paint only the specific region, and painting
a small region is obviously faster than painting a large one.
To better demonstrate the use of Regions, turn your attention to the
TextClipping sample. To begin with, this sample overrides both the
OnPaintBackground and OnPaint methods. Instead of listening for events,
directly overriding these methods will guarantee that the code will be
called before other painting calls and is a more efficient way to paint
on your own control. Also, for clarity, this sample will have a Setup
method which is used to define the graphics objects used throughout the
application.
Private Sub Setup()
Dim textPath As New GraphicsPath()
textPath.AddString(displayString, FontFamily.GenericSerif, 0, 75, New Point(10, 50), New StringFormat())
textRegion = New [Region](textPath)
backgroundBrush = New TextureBrush(New Bitmap("CoffeeBeanSmall.jpg"), WrapMode.Tile)
foregroundBrush = New SolidBrush(Color.Red)
End Sub
The Setup method above first defines an empty GraphicsPath. From
here, the boundaries of a string ("Windows Forms") is added to the
GraphicsPath. A Region is then created around this GraphicsPath. This
results in an efficient area (Region) which defines the location of
where the string will be drawn onto the surface of the Form. Finally,
the Setup method will define a textured background brush and a solid
foreground brush that will be used to paint the Form.
Protected Overrides Sub OnPaintBackground(e As PaintEventArgs)
MyBase.OnPaintBackground(e)
Dim bgGraphics As Graphics = e.Graphics
bgGraphics.SetClip(textRegion, CombineMode.Exclude)
bgGraphics.FillRectangle(backgroundBrush, e.ClipRectangle)
bgGraphics.Dispose()
End Sub
The OnPaintBackground method defined above begins by immediately
invoking it's base method - this should be done to ensure all lower
level painting code is processed. Next, the Graphics object is
obtained. From the reference to this Graphics object, the clipping area
can be defined to be the textRegion object. By specifying the
CombineMode.Exclude value, this is essentially saying that no matter
where or how you paint to this Graphics object, do not render within
this textRegion.
Protected Overrides Sub OnPaint(e As PaintEventArgs)
MyBase.OnPaint(e)
Dim fgGraphics As Graphics = e.Graphics
fgGraphics.FillRegion(foregroundBrush, textRegion)
fgGraphics.Dispose()
End Sub
Finally, the OnPaint event needs to actually draw the text. This can
be easily done by using the FillRegion method supplied by the Graphics
object. By specifying the foregroundBrush and the textRegion, only this
area is painted. The result? A Windows Form application that thinks -
before it paints.
The TextClipping sample, the "Windows Forms" text is defined by
a Region. This enables the application to isolate the area for painting
purposes.
With the proper use of regions and intelligent invalidation you can
produce fast painting code that doesn't flicker and consumes less
memory than an equivalent double buffered implementation.
Conclusion
If your Windows Forms application is drawing intensive, there are
several techniques to employ that will enhance painting performance.
Making use of the proper control style and handling the Paint event
properly is a great start to a solid application. Properly using a
double buffering technique can yield much better "eye candy" results -
as long as the tradeoffs are understood. Finally, thinking before
painting can be extremely beneficial if client rectangles and Regions
are used.
Hopefully the reader will take away from this article a deeper
understanding of the .Net Framework with regards to painting and how to
better applications.
Top 5 Things To Remember
- Leverage the power of the .Net Framework. Some controls,
such as the PictureBox are double buffered automatically. Also, pay
attention to how the ResizeRedraw, Opaque, DoubleBuffer, and other
control styles can benefit you.
- Consolidate painting code. The basic logic used to render
your Windows Forms application should exist in the OnPaint and
OnPaintBackground methods. Avoid creating drawing code in methods
responding to Resize, Load, or Show events.
- Think before you paint. Obviously, draw as little as possible for the best performance. Make use of Regions and clipping rectangles.
- Employ a double buffering technique if feasible.
- Know your tradeoffs. For example: keeping global Brush
objects may benefit the speed of your application, however the memory
footprint will be larger. Also, even though the floating point data
type is larger than an integer, the use of floats will provide more
accurate scaling.
|