GDI+

A Primer on Building a Color Picker User Control with GDI+ in Visual Basic .NET or C#

Ken Getz

Code download available at:GDIColorPicker.exe(395 KB)

This article assumes you're familiar with C# and Visual Basic .NET

Level of Difficulty123

SUMMARY

Although most developers and APIs use the RGB scheme when working with colors, it's not the only available way to represent or select colors. For instance, the standard Windows color-selection dialog box allows you to work with the HSL color scheme in an indirect way. In this article, the author describes several color selection schemes, and uses GDI+ (via the System.Drawing namespace) to create a component that makes it possible for your own applications to provide a simpler, friendlier color chooser. Along the way, you'll get tips to help you use GDI+ in your own apps.

Contents

Color Spaces
Investigating the Sample App
Converting Colors
Drawing the Color Wheel
Finishing the Drawing
Painting on Demand
Repainting is Expensive
Cleaning Up
Investigating the Sample
Conclusion

Because Windows® provides a standard common dialog box for selecting colors (see Figure 1) it's easy to assume that this is the only method for selecting colors. That is not true, however, and in this article I'll provide a simple-to-use replacement for the standard color-selection dialog box. The sample's layout and appearance are easy to modify. You can lay out the indicators just about any way you like, and the sample application includes two different layouts for the dialog box. This article focuses on two specific areas: investigating different color-selection techniques and explaining the members of the System.Drawing namespace that make the sample application work.

Color Spaces

The standard Windows color-selection dialog box represents colors in just two of the many ways you might represent colors in code. This representation of color choices is often called a "color space" because color representations normally allow you to refer to colors by a set of coordinates in three-dimensional space. Figure 1 shows the standard Windows dialog box with the Define Custom Colors button selected, so the right-hand pane appears, allowing you to define your own colors. Disregarding the left-hand side of the dialog box, which simply allows you to select from a palette of preselected colors, the right-hand pane allows you to drag the color selector with your mouse, or enter colors using two different color spaces, Red/Green/Blue (RGB) or Hue/Saturation/Luminosity (HSL). In this dialog box, the individual RGB values contain 8-bit values and range from 0 all the way up to 255. The HSL values range from 0 to 240.

Figure 1 Standard Color Selection in Windows

Figure 1** Standard Color Selection in Windows **

The RGB color system combines three color coordinates, along with a value that indicates the transparency of the color that's often referred to as the "alpha" component of the color, and creates a 32-bit unsigned integer value that represents the color. Note that the dialog box in Figure 1 doesn't provide any way to select an alpha component other than 255, allowing only fully opaque colors. The AlphaRGB (ARGB) value represents the color in a way that's easy for a computer to decipher. For example, with the color selected in Figure 1, the combined ARGB value is 13578293 decimal or FF30CFCB hex. Breaking this 4-byte value up into individual bytes, you'll find, as you might expect given the color in Figure 1, the values 255, 48, 207, and 203 for the alpha, red, green, and blue components of the color. Disregarding the alpha component, which will always be 255 (FF) when using this particular dialog box, it's easy to programmatically convert from the integer representation of the color to the color's individual bytes, hence the propensity for Windows, and therefore application developers, to use the RGB scheme to describe colors.

Although using the RGB color space does make it easier for the computer to convert back and forth between its internal representation of the color and the representation humans need in order to select a color, RGB doesn't provide any useful ordering of colors for selection. Some development team arbitrarily selected the layout of the color grid on the right-hand side of Figure 1—there's no reason it has to look this way. If you take a few minutes to investigate the meaning of contiguous colors within the color-selection square, watching the values for the R, G, and B components of the color as you move the mouse about the screen, you'll be able to discern a pattern. The pattern isn't obvious, however; nor could you ever guess an RGB value given the position of your selection. In a more logical color-selection scheme, the position of the selector would have some relationship to the selected color.

