Programming for Fun and Profit - Using the Card.dll
Some days and some code just feels like work. If you are like a little like me then you really love programming. If you also a little like me then some days and some code just feels like work.
There are many clever business people that contrive especially creative ways to make money with software. Unfortunately, not all of this software is very interesting to write. Some software and some days lack that frivolous sense of play that writing code is for me. To regain my happy thoughts, believe it or not, I often write reams of code just for fun. And, sometimes this frivolity is more fun and more interesting than work to the point of distraction. This essential play reinvigorates desire and like honing any skill can educate and help improve the art form.
Recently I stumbled across the cards.dll and had a lot of fun whipping together a reasonably good BlackJack game. In this article you get a chance to play with me and in the process learn a bit about using device contexts, the cards.dll API, and we will include a brief discussion on deriving good object-oriented abstractions. (Because the BlackJack game is already done we will frame our discussion in preparation for building the game, War.)
Working with Device Contexts and Graphics
DC, Device Context, hDC, canvas, and the Graphics class all generically or specifically refer to the same thing: the screen real estate that images are drawn on to create what you see on your computer's monitor. The hDC—handle to a device context—is the way the Windows API thinks of your screen's drawing surface, and the Graphics class is the object-oriented encapsulation of the hDC. If we are working at the Windows API level then we need an hDC; if we are working at the Visual Basic .NET level then we will be working with an instance of the Graphics class.
A real easy way to get an instance of the Graphics object representing a specific form's drawing surface is from the PaintEventArgs.Graphics property, and an easy way to get an hDC (handle to a device context) is from an instance of the Graphics' object's GetHdc method.
The Graphics object is considered stateless. For example, if you cache a Graphics object and the size of the drawing surface changes your cached instance will be unaware of this change. Due to this stateless nature, instances of the Graphics object and its underlying handle, hDC, should not be cached in .NET. One instance of a Graphics object should be used in the context of one method call, and its subordinate calls, but not beyond that. You will see this technique used in the code examples in the remainder of this article.
Painting Playing Cards
The cards.dll supports games that ship with Windows, like Solitaire. If you know how to use the API in cards.dll then you can draw those great looking cards that you see in the games that ship with Windows. The three basic methods we will need are cdtInit, cdtDrawExt, and cdtTerm. We also need a width and height variable that are set by the cards.dll initialization method, cdtInit. Listing 1 shows a functional definition of these API methods.
Listing 1: Declarations we need to tap into the cards.dll API library that ships with Windows.
Private width As Integer = 0 Private height As Integer = 0 Declare Function cdtInit Lib "cards.dll" (ByRef width As Integer, _ ByRef height As Integer) As Boolean Declare Function cdtDrawExt Lib "cards.dll" (ByVal hdc As IntPtr, _ ByVal x As Integer, ByVal y As Integer, ByVal dx As Integer, _ ByVal dy As Integer, ByVal card As Integer, _ ByVal type As Integer, ByVal color As Long) As Boolean Declare Sub cdtTerm Lib "cards.dll" ()
The basic rhythm to the cards.dll library is to call cdtInit at the start of your card game, call cdtDrawExt each time you need to draw a card, and call cdtTerm when you are finished with the cards.dll library.
The most interesting method is the cdtDrawExt method. The first parameter represents the drawing surface. The next four parameters represent the dimensions of the playing card. The card parameter contains most of the information needed to draw the card, and the type and color help create the visual effect. It requires special knowledge to paint the face of a card as opposed to the back of a card, and we can capture some of this special knowledge in the two wrapper methods shown in listing 2.
Listing 2: Two methods to paint the face of a specific card or the back of a card.
1: Public Sub PaintGraphicFace(ByVal g As Graphics, ByVal x As Integer, _ 2: ByVal y As Integer, ByVal dx As Integer, ByVal dy As Integer) 3: 4: Dim hdc As IntPtr = g.GetHdc() 5: Try 6: Dim Card As Integer = CType(Me.FCardFace, Integer) * 4 + _ 7: CType(Me.FCardSuit, Integer) 8: cdtDrawExt(hdc, x, y, dx, dy, Card, 0, 0) 9: Finally 10: ' If Intellisense doesn't show this method 11: ' unhide advanced members in Tools|Options 12: g.ReleaseHdc(hdc) 13: End Try 14: End Sub 15: 16: Public Sub PaintGraphicBack(ByVal g As Graphics, ByVal x As Integer, _ 17: ByVal y As Integer, ByVal dx As Integer, ByVal dy As Integer) 18: 19: Dim hdc As IntPtr = g.GetHdc() 20: Try 21: ' TODO: Make card style (hardcoded 61) a configurable property 22: cdtDrawExt(hdc, x, y, dx, dy, 61, 1, 0) 23: Finally 24: g.ReleaseHdc(hdc) 25: End Try 26: End Sub
(Line numbers were added for reference only.) PaintGraphicFace takes a Graphics object and the boundary of the card. A card face—Ace of Spades, King of Hearts—needs to be available to the method. (We'll come back to design issues in the next section.) A device context is obtained from the Graphics object, and the hDC represents a resource we need to protect. To that end we will ensure that Graphics.ReleaseHdc is called by using a Try...Finally...End Try resource protection block. The API method cdtDrawExt is called passing the hDC, and the four Cartesian points describing the card boundary. The card value and suit is expressed as a combined value in the card parameter. The type value, if 0 or 2, indicates we want to draw the face of a card, and 1 indicates that the back of a card is desired. The final color parameter is an RGB color. Passing 0 will result in the card being drawn with a default card color.
We use the same cards.dll API method to draw the back of a card. If the card value is 53 and 68 and the type is 1 then one of the possible back designs—between 53 and 68—is painted on the drawing surface. (In listing 2 the style of the cards is hard coded to 61.)
The value for the card suits are clubs equal 0, diamonds equal 1, hearts equal 2, and spades equal 3. The cards themselves are numbered from in order from lowest Ace equaling 0 to highest, King equaling 12. Thus if we want to draw the Ace of Spades then we need to call cdtInit and our PaintGraphicFace method indicating that we want to draw the ace of Spades. Listing 3 shows the code, and figure 1 illustrates the result.
Listing 3: Some sample code to quickly test the cards.dll API.
Public Class Form1 Inherits System.Windows.Forms.Form [ Windows Form Designer generated code ] Private w As Integer = 0 Private h As Integer = 0 Dim FCardFace As Integer Dim FSuit As Integer Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load cdtInit(w, h) FCardFace = 0 FSuit = 3 End Sub Private Sub Form1_Paint(ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint PaintGraphicFace(e.Graphics, 10, 10, 75, 100) End Sub Declare Function cdtInit Lib "cards.dll" (ByRef width As Integer, _ ByRef height As Integer) As Boolean Declare Function cdtDrawExt Lib "cards.dll" (ByVal hdc As IntPtr, _ ByVal x As Integer, ByVal y As Integer, ByVal dx As Integer, _ ByVal dy As Integer, ByVal card As Integer, _ ByVal suit As Integer, ByVal color As Long) As Boolean Declare Sub cdtTerm Lib "cards.dll" () Public Sub PaintGraphicFace(ByVal g As Graphics, ByVal x As Integer, _ ByVal y As Integer, ByVal dx As Integer, ByVal dy As Integer) Dim hdc As IntPtr = g.GetHdc() Try Dim Card As Integer = CType(Me.FCardFace, Integer) * 4 + FSuit cdtDrawExt(hdc, x, y, dx, dy, Card, 0, 0) Finally ' If Intellisense doesn't show this method ' unhide advanced members in Tools|Options g.ReleaseHdc(hdc) End Try End Sub Public Sub PaintGraphicBack(ByVal g As Graphics, ByVal x As Integer, _ ByVal y As Integer, ByVal dx As Integer, ByVal dy As Integer) Dim hdc As IntPtr = g.GetHdc() Try ' TODO: Make card style (hardcoded 61) a configurable property cdtDrawExt(hdc, x, y, dx, dy, 61, 0, 0) Finally g.ReleaseHdc(hdc) End Try End Sub Private Sub Form1_Closing(ByVal sender As Object, _ ByVal e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing cdtTerm() End Sub End Class
Figure 1: The expected Ace of Spades.
The code in listing 3 is what we can euphemistically refer to as hacky code. However, when piecing together some new code very quickly I will often employ a very hacky approach just to get something up and running. For example, this code permits me to very quickly experiment with the card-face drawing algorithm to make sure it produces the desired result.
Now that we have some functional code we can dramatically improve upon the code we have by Refactoring it into useful abstractions.