Advanced Basics

Adding New Features with User Controls

Ken Spencer

Code download available at:AdvancedBasics0306.exe(156 KB)

Q How can I draw a line on a Windows® Form that is selectable by the user and can respond to mouse events?

Q How can I draw a line on a Windows® Form that is selectable by the user and can respond to mouse events?

A In past versions of Visual Basic®, there were rudimentary graphics controls. In Visual Basic .NET you have the GDI+ library, which enables you to draw lines, circles, and most anything else. But how can you use the functionality of GDI+ to create lines and other graphics that respond to user mouse clicks and events?

A In past versions of Visual Basic®, there were rudimentary graphics controls. In Visual Basic .NET you have the GDI+ library, which enables you to draw lines, circles, and most anything else. But how can you use the functionality of GDI+ to create lines and other graphics that respond to user mouse clicks and events?

To answer, I created a UserControl that implements a line drawn with GDI+. At first this seems like a trivial exercise, but it's fun because of the various properties you create to control a line and the testing you go through to make this work.

Figure 1 shows my test application using the new line control. You can see the line across the top of the form, and the status bar shows the results of clicking the line. You can fiddle with the line's size and other properties using this form by changing various options and clicking the Change Line Properties button.

Figure 1 Control Test App

Figure 1** Control Test App **

Let's walk through the code used to implement this control. First, I created several enumerations to use for various line properties. The enumerations make it easy for users to enter a property value. They also strongly type the properties, making it very difficult to introduce errors by entering an incorrect value. The LineStyles enum controls the style of the line and the LineWeightStyles enum controls the weight (density) of the line:

Enum LineStyles Solid = 0 Hatched = 1 TiledImage = 2 End Enum Enum LineWeightStyles Small = 0 Medium = 1 Large = 2 Graphic = 3 End Enum

Next, I added a set of private variables to handle the internals of size, line weight, color, and other such properties. These variables are shown in Figure 2.

Figure 2 Line Variables

#Region "Private Variables" Private privateStart As Point = New Point(0, 0) Private privateEnd As Point = New Point(Me.width, 0) Private privateWeight As LineWeightStyles = _ LineWeightStyles.Small Private privateWeightPixels As Integer = 2 Private privateStyle As LineStyles = LineStyles.Solid Private privateColor As Color = Color.Black Private privateCurrentMouseX As Integer Private privateCurrentMouseY As Integer #End Region

Then, I created several public properties that are used to control the line. These properties are shown in Figure 3. The LineWidth property is straightforward; it simply sets the width of the line in pixels. Notice that it also sets the width of the UserControl to make the control's size match the size of the line. LineColor determines the color of the line. The LineStyle property can be used to specify how the line is displayed. In this version, this property is not fully implemented since only the Solid and Hatched styles are supported. The LineWeight property controls the thickness of the line. This property is interesting because the weight controls not only the thickness of the line but also the height of the control. You can see how the value set using the enum is converted to a pixel value in the control. This allows you to present a meaningful value to the user such as Large and then you can internally set Large to, say, 15 pixels or so.

Figure 3 Public Properties

#Region "Public Properties" Property LineWidth() As Integer Get Return privateEnd.X End Get Set(ByVal Value As Integer) privateEnd = New Point(Value, 0) If Me.Width <> privateEnd.X Then Me.Width = privateEnd.X End If If Me.Height <> privateEnd.Y + privateWeightPixels Then Me.Height = _ privateEnd.Y + privateWeightPixels End If RefreshMe() LogIt("LineWidth Set " & "end") End Set End Property Property LineColor() As Color Get Return privateColor End Get Set(ByVal Value As Color) LogIt("LineColor Set " & "start") privateColor = Value RefreshMe() LogIt("LineColor Set " & "end") End Set End Property Property LineStyle() As LineStyles Get Return privateStyle End Get Set(ByVal Value As LineStyles) LogIt("LineStyle Set " & "start") privateStyle = Value RefreshMe() LogIt("LineStyle Set " & "end") End Set End Property Property LineWeight() As LineWeightStyles Get Return privateWeight End Get Set(ByVal Value As LineWeightStyles) privateWeight = Value Select Case privateWeight Case LineWeightStyles.Small privateWeightPixels = 2 Case LineWeightStyles.Medium privateWeightPixels = 6 Case LineWeightStyles.Large privateWeightPixels = 10 Case LineWeightStyles.XLarge privateWeightPixels = 15 Case LineWeightStyles.XXLarge privateWeightPixels = 20 Case LineWeightStyles.Graphic End Select RefreshMe() End Set End Property #End Region