Back in the days before it was easy to use the Windows common dialog boxes, I remember attempting to create my own color-selection dialog box. Not knowing any better, I began by trying to create a two-dimensional grid providing the capability of selecting any RGB color. Quickly, I realized that I was attempting to solve a three-dimensional problem with two-dimensional tools. My simple solution at the time was to provide a list of available colors, the easy way out. As a matter of fact, the RGB color space consists of normal Cartesian coordinates, with the red, green, and blue portions of the colors acting as the x, y, and z values. Specifying a color using the RGB color space selects a point in three dimensions, where each component indicates the distance from black (0, 0, 0) on each of three axes. The color white (255, 255, 255) represents the corner on the color "cube" diagonally opposite the origin.

So RGB is convenient, but not for humans. Humans are accustomed to thinking about color in terms of hue (what color is it?), intensity (how strong is the color?), and brightness (is it pale or vivid?). In order to meet these needs, the standard color-selection dialog box also provides an alternative representation of the color value, the HSL format. This representation makes more sense to you and me, although it requires more work for the computer as it performs somewhat complex mathematical computations to convert HSL-formatted values to the more common RGB values. If you drag the selector left and right and then up and down within the dialog box and watch the HSL values, you'll see the pattern right away. That is, as you move from left to right across the square, the Hue value increases from 0 to 240. As you move from top to bottom in the square, the Saturation value goes from 240 to 0. As you drag the separate vertical slider from top to bottom, the Luminosity value goes from 240 (white) to 0 (black). Given these physical arrangements, you could, with relative ease, become proficient at converting from the position of the two pointers in the dialog box to a reasonable approximation of the corresponding HSL color values. Not so with RGB.

The Hue/Saturation/Value (HSV) color space is a modification of HSL in which the Value axis measures the brightness of the color, and you cannot progress beyond full brightness. In comparison, HSL allows you to move past full brightness, through pastel colors, all the way to white. HSV uses a Saturation value of 0 to provide all colors that have the same red, green, and blue components instead. (That is, shades of gray.) HSV is the color scheme demonstrated in the sample application, and it's simple to explain.

The first component of an HSV value, the Hue, corresponds to an angle on a circle. This circle contains the same colors as shown on the right-hand side of Figure 1, but arranged in circular fashion (see Figure 2). Given this layout, the color red corresponds to an angle of 0 degrees, yellow to 60 degrees, and so on. The particular variation of HSV used for the sample project scales the degrees on the circle so that values for the Hue portion of the color range from 0 to 255—the scaling is done internally, in the code.

Figure 2 Color Circle

Figure 2** Color Circle **

The Saturation value corresponds to the distance from the center of the circle. Colors on the edge of the circle are fully saturated and the center of the circle contains no saturation—it's white. Figure 3 shows the range of saturation values for the color green, as if extracted from the circle shown in Figure 2. Again, my selection of 0 to 255 as the range is arbitrary and only serves to make the selection of colors using HSV feel more like the familiar RGB. In reality, the example could just as easily have used floating point values between 0 and 1 to represent saturation.

Figure 3 Saturation

Figure 3** Saturation **

The Value portion contains the brightness of the selected color, and it ranges from 255 (fully bright) to 0 (black). Again, the exact values aren't important—the scaling here is just for convenience. Figure 4 shows the color green as its Value coordinate changes from 255 to 0.

Figure 4 Value

Figure 4** Value **

What you really have, when working with the HSV color space, is a cone of color. The circle in Figure 2 acts as the top of the cone, and a line from each point in the circle down to the vertex of the cone, representing the color black, provides the brightness range for each color. Figure 5 shows the cone using the brightness axis for three colors: green, cyan, and purple. Any color can be represented as a Hue coordinate, measuring an angle around the circle, a Saturation coordinate as the distance from the center of the circle, and a Value coordinate as the distance from the vertex at the bottom of the cone.

Figure 5 Color Cone

Figure 5** Color Cone **

Coincidentally, I ran across an application using a dialog box that allows users to select colors using the HSV color scheme (much like the one shown in Figure 6) right at the time I was attempting to teach myself how to use GDI+ in the Microsoft® .NET Framework. Fascinated by this fresh approach to selecting colors, I took on a "nights and weekends" project to create the same effect using Visual Studio® .NET and Visual Basic® .NET. That effort became the sample project discussed in this article. Along the way, I grew quite comfortable with the basics of GDI+ and many of the objects and members of the System.Drawing namespace. The remainder of this article discusses the particular GDI+ techniques, objects, and members used by this sample application. I can't describe each object and member in any detail here. The code in this article is a starting point, but the .NET Framework documentation is a major resource.

