Reviewing the System.Drawing Namespace
The System.Drawing (GDI+) namespace is one of those great features that will allow you to exercise precise control over custom drawing and your graphical user interface in Windows Forms. (You can use the classes in System.Drawing for WebForms, too.)
This section provides an overview of the System.Drawing namespace with some brief examples. For an extensive review and additional examples of GDI+, see Chapter 17, "Programming with GDI+."
Classes in the System.Drawing Namespace
There are several key classes in the Drawing namespace that are part of GDI+. Important classes include those that represent brushes, fonts, pens, drawing regions , and the Graphics class. Together these classes will allow you to create basic line art and font-based graphics output as well as advanced graphics rendering.
The Brushes class contains a long list of static properties that return system brushesBrush objects. A Brush object is used to define objects for filling the interior of graphical objects like rectangles and ellipses.
You can create an instance of a Brush object or use one of the many predefined brush objects in the Brushes class. Listing 15.4 demonstrates using Reflection to load a combo box with the names of all available brushes; this solution is more extensible and less painful than typing the brush names manually. When you select the brush represented by the name of its color in the demo application (DrawingDemo.sln), the name of the brush is displayed to the right of the combo box.
Listing 15.4 Using Reflection to list and display all brushes
1: Imports System.Drawing 2: 3: Public Class Form1 4: Inherits System.Windows.Forms.Form 5: 6: [ Windows Form Designer generated code] 7: 8: Private Sub Initialize() 9: Dim Information() As System.Reflection.PropertyInfo _ 10: = GetType(Brushes).GetProperties() 11: 12: Dim I As Integer 13: For I = 0 To Information.Length - 1 14: ComboBox1.Items.Add(Information(I).Name) 15: Next 16: 17: End Sub 18: 19: Private Sub Form1_Load(ByVal sender As System.Object, _ 20: ByVal e As System.EventArgs) Handles MyBase.Load 21: Initialize() 22: End Sub 23: 24: Private Function GetLeft() As Single 25: Return ComboBox1.Left + ComboBox1.Width + 10 26: End Function 27: 28: Private Function GetTop() As Single 29: Return Label1.Top 30: End Function 31: 32: Private Function GetBrush() As Brush 33: Return New SolidBrush(Color.FromName(ComboBox1.Text)) 34: End Function 35: 36: Private Function GetText() As String 37: Return ComboBox1.Text 38: End Function 39: 40: Private Sub ComboBox1_SelectedIndexChanged(_ 41: ByVal sender As System.Object, ByVal e As System.EventArgs) _ 42: Handles ComboBox1.SelectedIndexChanged 43: 44: Invalidate() 45: End Sub 46: 47: Private Sub Form1_Paint(ByVal sender As Object, _ 48: ByVal e As System.Windows.Forms.PaintEventArgs) _ 49: Handles MyBase.Paint 50: 51: If (GetText() = vbNullString) Then Exit Sub 52: 53: e.Graphics.DrawString(GetText(), _ 54: New Font("Courier", 16), GetBrush(), GetLeft(), GetTop()) 55: 56: End Sub 57: End Class
There are a few interesting aspects to the demo application. Initialize, beginning on line 8, uses the Reflection.PropertyInfo class to dynamically return the array of property information about the Brushes class. The names of the properties are written to ComboBox1. Because every property in Brushes consists only of Brush objects and includes all system brushes, the combo box will contain the names of all system Brush objects.
Lines 32 through 34 construct a SolidBrush object, initializing the color of the brush with the shared method Color.FromName. The names of the system brushes happen to be the names of the system colors, too. The Form.Paint handler tests to see if a brush is picked. If you select a brush, the Graphics argument representing the device context (DC) passed in the PaintEventArgs object is used to render the name of the brush to the right of the combo box. (The methods GetLeft and GetTop are refactored query methods that return offsets relative to adjacent controls.)
When you are writing text directly to a canvas, you can use an existing font object from the control containing the DC or you can create a new Font object.
Listing 15.4 demonstrates a simple way to create a new Font object on line 54. The Visual Basic .NET garbage collector (GC) will destroy the Font object when it's no longer needed, so it's perfectly acceptable to create the object inline. In fact, it's preferable to create the font object inline rather than introducing a temporary.
Suppose we wanted to display the brush using an italicized font. We could call an overloaded version of the Font constructor, passing the FontStyle to the constructor inline:
New Font("Garamond", 16, FontStyle.Italic)
The statement creates a font using the Garamond font family and the enumerated FontStyle.Italic style. If you want to combine font styles, use an or operation to combine styles. FontStyle.Italic Or FontStyle.Bold will display a bold and italicized font.
Brush objects are used to define fill color and patterns for shapes , and Pen objects are used to define line colors and styles.
We can use exactly the same technique to fill a combo box with the names of Pens. Pens, like Brushes, use the system color for the display name of the system pens in the Pens collection. Again, just as with the Brushes collection, Pens contains shared properties representing system Pen objects. Listing 15.5 demonstrates how to draw an ellipse using Graphics, Pen, and RectangleF objects.
Listing 15.5 Using Graphics, Pen, and RectangleF objects
1: Private Function RandomRect(_ 2: Optional ByVal Max As Integer = 36) As RectangleF 3: 4: Static Count As Integer = 0 5: Count += 1 : If (Count >= Max) Then Count = 0 6: Return New RectangleF(_ 7: (Panel1.ClientRectangle.Width - (Count*10))/2, 10, Count*10, 300) 8: 9: End Function 10: 11: Private Sub Panel1_Paint(ByVal sender As Object, _ 12: ByVal e As System.Windows.Forms.PaintEventArgs) _ 13: Handles Panel1.Paint 14: 15: Const Max As Integer = 36 16: Dim I As Integer 17: For I = 1 To Max 18: e.Graphics.DrawEllipse(Pens.Blue, RandomRect(Max)) 19: Next 20: 21: End Sub
Perform custom drawing in the Paint handler or override the Paint method; otherwise , when your control is invalidated, the custom drawing will be messed up.
Listing 15.5 creates some basic nested elliptical shapes. Pens are used to define lines. The listing uses a for loop and draws Max ellipses with a blue Pen and Max number of ellipses constrained by a bounding rectangle, all with the same center (see Figure 15.6).
Figure 15.6. Ellipses drawn using a blue Pen and bounding rectangles with the same center.
The Region class is used to define clipping regions in Visual Basic .NET. One humorous use for the Region was to create the dashboard for Donkey.Net, the game marking the 10-year anniversary of Visual Basic that Bill Gates played during his keynote address at TechEd 2001.
Bill Gates himself wrote donkey.bas, which shipped with QBasic, and the new version, Donkey.Net, demonstrates 3D graphics rendering and some pretty impressive VB capabilities, including Web Services, threads, and the Region class, among others.
Last time I checked, you could download the source for Donkey.Netand run over evil bunnies or piratesfrom MSDN at http://msdn.microsoft.com/vbasic/donkey.asp.
The Region class provides several capabilities for clipping text. (We will discuss GDI+ in greater detail in Chapter 17.) Figure 15.7 demonstrates the Region and GraphicsPath objects being used to create a clipped form region from a string.
Figure 15.7. The Region class can be used to create shaped forms, as shown.
Listing 15.6 contains the verbose form of the codeunrefactored code, using temporary variables demonstrating a GraphicsPath and Region that produce the string-shaped form shown in Figure 15.7.
Listing 15.6 Regions, Opacity, GraphicsPath, and Threads. Oh my!
1: Imports System.Threading 2: Public Class Form1 3: Inherits System.Windows.Forms.Form 4: 5: [ Windows Form Designer generated code ] 6: 7: Private Sub Form1_Load(ByVal sender As System.Object, _ 8: ByVal e As System.EventArgs) Handles MyBase.Load 9: 10: Dim AGraphicsPath As New Drawing2D.GraphicsPath() 11: Dim AFontFamily As New FontFamily("Haettenschweiler") 12: Dim APointF As New PointF(10, 10) 13: Dim AStringFormat As _ 14: New StringFormat(StringFormatFlags.NoWrap) 15: 16: Const Text As String = "Visual Basic .NET" & vbCrLf & " Unleashed" 17: 18: AGraphicsPath.AddString(Text, AFontFamily, _ 19: FontStyle.Bold, 100, APointF, AStringFormat) 20: 21: Region = New Region(AGraphicsPath) 22: Opacity = 0 23: 24: ThreadPool.QueueUserWorkItem(_ 25: CType(AddressOf FadeIn, WaitCallback)) 26: 27: End Sub 28: Private Done As Boolean = False 29: Private Sub FadeIn(ByVal State As Object) 30: While (Not Done) 31: Invoke(CType(AddressOf SynchOpacity, MethodInvoker)) 32: End While 33: End Sub 34: 35: Private Sub SynchOpacity() 36: Opacity += 0.1 37: Done = Opacity >= 1 38: End Sub 39: 40: End Class
Lines 10 through 21 demonstrate how to use the Region and GraphicsPath to create Figure 15.7. In short, you will need to construct a GraphicsPath object, construct a Region object, initializing the Region with the GraphicsPath object, and assign the new Region object to the Form's Region property.
Initializing the GraphicsPath Object
The GraphicsPath class is defined in the System.Drawing.Drawing2D namespace. Line 10 of Listing 15.6 constructs an instance of the GraphicsPath object, and lines 18 and 19 demonstrate using the GraphicsPath.AddString method to constrain the graphics path by the string. The FontFamily, PointF, StringFormat, and String objects created on lines 11 to 16 are used as arguments for the GraphicsPath.AddString method. (The FontStyle enumeration is used on lines 18 and 19, too.)
Basically, each of these arguments results in the creation of the image shown in Figure 15.7. FontFamily defines the font; Haettenschweiler is used in the example. PointF defines the starting top-left offset of the string-based GraphicsPath. StringFormat constrains the behavior of the string, String contains the text Visual Basic .NET Unleashed, and FontStyle.Bold describes the style of font to use.
Initializing the Region Object
The Region object constrains the clipping region based on the manner in which the GraphicsPath is initialized . There are several overloaded versions of the Region constructor, including a version that allows you to constrain the clipping region by a much simpler rectangle. The following code demonstrates a simple rectangular clipping region.
Region = New Region(New RectangleF(10, 10, 500, 100))
Replace lines 10 through 21 with the preceding single line, and you get a similar effect but a rectangular clipped region.
Using the ThreadPool
Line 24 demonstrates an easy way to use threads in Visual Basic .NET. The ThreadPool is a collection of threads managed by the CLR. Using the ThreadPool allows you to skip over the relatively expensive cost of creating a thread object, ensuring that it's initialized correctly, and writing your own thread pool. By queuing a work item, the ThreadPool will place the task described by the delegate (line 25) in a queue and the ThreadPool will process the task defined by the delegate procedure on a separate thread. The result is relatively easy multithreading.
The argument CType(AddressOf FadeIn, WaitCallback) is simply converting the delegate returned by AddressOf FadeIn to a WaitCallBack delegate. Alternatively, you could declare a WaitCallBack delegate and initialize it with the address of the FadeIn method, which has the correct signature.
The FadeIn method continues until the form's Opacity is 100%, or 1. The Opacity itself is actually changed on the same thread as the form. Because Windows Forms controls aren't thread safe, you have to use the Invoke method and a delegate to modify or interact with controls across thread boundaries. Again, on line 31 we are coercing the delegate returned by AddressOf SynchOpacity to a MethodInvoker delegate rather than explicitly declaring a MethodInvoker variable.
Control.Invoke essentially places interactions with Windows Forms controls in the same message queue on the same thread as the one on which the control resides. Rewriting the FadeIn procedure as follows might work, but isn't recommended and will likely result in sporadic and unreliable behavior:
29: Private Sub FadeIn(ByVal State As Object) 30: While (Opacity < 1) 31: Opacity += .1 32: End While 33: End Sub
To determine whether interthread access requires you to use the method invocation form of the call, you can call Control.InvokeRequired. InvokeRequired returns False if the thread on which you called the method is the same one on which the control resides. If InvokeRequired returns True, you need to call Invoke as demonstrated in Listing 15.6.
The Graphics class contains a Windows Device Context, also referred to as DC or sometimes the canvas. Graphics objects are returned by calling a control's CreateGraphics method or as one of the properties of the PaintEventArgs object passed to the Paint event handler.
Call CreateGraphics when you need a Graphics object and don't cache the object when you get it. You can't construct instances of Graphics objects manually; you must use the factory method CreateGraphics. The Graphics class is defined in the System.Drawing namespace. We will cover Graphics in detail in Chapter 17.
Structures in the System.Drawing Namespace
There are a couple of straightforward structures defined in the System.Drawing namespace, including Point, Rectangle, and Size. Point encapsulates a Cartesian X,Y location; Rectangle and RectangleF encapsulate the X, Y, Width, and Height properties for defining a rectangle, and Size represents the size of a rectangular region.
We will cover other interesting System.Drawing namespaces and elements of those namespaces in Chapter 17.