The next bit of code in the class is the LineClick event. This event is fired when the user clicks the control. The definition for the event is shown in the following code:

#Region "Public Events" Public Event LineClick(ByVal ClickedLocation As Point) #End Region

Now let's walk through creating the line in the Paint event (see Figure 4). The Paint event fires when the control is dirty and must be cleaned up—when another window covers the control or parent of the control. This event is a good place to add code to draw something on the control. The first line after the Try statement calls the ResetSize procedure, which sets the UserControl's size to the width and the height of the line.

Figure 4 Paint Event

Private Sub MyLine_Paint(ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) _ Handles MyBase.Paint Dim LinePen As Pen Dim LineBrush As Brush Try ResetSize() Select Case privateStyle Case LineStyles.Solid Dim SolidBrush As New SolidBrush(privateColor) LinePen = New Pen(SolidBrush, privateWeightPixels) e.Graphics.DrawLine(LinePen, privateStart.X, _ privateStart.Y, privateEnd.X, privateEnd.Y) Case LineStyles.Hatched Dim HatchedBrush As New HatchBrush( _ HatchStyle.DiagonalCross, privateColor) LinePen = New Pen(HatchedBrush, privateWeightPixels) e.Graphics.DrawLine(LinePen, privateStart.X, _ privateStart.Y, privateEnd.X, privateEnd.Y) Case LineStyles.TiledImage End Select Catch exc As Exception Throw New _ Exception("MyLine UserControl " & " - " & exc.Message) End Try End Sub

Now let's look at the Select Case statement inside the Try block. The first Case statement draws the solid line:

Select Case privateStyle Case LineStyles.Solid

This line creates a new solid brush in the color selected by the user:

Dim SolidBrush As New SolidBrush(privateColor)

The next line creates a new instance of a pen with the solid brush just created and the weight selected by the user:

LinePen = New Pen(SolidBrush, privateWeightPixels)

Next, draw the line by calling DrawLine:

e.Graphics.DrawLine(LinePen, privateStart.X, privateStart.Y, _ privateEnd.X, privateEnd.Y)

Now let's look at the Hatched line. Think of it as a little lesson in using GDI+ and a few aspects of the Microsoft® .NET Framework. Adding support for the hatched line only requires a few tweaks. The enum for the hatched style is already in place, so you only need to add support for this style in the Paint event. The first step in creating the hatched style is to create a HatchBrush instead of a SolidBrush:

Dim HatchedBrush As New HatchBrush(HatchStyle.DiagonalCross, privateColor)

The HatchStyle allows you to select a variety of options. This example uses a hardcoded style, but of course you can easily add an enum and property for the hatched style and implement a property for it. Then you simply change the HatchStyle to point to that variable. The rest of the code is the same as the Solid line:

LinePen = New Pen(HatchedBrush, privateWeightPixels) e.Graphics.DrawLine(LinePen, privateStart.X, privateStart.Y, _ privateEnd.X, privateEnd.Y)

Now, let's look at the code that handles the click event. The first thing to do is track where the user clicks the mouse. This is handled in the MouseMove event, which picks up the x and y coordinates of the clicked location:

Private Sub MyLine_MouseMove(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) _ Handles MyBase.MouseMove privateCurrentMouseX = e.X privateCurrentMouseY = e.Y End Sub

When the user clicks the line, they are really just clicking the UserControl, so the actual click is processed by the Click event for the UserControl.

Private Sub MyLine_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles MyBase.Click RaiseEvent LineClick(New Point(privateCurrentMouseX, _ privateCurrentMouseY)) End Sub

As you can see, the LineClick event is fired in the Click event for the UserControl. The already set mouse coordinates are then passed to this event.