Figure 6 Sample Form

Figure 6** Sample Form **

Run the sample application and load the sample form shown in Figure 6. You should be able to predict the behavior of the Hue, Saturation, and Brightness controls as you interact with the form. Click on the circle and then move the mouse in a circular fashion around the circle, keeping the distance from the center as constant as possible. You'll see just the Hue value change. Moving the mouse radially, from the center of the circle to the edge, the Saturation value increases. In either case, the Brightness value remains constant. Drag the pointer on the vertical bar to the right of the circle, and the Brightness value changes corresponding to the height of the pointer.

Investigating the Sample App

The sample includes two versions of the same project: ColorChooserVB.sln and ColorChooserCSharp.sln. You can download it from the link at the top of this article. The code is essentially the same in both projects, but I'll demonstrate with code samples from the Visual Basic .NET version. The classes in the sample projects are described in Figure 7. Each class has been saved in its own file, so it's easy to find each individual class. The ColorWheel class is the centerpiece of the application—it provides the support for drawing the various elements of the color-selection forms. Besides the fields, properties, and methods in the class, the ColorWheel class also raises the ColorChanged event whenever you move the mouse to select a new color on the color wheel or on the vertical brightness bar.

Figure 7 Sample Project Classes

Class Description
Main Provides the sample form, allowing you to select from three different color-choosing dialog boxes (see Figures 1, 6, and 14)
ColorWheel Contains the code that draws the color wheel, the brightness bar, and the selected color rectangle; handles repainting the elements as needed
ColorHandler Contains shared methods that convert colors between RGB and HSV color spaces
ColorChangedEventArgs Provides the ColorChanged event with a structure containing information about the selected color; inherits from EventArgs
ColorChooser1 Sample form demonstrating the use of the ColorWheel class, using NumericUpdown controls to display current colors (see Figure 6)
ColorChooser2 Sample form demonstrating the use of the ColorWheel class, using HorizontalScrollbar controls to display current colors (see Figure 14)

It's important to understand the design goals of the sample application. I knew that however I designed the color-selection form, the user might have a different opinion about how it should look or how it should be used. Because of this, I've taken special care to separate the specific user interface of the host form from the display of the color wheel, brightness bar, and selected color rectangle. As a matter of fact, the ColorWheel class maintains no specific information about its host. Each form or control creates a new instance of the ColorWheel class, passing Rectangle objects corresponding to the three areas that the ColorWheel instance needs to manage. The ColorWheel instance raises the ColorChanged event back to the host form or control when it determines that a new color has been selected. It's up to your host form to lay out the three areas, pass Rectangle objects corresponding to the areas to the ColorWheel class's constructor, and to call the ColorWheel class's Draw method whenever it's necessary. When the user drags the color or brightness selection pointers, or otherwise selects a new color, your form must call the ColorWheel.Draw method. See the code for the sample forms—they show two different examples of this interaction.

Converting Colors

Throughout the ColorWheel class and the sample project, the code needs to convert color values between RGB and HSV format. Generally, the code uses HSV format for most internal uses—it closely maps to the way the colors are represented on the screen—but uses RGB values when interacting with the user or when displaying the selected color on the screen. Although I made a valiant attempt at working out the conversion formulas during a cross-country flight, once I reconnected and did a quick search, I found several sites listing pretty much the same algorithm in several different languages, but none in C# or Visual Basic .NET. I converted the code to C# and Visual Basic .NET, and the ColorHandler class in the sample project provides shared/static methods that handle the conversions between RGB and HSV. The class also contains HSV and RGB structures that are used throughout the application. They are simple and provide an easy way to cart around RGB and HSV values.

Specifically, the ColorHandler class provides HSVToRGB, RGBToHSV, and HSVToColor methods that perform the color conversions. The Color structure within the .NET Framework provides FromArgb and ToArgb methods, so there's no need to create those methods here. If you're interested in the details of performing the conversions to and from the HSV color scheme, check out the code in the ColorHandler class. I haven't focused on this code here, as you're unlikely to need to modify the code for use in other projects—you can simply import the code or package it in any way you like for reuse.

