Introduction
This article presents a dPad
(direction pad) control with auto-repeat. Other features include color gradients and custom events. As with my other controls appearing on Code Project, dPad
uses double buffering to produce smooth painting effects.
While considering how to design this control, I initially thought of making a composite control with each of the four direction buttons as separate controls. I abandoned this approach because I wanted the center 'X' shape to act as the diagonal actuators for the control. It ultimately proved easier from the standpoint of drawing the control to simply combine the five elements into one.
Background
The logic behind the hit detection for the control is based on the control being composed of five main regions, the four buttons (up, down, left and right) plus the center 'X' region. A sixth region provides a diamond shaped 'dead zone' in the very center of the control. It's purpose is to ignore clicks in the central area of the control. This feature is controlled by the value of the IgnoreDiamondHits
property. The center diamond shape can be shown or hidden based on the value of the ShowDiamond
property.
Two custom events are defined for the control, ButtonDown
and ButtonUp
. The auto repeat feature allows the control to raise multiple ButtonDown
events while a button on the control is held down. This feature is controlled by the Repeat
property along with the RepeatRate
property. When Repeat
is False
, there is one ButtonDown
event raised when the mouse is pressed on the control, and a ButtonUp
event raised when the mouse is released. When Repeat
is True
, a series of ButtonDown
events are raised when the mouse is pressed and held on the control, at a rate set by RepeatRate
, and finally one ButtonUp
event raised when the mouse is released.
The ButtonDown
event has one parameter, a value from the Buttons
enumeration to indicate which button or buttons are being pressed. The Buttons
enumeration is declared with a FlagsAttribute
attribute, so that values can be combined by OR-ing them together.
Using the code
After downloading and building the solution, you can copy the dPadControl.dll to a convenient location and add it to the Toolbox in Visual Studio. Then add an instance of the control to your project.
The ButtonDown
event is the default event for the dPad
control. Double-clicking the control in the Design view will stub the following code into the Code view:
Private Sub DPad1_ButtonDown(ByVal btn As dPadControl.dPad.Buttons) _
Handles DPad1.ButtonDown
End Sub
In Code view, selecting DPad1
from the Class Name dropdown and then selecting ButtonUp
from the Method Name dropdown will stub in the following:
Private Sub DPad1_ButtonUp() Handles DPad1.ButtonUp
End Sub
Gradient type brushes are used throughout. If you want the buttons, for example, to be of a solid color, you must select the same color for the corresponding properties, e.g. ButtonColor
and ButtonBlendColor
.
Points of Interest
Before creating this control, I had some experience with .NET's GDI+, but I had never used any matrix operations. This proved to be quite a learning experience! One consideration was to try to do as much of the work as possible as few times as possible. To this end, I placed the heavier work of creating the regions used to draw the control and do the hit testing in the overridden OnResize
Sub
. Of course, this meant that the regions needed to be declared at the class level and persisted throughout the lifetime of the control, but this seemed a fair tradeoff to the alternative of calculating them each time the control was painted. Here is the code for the OnResize
Sub
:
Protected Overrides Sub OnResize(ByVal e As EventArgs)
Dim gp As New GraphicsPath
Dim gt As New GraphicsPath
Dim ga As New GraphicsPath
Dim mtx As Matrix
Dim cRectf As RectangleF
Dim pmid As New ArrayList
SetClientSizeCore(ClientSize.Width, ClientSize.Height)
cRectf = [RectangleF].op_Implicit(ClientRectangle)
cRectf.Width -= 1.0F : cRectf.Height -= 1.0F
If mshape = Shapes.Round Then
gp.AddArc(0, 0, cRectf.Width, cRectf.Height, 228.0F, 84.0F)
gp.AddLine(gp.GetLastPoint(), _
New PointF(cRectf.Width / 2.0F, cRectf.Height * 0.375F))
Else
gp.AddPie(0, 0, cRectf.Width, cRectf.Height * 0.75F, 216.87F, 106.26F)
End If
AddPoints(gp.PathPoints, pmid)
rn = New Region(gp)
ga = gp.Clone
mtx = New Matrix(0, 1, -1, 0, cRectf.Width, 0)
gt = gp.Clone
gt.Transform(mtx)
re = New Region(gt)
ga.AddPath(gt, False)
AddPoints(gt.PathPoints, pmid)
mtx = New Matrix(-1, 0, 0, -1, cRectf.Width, cRectf.Height)
gt = gp.Clone
gt.Transform(mtx)
rs = New Region(gt)
ga.AddPath(gt, False)
AddPoints(gt.PathPoints, pmid)
mtx = New Matrix(0, -1, 1, 0, 0, cRectf.Height)
gt = gp.Clone
gt.Transform(mtx)
rw = New Region(gt)
ga.AddPath(gt, False)
AddPoints(gt.PathPoints, pmid, True)
gt.Reset()
gt.AddPolygon(mArray)
rx = New Region(gt)
ga.AddPath(gt, False)
Me.Region = New Region(ga)
gt.Reset()
gt.AddPolygon(cArray)
rd = New Region(gt)
pmid.Clear() : mtx.Dispose()
gt.Dispose() : gp.Dispose() : ga.Dispose()
Me.Invalidate()
End Sub
Private Sub AddPoints(ByVal pa As PointF(), ByRef mi As ArrayList, _
Optional ByVal copy As Boolean = False)
With mi
If mshape = Shapes.Round Then
.Add(pa(0)) : .Add(pa(4)) : .Add(pa(3))
Else
.Add(pa(1)) : .Add(pa(0)) : .Add(pa(4))
End If
If copy Then
.CopyTo(mArray)
Dim m() As Integer = {1, 4, 7, 10}
For n As Integer = 0 To 3
cArray(n) = mArray(m(n))
Next
End If
End With
End Sub
Protected Overrides Sub SetClientSizeCore(ByVal x As Integer, _
ByVal y As Integer)
If x > y Then
MyBase.SetClientSizeCore(x, x)
Else
MyBase.SetClientSizeCore(y, y)
End If
End Sub
The first effective line calls the overridden SetClientSizeCore
method. This has the effect of forcing the control to remain square. The next lines create a RectangleF
structure from the control's ClientRectangle and reduce the width and height by 1. This accounts for the fact that the last point in the width or height is numbered one less than the given width or height, i.e. a width of 100 contains points numbered from 0 to 99.
Next, the gp
GraphicsPath
has a shape added to it based on the shape of the dPad
control from the Shape
property, Round
or ObRound
. This path is used to create the first region, rn
(or region north). The ga
GraphicsPath
is cloned from this path and forms the basis for the region which defines the entire control.
Next follows a series of three transforms, each starting by cloning the gp
path to a temporary gt
GraphicsPath
. After each transform, the next region in turn is created from the transformed path (regions east, south and west), and the transformed path is also added to ga
.
Prior to the first transform and following each transform, a set of three points are added to the pmid
ArrayList
via a call to the AddPoints
Sub
. The final call to AddPoints
with the copy parameter set to True
results in all 12 points stored in pmid
being copied to array mArray
, and four of the stored points being copied to array cArray
.
Finally, gt
is set to the path contained in mArray
and region rx
(the center 'X' shape) is created from this path. This path shape is also added to ga
and the control's entire region is set to path ga
. The last step is to create region rd
(the center diamond shape) from the path in cArray
. The control is then invalidated to cause a repaint.
Events, Event Handlers and NDoc
The easiest way to add your own event/event handler is to add a line like the following:
Public Event MyEvnt()
An event named MyEvnt
has been declared with no parameters. XML style comments have been added above the declaration (I used VBCommenter). Now if you fire up NDoc and compile this code, you will see that your help file claims to be missing documentation for MyEvntEventHandler
. What is happening?
Well, while you weren't looking, Visual Basic decided to help you out. You wrote one line of code but internally Visual Basic actually wrote two lines to replace yours!
Public MyEvnt As MyEvntEventHandler
Delegate Sub MyEvntEventHandler()
Note that in the code editor window, you still see only your original line, but by using Reflection, you will see it transformed into two lines. Well this is exactly how NDoc operates, using Reflection to get at the representation of the code you've written. So if you plan on using NDoc to create a Help file from your project, declare your event handler with the two lines and add appropriate comments. That way your Help file will contain the correct information for both the events and event delegates.
Conclusion
I honestly don't know if anyone will find a good use of this control, but I hope at least that some insight might be gleaned from the code presented here. If you have any questions or suggestions for improving the code, please leave a comment below. Thank you.
History
- Mar 22nd,2005 - Initial release.