Flylib.com

Books Software

 
 
 

Where Are We?


Where Are We?

This chapter deals with topics that are often dialog- related : getting data in and out, validating data, and letting users know about the required data format (including providing access to online help). But none of these topics is specific to "dialogs" (which is a slippery term to get hold of anyway). However, in this chapter and Chapter 2: Forms, we've covered almost everything a programmer needs to know about forms. If there are things that you haven't seen that interest you, such as drag and drop and form localization, you'll want to read on, especially Chapter 8: Controls (for drag and drop) and Chapter 10: Resources (for form localization).


Chapter 4. Drawing Basics

As handy as forms are, especially when laden with controls, sometimes the built-in controls [1] aren't sufficient to render the state of your application. In that case, you need to draw the state yourself. The drawing may be to the screen, to a file, or to a printer, but wherever you're drawing to, you'll be dealing with the same primitives ”colors, brushes, pens, and fonts ”and the same kinds of things to draw: shapes , images, and strings. This chapter starts by examining the basics of drawing to the screen and the basic building blocks of drawing.

[1] The standard controls that come with WinForms are listed in Appendix D: Standard WinForms Components and Controls.

Note that all the drawing techniques discussed in this chapter and in the next two chapters relate equally as well to controls as they do to forms. For information about building custom controls, see Chapter 8: Controls.

One more thing worth noting before we begin is that the System.Drawing namespace is implemented on top of GDI+ (Graphics Device Interface+), the successor to GDI. The original GDI has been a mainstay in Windows since there was a Windows, providing an abstraction over screens and printers to make writing GUI-style applications easy. [2] GDI+ is a Win32 DLL (gdiplus.dll) that ships with Windows XP and is available for older versions of Windows. GDI+ is also an unmanaged C++ class library that wraps gdiplus.dll. Because the System.Drawing classes share many of the same names with the GDI+ C++ classes, you may very well stumble onto the unmanaged classes when looking for the .NET classes in the online documentation. The concepts are the same, but the coding details are very different between unmanaged C++ and managed anything else, so keep an eye out.

[2] GDI programming certainly isn't easy when compared with System.Drawing programming, but it's orders of magnitude easier than supporting printers and video display adapters by hand, which was what DOS programmers had to do to put bread on the table.


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:


Dim drawEllipse As Boolean = False

Sub drawEllipseButton_Click(sender As Object, e As EventArgs) _
      Handles drawEllipseButton.Click

  ' Toggle whether or not to draw the ellipse
  drawEllipse = Not(drawEllipse)

  Dim g as Graphics = Me.CreateGraphics()
  Try
      If drawEllipse Then
         g.FillEllipse(Brushes.DarkBlue, Me.ClientRectangle)
      Else
         g.FillEllipse(SystemBrushes.Control, Me.ClientRectangle)
      End If
  Finally
      g.Dispose()
  End Try
End Sub

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. That's all fine, but you may wonder what the Try-Finally 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. Even though .NET implements a global garbage collection mechanism for cleaning up objects, there is still a need for programmers to explicitly release resources when they are finished. The .NET garbage collector releases only managed resources, or resources controlled by the runtime. Resources that are managed by Windows itself, however, are not managed in the .NET sense, and therefore the garbage collector can leave them in an indefinite state by cleaning up the objects that use them without releasing them explicitly.

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. Then, when the garbage collector cleans up the Graphics object, the underlying unmanaged resource has already been released.

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 Property ClipRectangle() As Rectangle
  Public Property Graphics() As Graphics
End Class

The PaintEventArgs comes with two properties: a rectangle describing the area Windows would like us to draw, and a graphics object that Windows itself will manage, relieving us of that responsibility. Because of this event, conventionally we manage drawing code for a form in the Paint event itself by using the state of the form to govern what's drawn:


Dim drawEllipse As Boolean = False

Sub drawEllipseButton_Click(sender As Object, e As EventArgs) _
  Handles drawEllipseButton.Click
  drawEllipse = Not(drawEllipse)
End Sub

Sub DrawingForm_Paint(sender As Object, e As PaintEventArgs) _
  Handles MyBase.Paint
  If Not(drawEllipse) Then Exit Sub
    Dim g As Graphics = e.Graphics
    g.FillEllipse(Brushes.DarkBlue, Me.ClientRectangle)
End Sub

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 to 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:


Sub drawEllipseButton_Click(sender As Object, e As EventArgs)
  DrawEllipse = Not(drawEllipse)
  Me.Invalidate(True) ' Ask Windows for a Paint event
                       ' for the form and its children
End Sub

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:


Sub drawEllipseButton_Click(sender As Object, e As EventArgs) _
  Handles drawEllipseButton.Click
  drawEllipse = Not drawEllipse
  ' Can do one or the other
  Me.Invalidate() ' Ask Windows for a Paint event
  Me.Update() ' Force the Paint event to happen now
  ' Or both at once
  Me.Refresh() ' Invalidate + Update
End Sub

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 Sub New

()

' This call is required by the Windows Form Designer.

InitializeComponent()

' Trigger a Paint event when the form is resized
  Me

.SetStyle(ControlStyles.ResizeRedraw,

True

)

End Sub

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.