It is important to note, however, that both the HSVToRGB and RGBToHSV methods expect that the individual color components—the R, G, B and H, S, V values—contain integers between 0 and 255. Although there's no reason that these exact values must be used, I chose to maintain consistency between the two color spaces, using the same range for both. If you want to handle the color values in a different way, you'll need to modify the code in the ColorHandler class.

Drawing the Color Wheel

To be honest, I started on this project mostly because I had seen an application that used an HSV color selection and was convinced that it was possible to create the circular gradient the form uses for selecting colors without writing much code using GDI+. As I suspected, it's really simple to create the gradient. Working with a PathGradientBrush object, you supply an array of points that define the edge of the gradient, indicate the center color and center point, supply a corresponding array of colors in a one-to-one mapping, and you're all set. GDI+ does all the work, filling in the designated region with the gradient you've requested. (Both the PathGradientBrush class and the LinearGradientBrush class discussed later inherit from the Brush class, and are among several means of filling a region.)

The CreateGradient procedure handles all these tasks, but in a way you might not expect. As I originally wrote it, the procedure generated the gradient each time it was called, and this procedure might be called each time you move the mouse. Of course, this inefficiency made it impossible to track the mouse effectively even on the most beefy hardware, and the current solution was born out of necessity—that is, rather than continually redrawing the gradient, the CreateGradient procedure (see Figure 8) draws the gradient once, creates an image of the gradient, and then displays the image whenever the host needs to be repainted.

Figure 8 CreateGradient and Supporting Procedures

Private Sub CreateGradient() Dim newGraphics As Graphics Dim pgb As PathGradientBrush Try ' Create a new PathGradientBrush, supplying an array of points created ' by calling the GetPoints method. pgb = New PathGradientBrush( _ GetPoints(radius, New Point(radius, radius))) ' Set the various properties. Note the SurroundColors property, which ' contains an array of points, in a one-to-one relationship with the ' points that created the gradient. pgb.CenterColor = Color.White pgb.CenterPoint = New PointF(radius, radius) pgb.SurroundColors = GetColors() ' Create a new bitmap containing the color wheel gradient, so the ' code only needs to do all this work once. Later code uses the bitmap ' rather than recreating the gradient. colorImage = New Bitmap( _ colorRectangle.Width, colorRectangle.Height, _ PixelFormat.Format32bppArgb) newGraphics = Graphics.FromImage(colorImage) newGraphics.FillEllipse(pgb, 0, 0, _ colorRectangle.Width, colorRectangle.Height) Finally If Not pgb Is Nothing Then pgb.Dispose() End If If Not newGraphics Is Nothing Then newGraphics.Dispose() End If End Try End Sub Private Function GetColors() As Color() ' Create an array of COLOR_COUNT colors, looping through all the hues ' between 0 and 255, broken into COLOR_COUNT intervals. HSV is ' particularly well suited for this because the only value that changes ' as you create colors is the Hue. Dim Colors(COLOR_COUNT - 1) As Color Dim i As Integer For i = 0 To COLOR_COUNT - 1 Colors(i) = ColorHandler.HSVtoColor( _ i * 255 \ COLOR_COUNT, 255, 255) Next Return Colors End Function Private Function GetPoints( _ ByVal radius As Double, ByVal centerPoint As Point) _ As Point() ' Generate the array of points that describe the locations of the ' COLOR_COUNT colors to be displayed on the color wheel. Dim Points(COLOR_COUNT - 1) As Point Dim i As Integer For i = 0 To COLOR_COUNT - 1 Points(i) = GetPoint( _ i * 360 / COLOR_COUNT, radius, centerPoint) Next Return Points End Function Private Function GetPoint( _ ByVal degrees As Double, _ ByVal radius As Double, _ ByVal centerPoint As Point) As Point ' Given the center of a circle and its radius, along with the angle ' corresponding to the point, find the coordinates. In other words, ' convert from polar to rectangular coordinates. Dim radians As Double = degrees / DEGREES_PER_RADIAN Return New Point( _ CInt(centerPoint.X + _ Math.Floor(radius * Math.Cos(radians))), _ CInt(centerPoint.Y - _ Math.Floor(radius * Math.Sin(radians)))) End Function

