Understanding the Rendering Process
In previous chapters of this book, you learned how to draw graphics shapes, curves, and images. In all of these cases, the Graphics object is responsible for the drawing. When we're drawing graphics objects from within a menu or button click event handler, a call to the Invalidate method becomes imperative. If we don't call this method, the form will not paint itself, but if we write the same code on a form's OnPaint or paint event handler, there is no need to invalidate the form. In this section we will find out why that's so.
13.1.1 Understanding the Paint Event
Paint event functionality is defined in the System.Windows.Forms.Control class, which is the base class for Windows Forms controls such as Label, ListBox, DataGrid, and TreeView. A paint event is fired when a control is redrawn. The Form class itself is inherited from the Control class. Figure 13.1 shows the Form class hierarchy.
Figure 13.1. The Form class hierarchy
The PaintEventArgs class provides data for the paint event. It provides two read-only properties: ClipRectangle and Graphics.ClipRectangle indicates the rectangle in which to paint, and the Graphics property indicates the Graphics object associated with the paint event of a particular control (including the form itself). Always be careful when you're dealing with the paint event because it is unpredictable and called automatically.
The Control class also provides OnPaint methods, which can be overridden in the derived classes to fire the paint event. The signature of the OnPaint method is defined as follows:
protected virtual void OnPaint( PaintEventArgs e);
As this definition shows, OnPaint takes a PaintEventArgs object as its only argument. The Graphics property of PaintEventArgs is used to get the Graphics object associated with a controlincluding the form.
13.1.2 Adding a Paint Event Handler to a Form
Adding a paint event handler for any Control-derived class is pretty simple. We write an event handler that has two parameters, of types object and PaintEventArgs:
private void MyPaintEventHandler(object sender, System.Windows.Forms.PaintEventArgs args) { }
We can give the event handler whatever name we want. After implementing this event handler, we use the parameter args (which is a PaintEventArgs object) to get the Graphics object for the control. The following code delegates the event handler for the Paint event:
this.Paint += new System.Windows.Forms.PaintEventHandler (this.MyPaintEventHandler);
The following code gives the paint event handler for a form:
private void MyPaintEventHandler(object sender, System.Windows.Forms.PaintEventArgs args) { // Write your code here }
Now we can use the PaintEventArgs object to get the Graphics object associated with the form and use the Graphics object's methods and properties to draw and fill lines, curves, shapes, text, and images. Let's draw a rectangle, an ellipse, and some text on the form, as shown in Listing 13.1.
Listing 13.1 Using the paint event handler to draw
private void MyPaintEventHandler(object sender, System.Windows.Forms.PaintEventArgs args) { // Drawing a rectangle args.Graphics.DrawRectangle( new Pen(Color.Blue, 3), new Rectangle(10, 10, 50, 50)); // Drawing an ellipse args.Graphics.FillEllipse( Brushes.Red, new Rectangle(60, 60, 100, 100)); // Drawing text args.Graphics.DrawString( "Text", new Font("Verdana", 14), new SolidBrush(Color.Green), 200, 200) ; }
Figure 13.2 shows the output from Listing 13.1. Now if the form is covered by another window and the focus returns to the form, the code on the paint event handler will repaint the form.
Figure 13.2. Drawing on a form
13.1.3 Adding a Paint Event Handler to Windows Controls
As mentioned earlier, the paint event handler can be added to any Windows control that is inherited from the Control class, such as Button, ListBox, or DataGrid. In other words, each Windows control can have a paint event handler and a Graphics object, which represents the control as a drawing canvas. That means we can use a button or a list box as a drawing canvas.
Let's add DataGrid and Button controls to a form. We will use the button and the data grid as our drawing canvases. Listing 13.2 adds the paint event methods of our Button1 and DataGrid1 controls.
Listing 13.2 Adding a paint event handler for Windows controls
// Adding a button's Paint event handler this.button1.Paint += new System.Windows.Forms.PaintEventHandler (this.TheButtonPaintEventHandler); // Adding a data grid's Paint event handler this.dataGrid1.Paint += new System.Windows.Forms.PaintEventHandler (this.TheDataGridPaintEventHandler);
Listing 13.3 gives the code for the Button and DataGrid paint event handlers. This code is useful when we need to draw graphics shapes on a control itself. For example, a column of a data grid can be used to display images or graphics shapes. In our example we draw an ellipse on these controls, instead of drawing on a form. The PaintEventArgs.Graphics object represents the Graphics object associated with a particular control. Once you have the Graphics object of a control, you are free to call its draw and fill methods.
Listing 13.3 Drawing on Windows controls
private void TheButtonPaintEventHandler(object sender, System.Windows.Forms.PaintEventArgs btnArgs) { btnArgs.Graphics.FillEllipse( Brushes.Blue, 10, 10, 100, 100); } private void TheDataGridPaintEventHandler(object sender, System.Windows.Forms.PaintEventArgs dtGridArgs) { dtGridArgs.Graphics.FillEllipse( Brushes.Blue, 10, 10, 100, 100); }
Figure 13.3 shows the output of Listing 13.3. As you can see, a button or a data grid can function as a drawing canvas. The top left-hand corner of a control is the (0, 0) coordinate of the canvas associated with that control.
Figure 13.3. Drawing on Windows controls
At this stage it is worth pointing out another big advantage that GDI+ has over GDI: the flexibility to have a Graphics object associated with a control.
13.1.4 Overriding the OnPaint Method of a Form
We have already seen this in previous chapters. We can override the OnPaint method by defining it as follows:
protected override void OnPaint( PaintEventArgs args) { // Add your drawing code here }
Then we can use the Graphics property of PaintEventArgs to draw lines, shapes, text, and images. Listing 13.4 draws a few graphics shapes and text on our form's OnPaint method. To test this code, create a Windows application and add the code to it.
Listing 13.4 Using OnPaint to draw
protected override void OnPaint( PaintEventArgs args ) { // Get the Graphics object from // PaintEventArgs Graphics g = args.Graphics; // Draw rectangle g.DrawRectangle( new Pen(Color.Blue, 3), new Rectangle(10, 10, 50, 50)); // Fill ellipse g.FillEllipse( Brushes.Red, new Rectangle(60, 60, 100, 100)); // Draw text g.DrawString("Text", new Font("Verdana", 14), new SolidBrush(Color.Green), 200, 200) ; }
13.1.5 Using Visual Studio .NET to Add the Paint Event Handler
If you are using Visual Studio .NET, the easiest way to add a paint event handler is to use the Properties windows of a form or control and add a paint event handler. We have seen examples of this in previous chapters.
13.1.6 Disposing of Graphics Objects
It is usually good programming practice to dispose of objects when you're finished using them. But it may not always be the best practice. A Graphics object must always be disposed of if it was created via the CreateGraphics method or other "CreateFrom" methods. If we use a Graphics object on a paint event or the OnPaint method from the PaintEventArgs.Graphics property, we do not have to dispose of it.
Note
Do not dispose of Graphics objects associated with Windows controls such as Button, ListBox, or DataGrid.
If you create objects such as pens and brushes, always dispose of them. Although it is acceptable practice to rely on the garbage collector, doing so may often be at the expense of application performance. Garbage collection can be a costly affair because the garbage collector checks the memory for objects that haven't been disposed of, and this process absorbs processor time. However, the Dispose method of an object tells the garbage collector that the object is finished and ready to be disposed of. Calling the Dispose method eliminates the need to have the garbage collector check memory, and thus saves processor time.
In Web pages, it is always good practice to dispose of objects as soon as they are done being used.
13.1.7 The OnPaintBackground Method
The OnPaintBackground method paints the background of a control. This method is usually overridden in the derived classes to handle the event without attaching a delegate. Calling the OnPaintBackground method calls OnPaintBackground of the base class automatically, so we do not need to call it explicitly.
13.1.8 Scope and Type of Variables and Performance
One of the best programming practices is the efficient use of variables and their scope. Before adding a new variable to a program, think for a second and ask yourself, "Do I really need this variable?" If you need a variable, do you really need it right now? The scope of variables and use of complex calculations can easily degrade the performance of your applications. Using global scope for pens, brushes, paths, and other objects may be useful instead of defining variables in the OnPaint or OnPaintBackground methods.
Let's look at a practical example: Listing 13.5 is written on a form's paint event handler, which creates pens and brushes, and draws rectangles and polygons.
Listing 13.5 Variables defined in the form's paint event handler
private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e) { // Create brushes and pens HatchBrush hatchBrush = new HatchBrush(HatchStyle.HorizontalBrick, Color.Red, Color.Blue); Pen redPen = new Pen(Color.Red, 2); Pen hatchPen = new Pen(hatchBrush, 4); SolidBrush brush = new SolidBrush(Color.Green); // Create points for curve PointF p1 = new PointF(40.0F, 50.0F); PointF p2 = new PointF(60.0F, 70.0F); PointF p3 = new PointF(80.0F, 34.0F); PointF p4 = new PointF(120.0F, 180.0F); PointF p5 = new PointF(200.0F, 150.0F); PointF[] ptsArray ={ p1, p2, p3, p4, p5 }; float x = 5.0F, y = 5.0F; float width = this.ClientRectangle.Width - 100; float height = this.ClientRectangle.Height - 100; Point pt1 = new Point(40, 30); Point pt2 = new Point(80, 100); Color [] lnColors = {Color.Black, Color.Red}; LinearGradientBrush lgBrush = new LinearGradientBrush (pt1, pt2, Color.Red, Color.Green); lgBrush.LinearColors = lnColors; lgBrush.GammaCorrection = true; // Draw objects e.Graphics.DrawPolygon(redPen, ptsArray); e.Graphics.DrawRectangle(hatchPen, x, y, width, height); e.Graphics.FillRectangle(lgBrush, 200, 200, 200, 200); // Dispose of objects lgBrush.Dispose(); brush.Dispose(); hatchPen.Dispose(); redPen.Dispose(); hatchBrush.Dispose(); }
In this example we define many variables, all of local scope. Throughout the application, the redPen, hatchBrush, hatchPen, brush, and other variables remain the same. Programmatically, it doesn't matter whether we define these variables locally or globally; the choice depends entirely on the application. It may be better to have variables defined with a global scope. If you repaint the form frequently, defining these variables globally may improve performance because time will not be wasted on re-creating the objects for each pass. On the other hand, defining objects globally may consume more resources (memory).
It is also good to avoid lengthy calculations in frequently called routines. Here's an example: Listing 13.6 draws a line in a loop. As you can see, int x and int y are defined inside the loop.
Listing 13.6 Defining variables inside a loop
for (int i = 0; i < 10000; i++) { Pen bluePen = new Pen(Color.Blue); int x = 100; int y = 100; g.DrawLine(bluePen, 0, 0, x, y); }
We can easily replace the code in Listing 13.6 with Listing 13.7, which is more efficient. If a code statement does the same thing every time a control reaches it inside a loop, it is a good idea to move that statement outside the loop to save processing cycles.
Listing 13.7 Defining variables outside a loop
Pen bluePen = new Pen(Color.Blue); int x = 100; int y = 100; for (int i = 0; i < 10000; i++) { g.DrawLine(bluePen, 0, 0, x, y); }
Sometimes using a floating point data type instead of an integer may affect the quality of a drawing, even though floating point data is costly in terms of resources.
A well-designed and well-coded application also plays a vital role in performance. For example, replacing multiple if statements with a single case statement may improve performance.