Drawing to the Screen


No matter what kind of drawing you're doing, the underlying abstraction that you're dealing with is the Graphics class from the System.Drawing namespace. The Graphics class provides the abstract surface on which you're drawing, whether the results of your drawing operations are displayed on the screen, stored in a file, or spooled to the printer. The Graphics class is too large to show here, but we'll come back to it again and again throughout the chapter.

One way to obtain a graphics object is to use CreateGraphics to create a new one associated with a form:

 bool drawEllipse = false; void drawEllipseButton_Click(object sender, EventArgs e) {   // Toggle whether or not to draw the ellipse   drawEllipse = !drawEllipse;  Graphics g = this.CreateGraphics();   try {  if( drawEllipse ) {       // Draw the ellipse       g.FillEllipse(Brushes.DarkBlue, this.ClientRectangle);     }     else {       // Erase the previously drawn ellipse       g.FillEllipse(SystemBrushes.Control, this.ClientRectangle);     }  }   finally {   g.Dispose();   }  } 

After we have a graphics object, we can use it to draw on the form. Because we're using the button to toggle whether or not to draw the ellipse, we either draw an ellipse in dark blue or use the system color as the background of the form. All that's fine, but you may wonder what the try-catch block is for.

Because the graphics object holds an underlying resource managed by Windows, we're responsible for releasing the resource when we're finished, even in the face of an exception, and that is what the try-finally block is for. The Graphics class, like many classes in .NET, implements the IDisposable interface. When an object implements the IDisposable interface, that's a signal for the client of that object to call the IDisposable Dispose method when the client is finished with the object. This lets the object know that it's time to clean up any resources it's holding, such as a file or a database connection. In this case, the Graphic class's implementation of IDisposable Dispose can release the underlying graphics object that it's maintaining.

To simplify things in C#, the try-catch block can be replaced with a using block:

 void drawEllipseButton_Click(object sender, EventArgs e) {  using( Graphics g = this.CreateGraphics() ) {  g.FillEllipse(Brushes.DarkBlue, this.ClientRectangle);  } // g.Dispose called automatically here  } 

The C# using block wraps the code it contains in a try block and always calls the IDisposable Dispose method at the end of the block for objects created as part of the using clause. This is a convenient shortcut for C# programmers. It's a good practice to get into and something you'll see used extensively in the rest of this book.

Handling the Paint Event

After we've got the Graphics resources managed properly, we have another issue: When the form is resized or covered and uncovered, the ellipse is not automatically redrawn. To deal with this, Windows asks a form (and all child controls) to redraw newly uncovered content via the Paint event, which provides a PaintEventArgs argument:

 class PaintEventArgs {   public Rectangle ClipRectangle { get; }   public Graphics Graphics { get; } } bool drawEllipse = false; void drawEllipseButton_Click(object sender, EventArgs e) {     drawEllipse = !drawEllipse; }  void DrawingForm_Paint(object sender, PaintEventArgs e) {  if( !drawEllipse ) return;  Graphics g = e.Graphics;  g.FillEllipse(Brushes.DarkBlue, this.ClientRectangle);  }  

By the time the Paint event is fired , the background of the form has already been drawn, [3] so any ellipse that was drawn during the last Paint event will be gone; this means that we must draw the ellipse only if the flag is set to true. However, even if we set the flag to draw the ellipse, Windows doesn't know that the state of the flag has changed, so the Paint event won't be triggered and the form won't get a chance to draw the ellipse. To avoid the need to draw the ellipse in the button's Click event as well as the form's Paint event, we must request a Paint event and let Windows know that the form needs to be redrawn.

[3] A form or control can draw its own background by overriding the OnPaintBackground method.

Triggering the Paint Event

To request a Paint event, we use the Invalidate method:

 void drawEllipseButton_Click(object sender, EventArgs e) {   drawEllipse = !drawEllipse;  this.Invalidate(true); // Ask Windows for a Paint event   // for the form and its children  } 

Now, when the user toggles the flag, we call Invalidate to let Windows know that a part of the form needs to be redrawn. However, because drawing is one of the more expensive operations, Windows will first handle all other events ”such as mouse movements, keyboard entry, and so on ”before firing the Paint event, just in case multiple areas of the form need to be redrawn at the same time.

To avoid this delay, use the Update method, which forces Windows to handle the Paint event immediately. Because invalidating and updating the entire client area of a form are so common, forms also have a Refresh method that combines the two:

 void drawEllipseButton_Click(object sender, EventArgs e) {   drawEllipse = !drawEllipse;  // Can do one or the other   this.Invalidate(true); // Ask Windows for a Paint event   // for the form and its children   this.Update(); // Force the Paint event to happen now   // Or can do both at once   this.Refresh(); // Invalidate(true) + Update  } 

However, if you can wait, it's best to let Windows handle the Paint event in its own sweet time. It's delayed for a reason: It's the slowest thing that the system does. Forcing all paints to happen immediately eliminates an important optimization.

If you've been following along with this simple example, you'll be pleased to see that pressing the button toggles whether or not the ellipse is shown on the form and that covering and uncovering the form draws as expected. However, if you resize the form, you'll be disappointed, as shown in Figures 4.1 and 4.2.

Figure 4.1. Ellipse Form before Resizing

Figure 4.2. Ellipse Form after Resizing

Notice that in Figure 4.2, it seems as if the ellipse is being drawn several times, but incompletely, as the form is resized. What's happening is that as the form is being expanded, Windows is drawing only the newly exposed rectangle, assuming that the existing rectangle doesn't need to be redrawn. Although we're redrawing the entire ellipse during each Paint event, Windows is ignoring everything outside the clip region ” that part of the form that needs redrawing ”and that leads to the strange drawing behavior. Luckily, you can set a style to request that Windows redraw the entire form during a resize:

 public DrawingForm() {   // Required for Windows Form Designer support   InitializeComponent();  // Trigger a Paint event when the form is resized   this.SetStyle(ControlStyles.ResizeRedraw, true);  } 

Forms (and controls) have several drawing styles (you'll see more in Chapter 6: Advanced Drawing). The ResizeRedraw style causes Windows to redraw the entire client area whenever the form is resized. Of course, this is less efficient, and that's why Windows defaults to the original behavior.



Windows Forms Programming in C#
Windows Forms Programming in C#
ISBN: 0321116208
EAN: 2147483647
Year: 2003
Pages: 136
Authors: Chris Sells

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net