The CreateGradient procedure needs to retrieve an array of points along the edge of the gradient, and the GetPoints procedure handles this calculation. GetPoints iterates through 1536 points on the edge of the circle, converting from polar to Cartesian coordinates. Although this example uses a specific number of points—1536, or 256 colors in each of six regions of the circle—you could certainly get by with fewer.

For those of you who have purged all your high school trigonometry from long-term memory, polar coordinates define locations in terms of the center of a circle, the radius of the circle, and an angle between 0 and 360 degrees on the circle. Cartesian coordinates are plain old x and y—horizontal and vertical—values. Converting between the two involves some ugly but simple math using the Sin and Cos methods of the Math class. Figure 8 contains the code for the GetPoint and GetPoints methods so if you're in a high school trig kind of mood, dig in!

The CreateGradient procedure starts by creating a new PathGradientBrush object, specifying the array of points returned by the GetPoints method:

pgb = New PathGradientBrush( _ GetPoints(radius, New Point(radius, radius)))

The code then sets the necessary properties of the PathGradientBrush object, filling in the SurroundColors property with the array of colors returned by the GetColors method:

pgb.CenterColor = Color.White pgb.CenterPoint = New PointF(radius, radius) pgb.SurroundColors = GetColors()

Finally, the code creates a new Bitmap object the same size as the rectangle bounding the color wheel, then creates a new Graphics object and uses its FillEllipse method to fill the bitmap with the gradient:

colorImage = New Bitmap( _ colorRectangle.Width, colorRectangle.Height, _ PixelFormat.Format32bppArgb) newGraphics = Graphics.FromImage(colorImage) newGraphics.FillEllipse(pgb, 0, 0, _ colorRectangle.Width, colorRectangle.Height)

Drawing the linear gradient displaying the range of brightness values for the selected color is much simpler. For this vertical bar, the code simply creates a LinearGradientBrush object, supplying the top and bottom colors, and the direction in which to fill—top to bottom in this case. The code can then take the LinearGradientBrush object and use it to fill the rectangle. The ColorWheel.DrawLinearGradient procedure does the work, as shown in Figure 9.

Figure 9 Drawing the Linear Gradient

Private Sub DrawLinearGradient(ByVal TopColor As Color) ' Given the top color, draw a linear gradient ranging from black to the ' top color. Use the brightness rectangle as the area to fill. Dim lgb As LinearGradientBrush Try lgb = New LinearGradientBrush( _ brightnessRectangle, TopColor, _ Color.Black, LinearGradientMode.Vertical) g.FillRectangle(lgb, brightnessRectangle) Finally If Not lgb Is Nothing Then lgb.Dispose() End If End Try End Sub

The CreateGradient and DrawLinearGradient procedures should call the Dispose method of the GDI+ managed wrapper objects instead of waiting for garbage collection, to avoid overusing window handles on older OSs. The Visual Basic .NET code handles this in the Finally block, ensuring that the objects get released correctly. C# provides a simpler mechanism, the using statement, which guarantees that the object(s) listed in the statement will have their Dispose method called automatically at the end of the block. In Visual Basic .NET, which doesn't provide a construct like the using statement, procedures require a bit more code. The following code sample shows the DrawLinearGradient procedure from the C# example, taking advantage of the using statement:

private void DrawLinearGradient(Color TopColor) { using (LinearGradientBrush lgb = new LinearGradientBrush(brightnessRectangle, TopColor, Color.Black, LinearGradientMode.Vertical)) { g.FillRectangle(lgb, brightnessRectangle); } }

Compare this to the Visual Basic .NET version shown in Figure 9.

Finishing the Drawing

The sample code uses GDI+ to draw the square and triangular pointers and to draw the selected color rectangle. The DrawColorPointer procedure calls the DrawRectangle method of the Graphics class, passing in a Pen object and coordinates for the rectangle. I used one of the predefined Pen objects, but you can also create your own Pen, passing a color and, optionally, a width:

