Creating the Base Tile Class
You know you’re going render the tiles via GDI vs. an animated bitmap/sprite method, so now it’s time to decide how to organize the Tile class. At first, it may seem natural to create a base ancestor class and have four descendant classes encapsulate the drawing of each shape, as shown in Figure 4-2.
Figure 4-2: First pass at the Tile class
| Caution | You’ll see later (unfortunately for me, much later) how and why this design for the Tile class breaks down. (See the “Developing Lose Your Mind” section.) |
The base class has the name ColoredShape. What’s particularly interesting (and happily accidental) about this class once it’s completed is that it uses a variety of the object-oriented features available in a .NET Framework language, especially for a relatively small class. Listing 4-2 shows the member interface for this class.Listing 4.2: The ColoredShape Base Class Interface
Public MustInherit Class ColoredShape
Inherits Control
Implements IComparable
Public Function CompareTo(ByVal obj As Object) _
As Integer Implements System.IComparable.CompareTo
Property Backwards() As Boolean
Property Border() As Integer
Property Color() As Color
ReadOnly Property ColorWord() As String
MustOverride ReadOnly Property ShapeWord() As String
Overrides Function ToString() As String
Protected Overridable Sub Draw(ByVal g As Graphics)
Protected Overrides Sub OnPaint(ByVal e As _
System.Windows.Forms.PaintEventArgs)
Shadows Property Width() As Integer
Shadows Property Height() As Integer
Public Overloads Function Equals(ByVal s As ColoredShape) As Boolean
Public Shared Function CreateByIndex(ByVal iShape As Integer, _
ByVal iColor As Integer) As ColoredShape
End Class
Just a brief perusal of this class definition should cause you to feel like you’re swimming in .NET keywords. Shared Function, MustOverride, Overrides, Shadows, Implements, Overloads...the list is quite impressive for such a relatively small class. This gives you a golden opportunity to learn all about the class while also learning the meaning of these class keywords. Let’s study each member individually.
Understanding the Class Definition
The class definition inherits from the base class Control, meaning that it acquires all of that class’s functionality for free. In other words, properties that define the control’s location and size already exist, as well as the inner machinery to maintain a window handle. The second part of the class definition declares that this class implements the IComparable interface. An interface is a declared set of properties, events, and methods that provide functionality. However, the author of the interface provides no code implementation for this set of members. Instead, the interface defines a set of functionality that can be obtained by providing the implementation for this subset of members. In the case of the IComparable interface, the functionality provided is the ability to compare two classes to determine which one might come before the other when sorting them. You can imagine different requirements for sorting two classes depending on the problem at hand. For example, you might need to sort two customers alphabetically for one report and then sort them by the dollars they’ve spent at your company for another report. You could do this by implementing separate IComparable interfaces for the Customer class.When you declare that a class implements a specific interface, you must implement every member declared in that interface before the program will compile. You’ll learn more about interfaces, including how to create and use interfaces that you create yourself, in Chapter 6, “Using Polymorphism via Interfaces.”You might observe that the code in Listing 4-1, which lists the member definitions for the ColoredShape class, is a type of code interface in itself. It lists the members for a class without showing their implementation. This code listing as shown isn’t 100-percent legal VB .NET code, however. It shows the actual class implementation and hides everything except for the first line that declares each member. Seeing class definitions in this way is a great first way to understand what a class is supposed to do before learning how it actually does those things.
Understanding the CompareTo Method
The CompareTo method implements the CompareTo method of the IComparable interface. As you can see, the code must specify that this member does indeed implement the member for this interface—simply having the same name as the member interface isn’t enough:
Public Function CompareTo(ByVal obj As Object) _
As Integer Implements System.IComparable.CompareTo
Dim o As ColoredShape = CType(obj, ColoredShape)
Return Me.Top.CompareTo(o.Top)
End Function
This CompareTo method compares the tops of the current control instance and the one passed in, meaning that any sort using this method will sort the tile controls in order of their vertical orientation on the form. You’ll see why you’re using this sorting method in the “Developing DeducTile Reasoning” section later in the chapter.The CompareTo method is the only member contained in the IComparable interface, meaning that the implementation of this method completes the implementation of this interface for this class. Other interfaces can (and usually do) have multiple members in them.
Understanding the Backwards Property
The Backwards property is a fairly uninteresting Boolean property that determines whether the Tile class is currently displaying its shape (Backwards = False) or whether the tile is “flipped over” and hiding its shape (Backwards = True). You define the property using the standard paired private variable method you’ve seen in prior examples:
Private FBackwards As Boolean
Property Backwards() As Boolean
Get
Return FBackwards
End Get
Set(ByVal Value As Boolean)
FBackwards = Value
Invalidate()
End Set
End Property
The only line of code that might be new to you is the call to the Invalidate method, which forces the control to repaint itself. This method should be called on a control whenever the appearance of the control is changing.
Understanding the Border Property
Another standard property, the Border property specifies how many black pixels are to be drawn around the tile. By drawing the black border between tiles “inside” the tile itself, you save yourself from having to compute a border between tiles when trying to arrange them together. In other words, if you have 32-pixel-wide tiles and want to arrange them horizontally, you can simply set their coordinates at 0, 32, 64, and so on. If the black border wasn’t “built into” the control, you’d have to put the tiles at the locations 2, 36, 66, and so on, which is harder to compute. The code for the Border property is as follows:
Private FBorder As Integer = 2
Property Border() As Integer
Get
Return FBorder
End Get
Set(ByVal Value As Integer)
If Value < 0 Then
Throw New Exception("Illegal value")
Else
FBorder = Value
Invalidate()
End If
End Set
End Property
New features in this code include the fact that the private variable initializes to 2 (a value you’ll probably rarely change, but you should reserve the right to change it by implementing this property) and the fact that some range validation happens when the property is set to make sure the value is non-negative. If a negative value is attempted, an exception is thrown and the value isn’t stored.
Understanding the Color Property
The Color property describes the color of the shape. As mentioned, the shape is always one of four colors, so you can’t simply use the existing ForeColor property on the Control class. If you do, nothing prevents a user from setting the color to gray, black, or something that doesn’t make sense for the tile games. Instead, you should implement a new property:
Private FColor As Color = Color.Red
Property Color() As Color
Get
Return FColor
End Get
Set(ByVal Value As Color)
Dim aC() As Color = _
{Color.Red, Color.Blue, Color.Green, Color.Yellow}
If Array.IndexOf(aC, Value) = -1 Then
Throw New Exception(_
"colors constrained to Red/Blue/Green/Yellow")
Else
FColor = Value
Invalidate()
End If
End Set
End Property
This property is of type System.Color, but it performs range checking to make sure the color is constrained to one of the four colors necessary for the tile. If the color is invalid, an exception is thrown. The range check itself uses an array of colors and the Array class’s IndexOf method to determine if the color is valid. You could just as easily use a series of Or clauses, but I find this code to be more readable.
Understanding the ColorWord Property
The ColorWord property is declared ReadOnly, meaning that the user of the class can’t set it. The value of this property is simply the string representation of the color, which is used when a tile needs to be described in text (Red Square). You can use this property when testing your games by writing the solution tile sequence to the debug window, for example, to make sure the clue generation or hint code is creating the correct results:
ReadOnly Property ColorWord() As String
Get
Dim s As String
If Color.Equals(Color.Red) Then
s = "Red"
ElseIf Color.Equals(Color.Blue) Then
s = "Blue"
ElseIf Color.Equals(Color.Green) Then
s = "Green"
ElseIf Color.Equals(Color.Yellow) Then
s = "Yellow"
Else
'won't happen, but just in case
s = Color.ToString
End If
Return s
End Get
End Property
Note that because this is a read-only property, it doesn’t require a Set clause. In fact, the compiler complains if you attempt to add a Set clause to a read-only property.
Understanding the ShapeWord Property
The ShapeWord property is declared both ReadOnly and MustOverride. A property with the keyword MustOverride has no implementation in the base class; therefore, the only code in the base class related to this property is the declaration line shown in Listing 4-2.This property returns the shape of the tile as a string, which is used in the same places that the ColorWord property is used—whenever a tile has to be described in text.
Understanding the ToString Method
The ToString method is declared Overrides, meaning that this method exists in an ancestor class and the functionality is being changed in this descendant class. The ToString method is first implemented way up in the Object class—the root, granddaddy class to all other classes in the .NET Framework.The implementation of the ToString method for this class returns the color and shape of the current Tile object as strings, with a space between them:
Overrides Function ToString() As String
Return Me.ColorWord & " " & Me.ShapeWord
End Function
Examples of what this method might return are Red Circle and Blue Triangle.
Understanding the Draw Method
The Draw method is protected, meaning that it can be called only from within the class and from within descendant classes. In addition, it’s defined as Overridable,meaning that descendant classes can extend the functionality of this method by overriding it. The base method handles the drawing of the background part of the tile. The plan is to have the subclasses handle the drawing of the different shapes:
Protected Overridable Sub Draw(ByVal g As Graphics)
Dim b As LinearGradientBrush
Dim iRad As Integer = Width - (Me.Border * 2)
Dim r As New Rectangle(Me.Border, Me.Border, iRad, iRad)
b =New LinearGradientBrush(r, _
Color.White, Color.DarkGray, LinearGradientMode.Vertical)
g.FillRectangle(b, r)
g.DrawRectangle(Pens.White, r)
End Sub
This code defines a rectangle within the bounds of the control. The Border property defines the left and top edges of the rectangle, and the rectangle’s width and height are defined as the width/height of the control less twice the Border property. This gives you a rectangle that fits inside the control with an even border around it.
| Note | This definition doesn’t consider the position of the control. That is, you don’t have to define the rectangle in relation to where the control might be on its parent form. All drawing within a control happens with the origin in the upper-left corner of the control. |
Drawing the tile happens with a new type of brush. This brush is a LinearGradientBrush, and its function is to draw a color gradient in a specified direction, using two specified colors. The Tile class creates a white-to-gray gradient from top to bottom of the tile, producing a subtle shadow effect. The rectangle is then outlined in white.
Understanding the OnPaint Method
The OnPaint method is part of the Control class; you’re overriding it here so you can extend the original functionality of the control. The OnPaint method is called automatically whenever the control is to be redrawn, so this is the “hook,” or the proper place, to put the drawing code:
Protected Overrides Sub OnPaint(ByVal e As_
System.Windows.Forms.PaintEventArgs)
MyBase.OnPaint(e)
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias
Draw(e.Graphics)
End Sub
The first line calls the OnPaint method in the base class. This ensures that any custom paint event handlers attached to this control by an outside program are called and also that any painting that might be done by the base class actually gets done. For the purposes of this control, the painting is done 100 percent by this class and its subclasses, so this call isn’t strictly needed. However, Microsoft recommends your class overrides always call their base class methods whether strictly needed or not. The second line turns anti-aliased drawing on for this control, which makes the shapes appear smoother. The last line calls the Draw method discussed earlier.
Understanding the Width and Height Properties
Yet another member keyword is used for the width and height properties—the Shadows keyword. By indicating these members with Shadows, you’re saying that although these members exist in the base class, you want to replace the functionality of those members with your own. This is different from overriding a member because a member must be declared Overridable in a base class or you can’t override it in a subclass. You can shadow a member without “permission” from the base class—more like a hostile takeover of a member.The intended purpose of shadowing the Width and Height members in the ColoredShape class is to constrain the shape of the tile to a square. You do this by changing the Width and Height to the same value whenever one of these properties changes. The following code shows the shadowed Width property and shows how it uses the passed-in value to change the width of the control and change the height at the same time:
Shadows Property Width() As Integer
Get
Return MyBase.Width
End Get
Set(ByVal Value As Integer)
MyBase.Width = Value
MyBase.Height = Value
End Set
End Property
Understanding the Equals Method
The Equals method is another one that’s defined way up the class tree in the root class, Object. This method is declared with the keyword Overloads.Anoverloaded method is where the same method name can be declared multiple times within a class, differing only in the parameters passed to it. Consider this simple example:
Public Overloads Function SumTwoNumbers(_
ByVal x As Integer, ByVal y As Integer) As Integer
Return x + y
End Function
Public Overloads Function SumTwoNumbers(_
ByVal x As Single, ByVal y As Single) As Single
Return x + y
End Function
You can declare these two functions in the same class even though they have the same name. When an outsider calls the SumTwoNumbers method, the compiler determines if any of the overloaded methods have a signature matching the one that the caller is attempting and allows the call to compile if it finds a match. If it doesn’t find a match, it produces an error.In the case of the Equals method, it’s usually overloaded so that you can pass in a class of the same type as the class being defined. This allows the method to directly compare whether the two tiles are the same. In these games, two tiles are defined as being the same if they have the same color and the same shape. Therefore, the Equals function compares these two elements and returns True if they’re the same:
Public Overloads Function Equals(ByVal s As ColoredShape) As Boolean
'two tiles equal if color and class equal
Return Me.Color.Equals(s.Color) _
And s.GetType.FullName = Me.GetType.FullName
End Function
Because the shape of each tile is being implemented as a separate subclass, it’s necessary to compare the type of the two classes to determine equality:
Public Overloads Function Equals(ByVal s As ColoredShape) As Boolean
'two tiles equal if color and class equal
Return Me.Color.Equals(s.Color) _
And s.GetType.Equals(Me.GetType)
End Function
Understanding the CreateByIndex Function
This method is declared as Shared Function, which means you don’t actually call this method from an object instance but from the class name itself. The reason you’re implementing this method as shared is that its purpose is to return an instance of the ColoredShape class, based on a passed-in integer representing the color and shape to create. You can’t implement this method on the ColoredShape class itself because you’ve already chosen to mark this class as MustOverride, meaning you can’t create an instance of this class (you can only create its subclasses). To create a method that was usable from the base class, you have to create it as shared.Why do you need a method that creates a tile based on an index? The answer to this is so you can create multiple tiles in a loop. The following code creates 16 tiles, one in each shape/color permutation:
Dim oShape As ColoredShape
Dim i, j as Integer
For i = 0 To 3 '4 shapes...
For j = 0 To 3 '4 colors...
oShape = ColoredShape.CreatByIndex(i, j)
oShape.Width = 48
Me.Controls.Add(oShape)
Next
Next
You can see where having the ability to create many tiles in an integer-based loop might be useful. In fact, you can use code similar to the previous loop to create the starting board for the Brain Drain Concentration game.The method itself merely produces the correct subclass based on the two integers and throws exceptions if the passed-in numbers aren’t in the range 0–3:
Public Shared Function CreateByIndex(ByVal iShape As Integer, _
ByVal iColor As Integer) As ColoredShape
Dim o As ColoredShape
Dim oClr As New Color
Select Case iShape
Case 0
o = New SquareColoredShape
Case 1
o = New CircleColoredShape
Case 2
o = New DiamondColoredShape
Case 3
o = New TriangleColoredShape
Case Else
Throw New Exception("iShape index must be 0-3")
End Select
Select Case iColor
Case 0
o.Color = oClr.Red
Case 1
o.Color = oClr.Yellow
Case 2
o.Color = oClr.Blue
Case 3
o.Color = oClr.Green
Case Else
Throw New Exception("iColor index must be 0-3")
End Select
Return o
End Function
Subclassing the ColoredShape Class
Learning about the ancestor ColoredShape class is educational for learning object-oriented features, which is why this chapter spends a good deal of time going through each member. However, you still don’t have a class you can use in the games. You still need to create descendant classes to implement each shape.Fortunately, the base class has already done most of the work for the class, and all that remains to implement in the subclasses are two methods—the overridden Draw method and the ShapeWord method. Furthermore, the latter of these two methods is trivial; it merely returns a string corresponding to the name of the shape.The Draw method on each subclass is pretty interesting, however, and demonstrates some important features in the GDI+ part of the .NET Framework. Listing 4-3 shows the Draw method of the DiamondColoredShape class.Listing 4.3: Drawing the Diamond-Shaped Tile in a Subclass
Protected Overrides Sub Draw(ByVal g As Graphics)
MyBase.Draw(g) 'draw the background
If Backwards Then Exit Sub
Dim b As New SolidBrush(Me.Color)
Dim iRad As Integer = (Width - Me.Border) \ 2
Dim ogc As GraphicsContainer
Dim r As New Rectangle(-iRad \ 2, -iRad \ 2, iRad, iRad)
ogc = g.BeginContainer
Try
With g
.SmoothingMode = SmoothingMode.AntiAlias
.TranslateTransform(Width \ 2, Width \ 2)
.RotateTransform(45)
.ScaleTransform(0.8, 0.8)
.FillRectangle(b, r)
.DrawRectangle(Pens.Black, r)
End With
Finally
g.EndContainer(ogc)
End Try
End Sub
The first line of the overridden Draw method calls the base class Draw, which you’ve already seen in the code that draws the background of the tile using the cool gradient brush. The next line exits if the tile is backward because the shape on the tile is obscured by design.Once the method determines that the shape is to be drawn, it creates a new class called GraphicsContainer. This object allows you to save the state of a Graphics object and return it without having to undo all of the transformations you’ve done. You accomplish this by calling the BeginContainer method on the Graphics object, which returns an instance of the GraphicsContainer class and then later calls EndContainer to restore the Graphics object to its exact state when the BeginContainer was called. Placing the EndContainer inside a Finally exception handler guarantees that it gets called regardless of any errors that take place beforehand.Now that you have a method to save and restore the state of the Graphics object, what changes do you intend to make to it? For these shape-drawing routines, the goal is to change the coordinate space of the class so you can draw a diamond. One way to draw the diamond is to manually map out the four coordinates and draw them that way. This method would work perfectly for this case, but consider a second method. What if you could declare a rectangle, move it to the middle of the tile, and rotate it 45 degrees? That would also be a diamond.The Graphics class allows you to perform these transformations on its coordinate system on the fly. Listing 4-3 does exactly this—transforms the graphics coordinate system to the middle of the tile, rotates it 45 degrees, and scales it down so that its size is comparable to the other shapes. Once you’ve transformed the Graphics class in this way, drawing the diamond is as easy as declaring a Rectangle object centered on the origin (0, 0) and calling the DrawRectangle method. This is much easier than calculating coordinates yourself—why not have the .NET Framework do the calculations for you?This technique is especially powerful when drawing sprites on the screen. Imagine how easy it’ll be to make a square “roll” across the screen by simply translating and rotating the Graphics class within a loop calling the same DrawingRectangle method over and over. Sounds like the inspiration for a Space Invaders game to me!
Each of the three other tile subclasses performs drawing in a similar way; the difference lies only in which shape is drawn. You can study each as you see fit.