Export (0) Print
Expand All
Around the World with Visual Basic
Asynchronous Method Execution Using Delegates
Building a Progress Bar that Doesn't Progress
Calling All Operators
Create a Graphical Editor Using RichTextBox and GDI+
Creating A Breadcrumb Control
Creating a Five-Star Rating Control
Creating and Managing Secondary Threads
Data Binding Radio Buttons to a List
Deploying Assemblies
Designing With Custom Attributes
Digital Grandma
Doing Async the Easy Way
Extracting Data from .NET Assemblies
Implementing Callbacks with a Multicast Delegate
Naming and Building Assemblies in Visual Basic .NET
Programming Events of the Framework Class Libraries
Programming I/O with Streams in Visual Basic .NET
Reflection in Visual Basic .NET
Remembering User Information in Visual Basic .NET
Advanced Basics: Revisiting Operator Overloading
Scaling Up: The Very Busy Background Compiler
Synchronizing Multiple Windows Forms
Thread Synchronization
Updating the UI from a Secondary Thread
Using Inheritance in the .NET World
Using the ReaderWriterLock Class
Visual Basic: Simplify Common Tasks by Customizing the My Namespace
What's My IP Address?
Windows Forms Controls: Z-order and Copying Collections
Expand Minimize
7 out of 12 rated this helpful - Rate this topic

Using GDI+ and Visual Basic .NET

Visual Studio .NET 2003
 

Ken Getz
MCW Technologies, LLC

June 2003

Summary: Provides a simple but demonstrative example of how a clock demo can take advantage of the GDI+ features made available by the .NET Framework, as well as sharpen up your Visual Basic .NET skills. (6 printed pages)

Download the GDIPlus.msi sample file.

Note   To run the sample application, you will need a version of Microsoft Windows® with version 1.0 Service Pack (SP) 2 of the .NET Framework installed. All code presented in this article is Visual Basic® .NET version, written and tested using Visual Studio® 2002. Testing was done on a system with Windows XP Professional SP1.

Introduction
Using the Application
Application Design Issues
How It Works
Conclusion

Introduction

GDI+, a group of classes provided by the System.Drawing namespace in the .NET Framework, makes it possible for developers to easily create graphic applications by taking advantage of the graphics capabilities built into Windows. This simple application demonstrates many of the GDI+ objects and their members, including (but not limited to) working with Pen, Brush (solid and gradient), Point, Rectangle, Ellipse, and Region objects. It's amazing how many GDI+ features you can pack into a simple clock demo!

Using the Application

The sample application allows you to display the current time using either an analog display (as shown in Figure 1 below) or a digital display.

Figure 1. GDI+ handles all of the graphics work for this simple clock application

To get started, load the solution into Visual Studio .NET and press F5 to load and run the project. By default, the clock appears in its analog guise with its form border showing, but you can alter the behavior as follows:

  • Resize the form to resize the clock. The clock face always centers itself as a circle in the minimum of the width and height of the form's client area.
  • Double-click the form (or right-click and select Show Frame from the Context menu) to toggle the display of the form surrounding the clock face.