Private Sub DrawColorPointer(ByVal pt As Point) ' Given a point, draw the color selector. ' The constant SIZE represents half ' the width — the square will be twice ' this value in width and height. Const SIZE As Integer = 3 g.DrawRectangle(Pens.Black, _ pt.X - SIZE, pt.Y - SIZE, _ SIZE * 2, SIZE * 2) End Sub

The DrawBrightnessPointer procedure draws the triangular pointer, using the FillPolygon method of the Graphics class. This method allows you to specify a Brush (again, using one of the built-in brushes) and an array of points:

Private Sub DrawBrightnessPointer(ByVal pt As Point) ' Draw a triangle for the brightness indicator that "points" ' at the provided point. Const HEIGHT As Integer = 10 Const WIDTH As Integer = 7 Dim Points(2) As Point Points(0) = pt Points(1) = New Point(pt.X + WIDTH, pt.Y + HEIGHT \ 2) Points(2) = New Point(pt.X + WIDTH, pt.Y - HEIGHT \ 2) g.FillPolygon(Brushes.Black, Points) End Sub

Every time the display needs updating—which is generally every time you move the mouse or select a new color—the UpdateDisplay procedure creates a new SolidBrush object based on the selected color, redraws the color wheel image, draws the selected color rectangle, draws the linear gradient containing brightness values, and updates the pointers. The code in Figure 10 handles these tasks, calling procedures you've already seen, along with System.Drawing methods.

Figure 10 Update the Display

Private Sub UpdateDisplay() ' Update the gradients, and place the pointers correctly based on ' colors and brightness. Dim selectedBrush As Brush Try ' Draw the "selected color" rectangle. selectedBrush = New SolidBrush(selectedColor) ' Draw the saved color wheel image. g.DrawImage(colorImage, colorRectangle) g.FillRectangle( _ selectedBrush, selectedColorRectangle) ' Draw the "brightness" rectangle. DrawLinearGradient(fullColor) ' Draw the two pointers. DrawColorPointer(colorPoint) DrawBrightnessPointer(brightnessPoint) Finally If Not selectedBrush Is Nothing Then selectedBrush.Dispose() End If End Try End Sub

Painting on Demand

In the sample applications, it's up to the forms hosting the ColorWheel class to call the ColorWheel.Draw method every time the display needs to be updated. This might occur when the user moves the mouse or selects a new color using the input method provided by the host form. The sample forms use NumericUpdown or HorizontalScrollbar controls. No matter when or how the updating needs to occur, the ColorWheel.Draw method expects to receive a Graphics object from its host, so it has a context on which to draw its graphic elements. Although you can call the CreateGraphics method at any time, doing so presents risks. For example, the Graphics object is only valid for the duration of the Windows message and must be explicitly disposed. I've found it simplest to perform all painting from within the host's Paint event handler, mostly because the Paint event receives a PaintEventArgs object as one of its parameters, and this object provides a Graphics property containing the container's graphics context. Writing it this way also allows double buffering, as you'll see in the next section.

Handling all the updates in the Paint event centralizes all the drawing code, but it means that your apps must manage their own state and take actions based on the current state from the Paint event handler. In the sample forms, you'll find an enumeration that tracks the type of change, so the Paint event handler can call the correct overloaded version of the ColorWheel.Draw method:

Private Enum ChangeStyle MouseMove RGB HSV None End Enum Private changeType As ChangeStyle = ChangeStyle.None Private selectedPoint As Point

When you move the mouse or otherwise indicate that you want to modify the selected color, the host's code must track the current state and invalidate the host. This causes a request for the region to be painted, raising the Paint event. For example, moving the mouse on the sample forms causes the following code to run:

Private Sub HandleMouse( _ ByVal sender As Object, ByVal e As MouseEventArgs) _ Handles MyBase.MouseMove, MyBase.MouseDown ' If you have the left mouse button down, then update the ' selectedPoint value and force a repaint of the color wheel. If e.Button = MouseButtons.Left Then changeType = ChangeStyle.MouseMove selectedPoint = New Point(e.X, e.Y) Me.Invalidate() End If End Sub