Figure 5 shows the key event procedures from the form which uses the new line control, FormTwo.vb. The form load event checks several properties in the line control (MyLine1) and modifies the interface as necessary for those controls. The cmdChange_Click sets the control from properties set on the form. This will repaint the control with the new values. The exception to this is the color picker, which repaints the control by resetting the LineColor property directly.

Figure 5 Selected Events from FormTwo.vb

Private Sub frmTwo_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load numEnd.Value = MyLine1.LineWidth lblLineColor.Text = MyLine1.LineColor.ToString Select Case MyLine1.LineWeight Case LineWeightStyles.Small rdoWeightSmall.Checked = True Case LineWeightStyles.Medium rdoWeightMedium.Checked = True Case LineWeightStyles.Large rdoWeightLarge.Checked = True Case LineWeightStyles.XLarge rdoWeightXLarge.Checked = True Case LineWeightStyles.XXLarge rdoWeightXXLarge.Checked = True End Select If MyLine1.LineStyle = _LineStyles.Hatched Then chkStyleHatched.Checked = True Else chkStyleHatched.Checked = False End If End Sub Private Sub cmdChange_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdChange.Click MyLine1.LineWidth = CInt(numEnd.Value) If rdoWeightSmall.Checked Then MyLine1.LineWeight = LineWeightStyles.Small ElseIf rdoWeightMedium.Checked Then MyLine1.LineWeight = LineWeightStyles.Medium ElseIf rdoWeightLarge.Checked Then MyLine1.LineWeight = LineWeightStyles.Large ElseIf rdoWeightXLarge.Checked Then MyLine1.LineWeight = LineWeightStyles.XLarge ElseIf rdoWeightXXLarge.Checked Then MyLine1.LineWeight = LineWeightStyles.XXLarge End If If chkStyleHatched.Checked Then MyLine1.LineStyle = LineStyles.Hatched Else MyLine1.LineStyle = LineStyles.Solid End If StatusBar1.Text = "Weight is " & MyLine1.LineWeight.ToString End Sub Private Sub cmdSelectColor_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdSelectColor.Click Dim oDialog As New ColorDialog() oDialog.ShowHelp = True If (oDialog.ShowDialog() = DialogResult.OK) Then MyLine1.LineColor = oDialog.Color lblLineColor.Text = MyLine1.LineColor.ToString End If End Sub Private Sub MyLine1_LineClick( _ ByVal ClickedLocation As System.Drawing.Point) _ Handles MyLine1.LineClick StatusBar1.Text = "You clicked the line at " & _ ClickedLocation.ToString End Sub

The most important code in FormTwo that is related to your question is the MyLine1_LineClick shown in the following code:

Private Sub MyLine1_LineClick( _ ByVal ClickedLocation As System.Drawing.Point) Handles MyLine1.LineClick StatusBar1.Text = "You clicked the line at " & _ ClickedLocation.ToString End Sub

This implements the LineClick event from MyLine, which fires when the line is clicked. This event simply takes the location that's clicked and shows it in the status bar.

That's it. You now have a line control that can show either a solid or hatched horizontal line.

As mentioned earlier, you are not limited to a line control using this technique. You can build anything—such as a rectangle, circle, curve, sophisticated graphics, and so on. All of these controls can respond to any mouse event you want to implement because you can intercept the click to the UserControl just as this control does.

The only trick in implementing many other kinds of graphics will be how you handle non-rectangular images and other shapes. Let's say you need to implement a diagonal line. To do so you'll need to handle the non-rectangular shape in order to prevent the control from blocking other controls that are behind it and outside of the drawn area. The second issue concerns oddly shaped graphics. For non-horizontal and non-vertical shapes, you must calculate where on the control the user clicked and possibly check that a point is contained within a region. If you try this yourself, you'll see that it's pretty straightforward, but more difficult than implementing simple horizontal and vertical shapes.

Send your questions and comments for Ken to  basics@microsoft.com.

Ken Spencer works for 32X Tech (https://www.32X.com), where he provides training, software development, and consulting services on Microsoft technologies.