From the Context menu, you can try out these options:

  • Select Analog or Digital to display the clock in analog or digital format. The digital format is much simpler, but less interesting.
  • Select Always on Top to allow the clock to float on top of all other windows. (Selecting this option sets the form's TopMost property.)
  • Select the Run at Startup option to have the application add the appropriate entry in the Windows registry, causing the clock to load each time you log in. (To be honest, when testing this application, one of the testers liked it so much he requested this option so that the clock would always be running on his desktop. I've adopted this mode, as well. We both really miss the Windows NT® Clock application.)
  • If you're displaying the clock in analog mode, select CountDown (and an interval) to display a pie-shaped region indicating a time delay. Originally added to demonstrate the FillPie method, this feature allows you to set a timer and see a flashing warning when the time has expired.
  • If you're displaying the clock in analog mode, select Gradient (and a particular gradient) to display one of the four preset gradient fills for the clock. You can investigate the code to see how the gradients work, but each of the four gradients shows off different features of GDI+.
  • Select Fill Color, and one of the available colors, to set the clock's background color. Note that this particular menu shows off another use for the GDI+ features—this owner-draw menu includes a rectangle showing the colors. Creating owner-draw menus isn't difficult, and it's well documented. You can use this example as a starting point for your own individualized menus, displaying graphics, or bitmaps.
  • Select Text Color, which uses the color-picker common dialog to select a color for the text on the clock.

Application Design Issues

In addition to using a clock as the inspiration for this GDI+ demonstration, the sample application splits its functionality between the sample form, frmClock.vb (which handles all the menus and user-interface for the application) and the class Clock.vb (which displays the clock itself). By separating the form from the clock, you'll find that you can reuse the Clock class in any application that requires an analog clock. All you need to do is instantiate a Clock instance, set a few properties, and then call the appropriate method of the Clock object from a Paint event. See the How It Works section below for more information.

To use the Clock class outside of this application, add the Clock.vb file to your own project. The constructor for the Clock object expects you to supply a Form object:

Dim MyClock As New Clock(Me)

Then, in the code that handles your form's Paint event, call the appropriate code to display either a digital or analog version of the clock:

' Analog clock:
Draw(grfx As Graphics, Radius As Integer, Origin As Point)
' Digital clock:
Draw(grfx As Graphics, ClientRectangle As Rectangle)

For the analog clock, you must supply a radius and origin (the coordinates of the upper-left corner of the clock, not the center). For the digital clock, you simply supply a Rectangle object describing the area in which you'd like to display the clock (normally, the ClientRectangle property of a form).

You're probably expecting to find a Timer control on the form itself, updating the display every second. That's a reasonable design, but not the one used here. The Clock class itself maintains its own timer, and simply invalidates its parent form when it determines that it's time to update the display. Using this technique, you can have multiple clocks, each maintaining its own time, without needing to worry about the various timers. (It's useful to note that the clock doesn't invalidate the entire parent form—that would be too expensive, and unnecessary. Instead, the clock only invalidates the region of the form where it's displaying content—the region describing the clock.) When the Clock class invalidates the parent form (or a region of the form), the form's Paint event code runs again, and the clock gets redisplayed.

Because almost every method in GDI+ requires a Graphics object as a context in which to display output, it's simplest to pass that Graphics context as a parameter to the clock's Draw method from the Paint event of the form. The event procedure receives, as its parameter, a PaintEventArgs object that includes the form's graphics context as one of its properties. The sample project uses code like this, from the form's Paint event handler:

DemoClock.Draw(e.Graphics, Radius, Origin)

How It Works

Once you get past the concept of the seemingly backwards manner in which the application works (that is, the form calling the Draw method of the Clock class from its Paint event handler, and the timer in the Clock class raising the form's Paint event by invalidating a region of the form), you may want to dig into various aspects of how the GDI clock works.

Handling the Timer

Because you can't really be sure a timer's Tick event will occur at absolutely regular intervals, you can't set the Interval property of a Windows Timer to exactly one second and expect that your clock will update correctly. Instead, this clock uses an alternate technique. It sets the interval to 1/10 second, and each time the code runs, it compares the current second to the previously displayed second. If the values are different, the Clock class knows it's time to update the display, and then (and only then) triggers the parent form's invalidation. You'll find code like this in the Clock class's Timer_Tick event handler:

' In the class:
Private CurrentTime As DateTime = DateTime.Now

' In the Timer_Tick event handler:
Static dtmPrevious As DateTime

CurrentTime = DateTime.Now
If CurrentTime.Second <> dtmPrevious.Second Then
    dtmPrevious = CurrentTime
    ParentForm.Invalidate(InvalidRegion)
End If 

The Dreaded Trigonometry

You probably thought you would never use high-school trig again (if you even survived it once), but it's crucial when you want to work with circular objects. In this application, most of the "math" work is done in converting to and from angles around the clock face to actual points on the form display, so that the code can draw the necessary lines and circles on the screen. In the Clock class, the GetPoint, GetHourDegrees, GetMinuteDegrees, and GetSecondDegrees procedures handle the ugly work of converting from circular coordinates to rectangular coordinates, and back. (See the GetHourDegrees method for an explanation of what the code is doing.) Yes, you're going to need to dig into the dark recesses of your brain to figure out what the Sin and Cos functions are doing, but the most important facts to remember are (look at a clock face as you work through this):

  • The circle (the clock face) is divided into 360 even units (called "degrees"), with 0 degrees at the 3 o'clock position, 90 degrees at 12 o'clock, and so on. You can also treat angles as negative values, so that –90 degrees is at 6 o'clock, –180 degrees is at 9 o'clock, and so on.
  • Rectangular positions are measured as Point objects (or PointF objects, if you need floating-point accuracy), measured as x and y coordinates. In this example, the Origin parameter passed to the Draw method of the clock indicates the upper-left corner of the clock face (imagine a rectangle drawn around the clock, with the origin being the upper-left corner of that rectangle). The x coordinate grows as you move to the right. The y coordinate grows as you move down.
  • The GetPoint function converts from an angle, a center point, and a distance from the center, to a standard point. That is, this procedure converts from polar (circular) coordinates to rectangular coordinates, taking into account the particular geometry of this application.
  • The GetHour/Minute/SecondDegrees methods convert the hour, minute, and second portions of the current time and return the position of the particular clock hand, in degrees, for the current time. For example, at 21:00, the GetHourDegrees method returns –180 degrees.
  • The Clock class uses the return value from the GetHourDegrees method (and its siblings) to render the clock hands. The code calls the GDI+ DrawLine method to draw each hand, using different Pen objects for each hand. The constructor for the Clock class sets up some specific pens for use while the clock displays its time, including the use of the StartCap and EndCap properties of the Pen object. Using these properties, the hands can automatically include the arrow on one end, and the "ball" on the other.
  • The Clock class calls the GetPoint method, using a somewhat abbreviated radius, to calculate the points at which to draw the "tick" marks and the hour numbers on the clock face, as well. (The path gradient also uses this method, calculating the locations of points along the circle's edge.)

It will take some studying to completely understand the trigonometry used by the Clock class. If you only care about the GDI+ functionality, you can probably get by with just believing that it works, but in that case, you can instead focus on the methods that call GDI+ members directly, drawing lines and circles, brushes, pens, and so on.

Owner-Draw Menus

GDI+ also makes it possible (and relatively easy) to handle the drawing of each menu item from within your code. This example uses owner-draw techniques for the Fill Color menu, selecting from a range of colors described in an array in the form's class:

Private LightColors() As Color = _
{Color.LightBlue, Color.LightCoral, _
Color.LightCyan, Color.LightGoldenrodYellow, _
Color.LightGray, Color.LightGreen, _
Color.LightPink, Color.LightSalmon, _
Color.LightSeaGreen, Color.LightSkyBlue, _
Color.LightSlateGray, Color.LightSteelBlue, _
Color.LightYellow, Color.White} 

In order to create an owner-draw menu, you must set the OwnerDraw property of the menu item to True (either in code or in the designer). Setting this property moves the responsibility for creating and displaying the menu item to your code. Yyou must react to the DrawItem and MeasureItem events of each menu item, and your event-handling code must provide the necessary information to display each item. The sample application draws a small rectangle, in each color, next to text containing the name of each item. This code uses GDI+ to display both the rectangle and the text.

Setting Form Styles

Displaying a gradient on the clock isn't very difficult (see the GradientFill1, GradientFill2, and GradientFill3 procedures in Clock.vb), but updating the gradient fill takes processing power, and it's not something you want to do every second.

To solve the problem, you need some way of keeping a cached copy of the form's contents in memory, and only updating the small portion of the form that needs updating each second (normally, just the second hand and the seconds display in the digital portion). You could manage this all yourself, but a simpler solution is to allow the form to "double-buffer." You can have the form manage its own updating, so that displaying a background gradient doesn't cause your clock to flash every second.

In addition, by default, there's no code handling the form's Resize event. Without taking extra steps, if you simply call the Clock class from your form, resizing the form won't cause the clock to repaint until the next second ticks by and the Clock class invalidates its parent.

You can solve both these problems using the SetStyle method of the Form class. The frmClock_Load procedure includes code like this (see the documentation for the SetStyle method for more information):

Me.SetStyle(ControlStyles.ResizeRedraw, True)
Me.SetStyle(ControlStyles.AllPaintingInWmPaint Or _
    ControlStyles.UserPaint Or ControlStyles.DoubleBuffer, True) 

Saving User Settings

At the request of testers who decided to actually use the GDIClock application daily, the sample application includes code to save and restore your settings to a configuration file. The sample includes the AppSettings class that handles the details of reading and writing from the configuration file (in the Documents and Settings/<UserName>/Application Data/GDIClock folder), and the RegSettings class which handles saving the "run at startup" information in the registry. If you're interested in how to read and write information in configuration files, you might want to check out those classes as well.

Conclusion

There you have it. A simple but demonstrative example of how a clock demo can take advantage of the GDI+ features made available by the .NET Framework, as well as sharpen up your Visual Basic skills.

Show:
© 2014 Microsoft. All rights reserved.