The call to the Invalidate method causes the form's Paint event handler to kick in, running the code shown in Figure 11. Selecting a value from one of the NumericUpdown controls on the first sample form that corresponds to an RGB value (look back to Figure 6) runs the code shown in Figure 12. This procedure first checks to make sure it's not being called recursively. It's impossible to determine if a NumericUpdown control is being updated by a user or in code, so the sample uses a class-level variable, isInUpdate, to avoid recursive changes to the controls. The code sets the change type, calculates the RGB and HSV values, and then invalidates the form, raising the Paint event and running the Paint event handler code.

Figure 12 Color Changed

Private Sub HandleRGBChange( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles nudRed.ValueChanged, nudBlue.ValueChanged, _ nudGreen.ValueChanged ' If the R, G, or B values change, use this code to update the HSV ' values and invalidate the color wheel (so it updates the pointers). ' Check the isInUpdate flag to avoid recursive events when you update ' the NumericUpdownControls. If Not isInUpdate Then changeType = ChangeStyle.RGB RGB = New ColorHandler.RGB(CInt(nudRed.Value), _ CInt(nudGreen.Value), CInt(nudBlue.Value)) SetHSV(ColorHandler.RGBtoHSV(RGB)) Me.Invalidate() End If End Sub

Figure 11 Paint Event Handler

Private Sub ColorChooser1_Paint( _ ByVal sender As Object, _ ByVal e As PaintEventArgs) Handles MyBase.Paint ' Depending on the circumstances, force a repaint ' of the color wheel passing different information. Select Case changeType Case ChangeStyle.HSV myColorWheel.Draw(e.Graphics, HSV) Case ChangeStyle.MouseMove, ChangeStyle.None myColorWheel.Draw(e.Graphics, selectedPoint) Case ChangeStyle.RGB myColorWheel.Draw(e.Graphics, RGB) End Select End Sub

Repainting is Expensive

Even having optimized the drawing of the circular gradient so that the major calculations happen only once, the Draw method of the ColorWheel class needs to display the image of the gradient each time you move the mouse so it can redraw the rectangle pointer. This, too, is quite expensive and slow. There is a simple solution, however: double buffering. This technique creates an image of the form in memory. Each time you invalidate the form's image, the Windows Forms engine can figure out exactly which pixels need to be redisplayed by comparing the current image to the saved image and updating only the changed pixels. By creating an in-memory image of the display surface, the Windows Forms engine can redisplay only the necessary portion of the surface as your code calls the ColorWheel.Draw method repeatedly.

The System.Windows.Forms.Control class makes this easy—you can set bits in the ControlStyles field that affect the way the form draws itself, calling the SetStyle method. Although the SetStyle method provides a number of values you can combine to control painting, to reduce flicker by using double buffering you must set the UserPaint, AllPaintingInWmPaint, and DoubleBuffer style bits to True, like this:

Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True) Me.SetStyle(ControlStyles.UserPaint, True) Me.SetStyle(ControlStyles.DoubleBuffer, True)

See the .NET Framework documentation on the Control.SetStyle method for more information on these and other style bits. You might also want to try commenting out these lines in the sample forms (the code in the form's Load event handlers) to verify the difference that double buffering can make.

Cleaning Up

The ColorWheel class implements the IDisposable interface and so supplies the Dispose method required by the interface. The point is that the class takes advantage of some graphics resources that should always be disposed, when the class is no longer in use (see Figure 13).

Figure 13 ColorWheel Class

' Declared in the ColorWheel class: Private g As Graphics Private colorRegion As Region Private brightnessRegion As Region Private colorImage As Bitmap Private Sub Dispose() Implements IDisposable.Dispose ' Dispose of graphic resources If Not colorImage Is Nothing Then colorImage.Dispose() End If If Not colorRegion Is Nothing Then colorRegion.Dispose() End If If Not brightnessRegion Is Nothing Then brightnessRegion.Dispose() End If If Not g Is Nothing Then g.Dispose() End If End Sub

When working with GDI+, it's important to remember that you should call the Dispose method of any object that provides one. That way, the wrappers around unmanaged objects provided by the operating system won't hog resources.

Investigating the Sample

The sample project contains the two forms shown in Figure 6 and Figure 14, in addition to a main form that allows you to test the three different color dialog boxes shown in this article. The two forms, ColorChooser1 (see Figure 6) and ColorChooser2 (see Figure 14), each take advantage of the fact that the class that actually generates and manages the color selection, ColorWheel, requires its clients to pass in the locations of the color wheel, brightness rectangle, and selected color rectangle as parameters to its constructor. In each case, the client passes a Rectangle object containing the coordinates of the corresponding area on the client form. That is, the ColorWheel class itself has no direct interaction with its host. Figure 15 shows the ColorChooser1 form in design view, with three Panel controls acting as placeholders for the graphic elements to be displayed by the ColorWheel class. The point here is that the ColorWheel class needs to know only the locations in which to display its color wheel, brightness selector, and selected color rectangle. Code in your form passes these locations to the constructor in the ColorWheel class.

Figure 14 Another Color Picker Form

Figure 14** Another Color Picker Form **

The sample forms declare a class-level variable, myColorWheel. In Visual Basic .NET, the WithEvents keyword makes it easy to hook up event handling. In C#, the sample hooks up the event handling declaratively, instead:

Private WithEvents myColorWheel As ColorWheel

As the form loads, the code within the form's Load event handler hides the panels and then retrieves their coordinates into Rectangle objects (see Figure 16).

Figure 16 Get Coordinates

pnlSelectedColor.Visible = False pnlBrightness.Visible = False pnlColor.Visible = False ' Calculate the coordinates of the three required regions on the form. Dim SelectedColorRectangle As Rectangle = _ New Rectangle(pnlSelectedColor.Location, _ pnlSelectedColor.Size) Dim BrightnessRectangle As Rectangle = _ New Rectangle(pnlBrightness.Location, _ pnlBrightness.Size) Dim ColorRectangle As Rectangle = _ New Rectangle(pnlColor.Location, pnlColor.Size)

Figure 15 Design View

Figure 15** Design View **

I decided to use Panel controls only because they provide a convenient way to lay out the forms. They're not required; you can build your forms by simply specifying the locations where you want the three user interface features of the ColorWheel class. I found it easiest to lay these out at design time, using the Panel controls, and then pass the coordinates of those panels to the constructor in the ColorWheel class.

Finally, the form creates a new ColorWheel object, passing in the three Rectangle objects, and hooks up the ColorChanged handler, as you'll see in the following code:

myColorWheel = New ColorWheel( _ ColorRectangle, BrightnessRectangle, _ SelectedColorRectangle)

The ColorWheel class raises its ColorChanged event every time you select a new color, and the hosting form needs to react to that event, updating the display as it sees fit. The first sample form, shown in Figure 6, updates NumericUpdown controls as you move the mouse on the form. The second sample form, shown in Figure 14, updates HorizontalScrollbar controls. You aren't required, of course, to update any controls—you could simply allow the user to move the mouse, and the ColorWheel class would keep the area designated by the third rectangle that was passed by parameter to its constructor updated with the selected color.

Conclusion

If you want to take advantage of the ColorWheel class in your own applications, you could call it from any form, create a custom control that mimics the ColorDialog control, or use the class in any other way you find useful. In any case, you can simply examine the sample forms and the ColorWheel class, and go from there.

As you can see, you can easily break out of the RGB rut and offer your users or other developers an alternate means of selecting colors. In addition, you've seen in this example that you can take advantage of the powerful features provided by the System.Drawing namespace without having to write much code at all.

For related articles see:
Computer Graphics: Principles and Practice by James Foley et. al. (Addison-Wesley, 1995)
Programming Windows with C# by Charles Petzold (Microsoft Press, 2001)

For background information see:
About GDI+
Using GDI+

Ken Getz is a senior consultant with MCW Technologies and a Microsoft Regional Director for Southern California. Ken is coauthor of ASP.NET Developer's Jumpstart (Addison-Wesley, 2002), Access Developer's Handbook (Sybex, 2001), and VBA Developer's Handbook, 2nd Edition (Sybex, 2001). Reach him at keng@mcwtech.com.