More complex hot spots

Using rectangular hot spots is fine for many uses, such as toolbars, but there may well be cases for which you need to be able to produce hot spots around complex objects. A good example of this is the Living Book series of children’s edutainment, in which the cartoon pages have many hot spots of different shapes that enable the user to interact with the images on the page.

There are two stages to building irregular hot spots, as you’ll see in the following sample application. (Note: This program is intended for Windows 95 only.) The first is defining the hot spot area by using the Windows API to produce a polygon object in memory. After you’ve created the hot spots, you should save them so that you can use them in the second stage. Using this area against a background image, we can activate the hot spot and undertake the action required. The actual method used to save the details will depend on your requirements, but with the improvements to the Jet database engine, this has become a viable option, so we’ll create a hot spot editor using Jet as our storage medium. You will find this application on the CD as Irreg.vbp.

An irregular hot spot A BAS file (DEFINE.BAS) in the application holds the public variable declarations, but more important, it holds the Windows API declarations that make this application work:

' Public definitions for the project
Public bPuStart As Boolean ' Tells system that polygon is
                           ' started.
Public bPuDraw As Boolean ' Put application into draw mode.
Public bPuOpendb As Boolean ' Check for open database.
Public sPuHotdb As String ' Name of the hot spot database
Public dbPuHot As Database ' The database object
Public sPuImageFile As String ' Name and path for the image

' Type definition for the polygon region points
Type POINT32
    x As Long
    y As Long
End Type
    
' 32-bit Windows API declarations
Declare Function WinCreatePolygonRgn Lib "gdi32" _
    Alias "CreatePolygonRgn" (lpPoint As POINT32, _
    ByVal nCount As Long, ByVal nPolyFillMode As Long) _
    As Long
Declare Function WinPtInRegion Lib "gdi32" _
    Alias "PtInRegion" (ByVal hRgn As Long, _
    ByVal x As Long, ByVal y As Long) As Long
Declare Function WinDeleteObject Lib "gdi32" _
    Alias "DeleteObject" (ByVal hObject As Long) As Long

The first two Windows API functions are the ones that create and activate the hot spot. The first, CreatePolygonRgn, takes an array of x,y coordinates to produce a closed polygon object in memory. It uses a structure named POINT32, which is an array of x,y coordinates that make up the polygon. If you do not close the polygon, Windows will close it for you. With the creation program, I’m allowing a 10-pixel error margin in the creation of a hot spot. Feel free to change this if you’re an excellent mouse driver or you have a big monitor and accuracy is not a problem. You might also need to change this if the object you want to make a hot spot is very small.

PtInRegion determines whether you have the mouse pointer within the region specified by a handle, which is the return value of CreatePolygonRgn. At the end of the application, you should destroy all objects that you create by using DeleteObject. If you don’t do this, at best you will get “memory leak,” which means that even after your application has ended, it will still be reserving GDI resource and memory because it was never released. This principle applies for all Windows API objects. It’s excellent practice to tidy up when you’ve finished.

The hot spot editor The next task is to create the database, the structure of which is shown in Figure 11-5, and table structures. This application contains two tables—the first is essentially a hot spot header table that holds the image details, and the second is the hot spot ID. The hot spot ID table is the parent of a table that holds the x,y coordinates that make up the hot spot.

Figure 11-5 The hot spot database

You will find a blank database on the CD named HotBlank.MDB. This is the template from which you can set up hot spot databases. To use the application, copy the project and directory structure to your hard disk. Be sure that HotBlank.MDB is in the database directory and that the bitmaps directory also exists under the application (or alter the code in the menu events).

The application will allow you to load an image into the picture box and then set up and test the hot spots for the image. When you complete a hot spot, write its details to the database by choosing Save Hot Spot from the Hot Spot menu. When you add a new image, the filename will be loaded into the Spot table, and the image will be copied into the bitmaps directory below the application directory. This approach ensures that no absolute paths are used and produces a tidy directory structure.

For simplicity, I didn’t include the ability to edit the data on the database or the ability to edit hot spot details, although you can add hot spots to a saved image. Try writing these yourself. This will help you become familiar with the techniques involved, and you’ll find that you learn more by actually coding than reading about it.

The hot spot editor at run time is shown in Figure 11-6.

Figure 11-6 The hot spot editor at run time

Figure 11-7 The hot spot editor menu structure

To open a saved database, select the details that you want to load from the Image ID combo box. The editor will load the image and set up the hot spot details saved in the database. When you select a new database, the database is created from the blank database and then is ready for you to create image and hot spot details. The menu structure for the hot spot editor is shown in Figure 11-7.

Taking it from the top, let’s see what each of the menu items does, and review some of the more interesting code.

The File New menu item This menu item uses the CommonDialog control to get the path and filename for your new hot spot database and then copies the blank hot spot database to the specified location. Finally, the database is opened, along with the recordsets for the two tables.

Note I’m using dynaset recordsets so that the application can run against other backend databases. If you’re certain that you will be using only Jet, you can use table types to be more efficient.

At the end of the event, the indexes and the hot spot array are reset, ready for you to enter the new details.

The File Open and Menu Exit items The File Open menu item is similar to File New, with a few notable exceptions. The initial code at the start (not shown here) gets the name and path of an existing database. If a database is open, the HotSpotDBClose procedure is run. HotSpotDBClose closes the database and resets the hot spots. (We’ll look at these a little later.) Then the recordsets are opened, and a combo box is populated with the Image IDs from the hot spot table. These are used to load the associated image at a later date. The error trap is used to determine whether any images are set up. In this instance, the error 3021 means that there are no records in the recordset.

The Menu Exit menu item calls the HotSpotDBClose procedure and then unloads the form.

If bPuOpendb Then Call HotSpotDBClose ' Close the current
                                          ' database.
    ' Open the database in the default workspace.
    Set dbPuHot = DBEngine.Workspaces(0).OpenDatabase(sPuHotdb)
    bPuOpendb = True

    ' Open the recordsets.
    Set rsPiSpot = dbPuHot.OpenRecordset("Spot", dbOpenDynaset)
    Set rsPiPoint = dbPuHot.OpenRecordset("Points", _
        dbOpenDynaset)
    mnuHotSpot.Enabled = True

    ' Load the image IDs into the combo box.
    rsPiSpot.MoveFirst
    sID = rsPiSpot!ImageID

    Do
        cboImageID.AddItem sID
        rsPiSpot.FindLast "imageid = '" & sID & "'" ' Find the
                                                    ' last record
                                                    ' with the ID.
        ' Move to the next record. (This should be the first of
        ' a new image.)

        rsPiSpot.MoveNext
        If rsPiSpot.EOF = False Then sID = rsPiSpot!ImageID
    Loop Until rsPiSpot.EOF

    Exit Sub
ErrDetect:
' Handle the blank database error.
    If Err.Number = 3021 Then ' No current record
        MsgBox "The database is blank; please set up a " & _
            "new set of details", vbInformation
        Exit Sub
    End If

    MsgBox "The error " & Err.Description & " has occurred"

End Sub

The New Image menu item The initial section of code calls the common dialog box to get the name of the image to be loaded. The filename is extracted from the file details with a small function named FileNameGet. The ResetHotSpots procedure is called to clear all of the currently specified hot spots. The image is copied and loaded, and the user is reminded to enter a unique Image ID and a name for the image. If you don’t select an image, a message box is displayed, and the procedure exits.

The Load Image menu item This is a little more complex than the New Image menu item because we need to get the details from the database and then load the image and set up the currently defined hot spots. Because this is the key to the editor, here is the full event code:

Private Sub mnuImageLoad_Click()
' Load an image from the database, and set the indexes
' and the hot spots.
Dim nSpot   As Integer ' Current hot spot
Dim nPoints As Integer ' Number of points for the hot spot

    If cboImageID.Text = "" Then
        MsgBox "Please select an image ID to load!", _
            vbCritical, "Image Load Error"
        cboImageID.SetFocus
        Exit Sub
    End If
    ResetHotSpots ' Reset the indexes.
    
    ' Get the Image details.
    rsPiSpot.FindFirst "ImageID = '" & cboImageID.Text & "'"
    If rsPiSpot.NoMatch Then
        MsgBox "The ID you have selected does not exist. " & _
            "Please select from the combo box", vbCritical, _
            "Image Load Error"
        cboImageID.SetFocus
        Exit Sub
    End If

    txtName.Text = rsPiSpot!imagename
    txtName.Enabled = False
    sPuImageFile = App.Path & "\bitmaps\" & _
        rsPiSpot!ImageLocation
    lblLocation = sPuImageFile

    picHot.Picture = LoadPicture(sPuImageFile)

    ' Set up the current hot spots from the points table.
    rsPiPoint.FindFirst "ImageID = '" & cboImageID.Text & "'"

    nSpot = rsPiPoint!HotSpotID
    nPoints = 0
     
    Do
        If nSpot = rsPiPoint!HotSpotID Then ' Same hot spot
            PolyPoints(rsPiPoint!PointNo).x = rsPiPoint!x
            PolyPoints(rsPiPoint!PointNo).y = rsPiPoint!y
            nPoints = nPoints + 1
        Else ' Create the hot spot region and save it.
            ReDim Preserve lPiHotSpots(nSpot)
            lPiHotSpots(nSpot) = WinCreatePolygonRgn( _
                PolyPoints(0), nPoints, 0)

            nSpot = rsPiPoint!HotSpotID
            PolyPoints(rsPiPoint!PointNo).x = rsPiPoint!x
            PolyPoints(rsPiPoint!PointNo).y = rsPiPoint!y
            nPoints = 1

        End If
        rsPiPoint.FindNext "ImageID = '" & _
            cboImageID.Text & "'"
        If rsPiPoint.NoMatch Then ' Save the last hot spot.
            ReDim Preserve lPiHotSpots(nSpot)
            rsPiPoint.FindLast "ImageID = '" & _
                cboImageID.Text & "'"
            lPiHotSpots(nSpot) = WinCreatePolygonRgn( _
                PolyPoints(0), rsPiPoint!PointNo + 1, 0)
            nPiHotIndex = rsPiPoint!HotSpotID
            lblHotSpotID = nPiHotIndex
            rsPiPoint.FindNext "ImageID = '" & _
                cboImageID.Text & "'"
        End If

    Loop Until rsPiPoint.NoMatch

End Sub

The image details are loaded from the selection in the combo box, and the hot spots are cleared. Because we have all of the reference details, these are displayed and then the text boxes are disabled. Now we need to set up the hot spots for the selected image. This involves looping through the spot recordset for the selected image, using FindNext, and picking up the associated x,y coordinates from the points table. These are then loaded into the Polypoints array. When the hot spot ID changes, the hot spot is created using CreatePolygonRgn. Creating the hot spot requires only a call to CreatePolygonRgn with all of the x,y points declared.

To pass an array structure (in this case, an array of POINT32 structures) to a Windows API function, pass the first element of the array. This has the effect of passing a pointer to the array in memory because all of the array elements are contiguous in memory. The second parameter is the number of elements in the array, which is one more than the index. (Remember that arrays are zero-based unless you use the Option Base statement.) The final parameter is the polygon fill mode. We do not use the polygon fill mode in this application, so set the parameter to 0. You will now be able to add to the hot spots for the loaded image.

The Close Image menu item The Close Image menu item resets the hot spots, removes the image from the picture box, and clears the data from the text boxes.

The Draw and Test Hot Spot menu items The Draw Hot Spot Polygon menu item sets the editor to draw mode by setting the draw flag bpuDraw to true and the Polypoints index to -1 so that when 1 is added to the index, it is at the starting point 0. bPiDirty is used to determine whether a polygon has not been saved.

The Test Hot Spot menu item sets the bpuDraw variable to False, which, in effect, switches the drawing off so that the system can respond to mouse events.

' Define a new hot spot for the image.
    ' Check that the last polygon has been completed first.
    If bPiDirty Then
        MsgBox "You have a polygon defined. Save the " & _
            "hot spot and try again."
        Exit Sub
    End If

    ' Reset the flags to start a new polygon.
    bPuDraw = True
    nPiPointIndex = -1

The Save Hot Spot menu item The Save Hot Spot menu item is used to save all of the hot spot details to the database. If this is the first hot spot that has been defined, the image details are also saved because they are not saved anywhere else.

Private Sub mnuHotSpotCreate_Click()
' Create the polygon region so that the hot spot area can be
' tested, and then save the details to the hot spot database.
Dim nCount As Integer

    If bPiDirty = False Then ' Nothing to save
        MsgBox "You do not have anything unsaved", _
            vbInformation, "Save not required"
        Exit Sub
    End If

    If txtName = "" Or cboImageID = "" Then ' Missing details
        MsgBox "You must supply the name and ID for the image", _
            vbCritical, "Image Detail Error"
        txtName.SetFocus
        Exit Sub
    End If
    txtName.Enabled = False
    ' cboImageID.Enabled = False
    nPiHotIndex = nPiHotIndex + 1
    ReDim Preserve lPiHotSpots(nPiHotIndex)

    lblHotSpotID.Caption = nPiHotIndex

    ' The return variable is the handle to the polygon region.
    lPiHotSpots(nPiHotIndex) = WinCreatePolygonRgn(PolyPoints(0), _
        nPiPointIndex + 1, 0)
    bPiDirty = False

    ' Save the data to the database.
    rsPiSpot.AddNew
    rsPiSpot!ImageID = cboImageID.Text
    rsPiSpot!ImageName = txtName.Text
    rsPiSpot!HotSpotID = nPiHotIndex
    rsPiSpot!ImageLocation = FileNameGet(sPuImageFile)
    rsPiSpot.Update

    ' It's a new image; add its ID to the combo box.
    If nPiHotIndex = 1 Then cboImageID.AddItem cboImageID.Text

    ' Now save the points.
    For nCount = 0 To nPiPointIndex
        rsPiPoint.AddNew
        rsPiPoint!ImageID = cboImageID.Text
        rsPiPoint!HotSpotID = nPiHotIndex
         rsPiPoint!PointNo = nCount
        rsPiPoint!x = PolyPoints(nCount).x
        rsPiPoint!y = PolyPoints(nCount).y
        rsPiPoint.Update
    Next nCount

End Sub

The multiple hot spots are handled by creating an array of polygon handles, which are already defined in memory. The checks at the start of the procedure ensure that there is something to be saved and that the key information is entered into the text boxes to save the details to the database. After the checks, the hot spot ID is generated and displayed in a label. The polygon is then created in memory, and the handle is saved in the array. At the end, the new hot spot details are saved to the database. If it’s the first hot spot of a new image, the image ID is added to the combo box so that you can work on it later without having to restart the application.

The mouse events The mouse events have two modes. In draw mode, the hot spot details are being drawn, and in normal mode, the hot spot details can be checked and tested.

The MouseDown event code is used to save the current hot spot details when the application is in draw mode:

Private Sub picHot_MouseDown(Button As Integer, _
    Shift As Integer, x As Single, y As Single)

' Check the draw mode. If we're in drawing mode, save the
' coordinates to start a new line.

    If bPuDraw Then ' We are in drawing mode.
        nPiPrevX = x
        nPiPrevY = y

        ' Add 1 to the index, and save the points.
        nPiPointIndex = nPiPointIndex + 1
        PolyPoints(nPiPointIndex).x = x
        PolyPoints(nPiPointIndex).y = y
        bPiDirty = True
    End If

End Sub

In the MouseMove event for draw mode, a line is drawn to indicate the hot spot details. In normal mode, the hot spot can be tested by calling a function nHotSpot, which is passed the x,y mouse coordinates and checks to see whether it’s over a hot spot:

Private Sub picHot_MouseMove(Button As Integer, _
    Shift As Integer, x As Single, y As Single)
' Draw the line as the mouse is dragged.

    If bPuDraw And nPiPointIndex > -1 Then ' We are in drawing
                                           ' mode.
        picHot.Line (PolyPoints(nPiPointIndex).x, _
            PolyPoints(nPiPointIndex).y)-(nPiPrevX, nPiPrevY)
        nPiPrevX = x
        nPiPrevY = y
        picHot.Line (PolyPoints(nPiPointIndex).x, _
            PolyPoints(nPiPointIndex).y)-(nPiPrevX, nPiPrevY)

    Else ' We are in hot spot mode, so check the mouse pointer
         ' location.
        Call nHotSpot(x, y)
    End If ' End of draw mode check
End Sub

The MouseMove event has a couple of functions. The first is used when you are in drawing mode and you want to draw a line on the screen that shows the bounds of the hot spot. This is done using the Line method. The PictureBox control draw mode is set to 6 - Invert so that the previous line can be easily erased by drawing over it.

Note There are, in fact, 16 draw modes available. The draw modes determine the pen style used to produce the graphic image. You’ll find out more about graphic objects and draw modes in the Visual Basic help files or on MSDN. If you plan to use graphic objects or you want to produce special effects, you might find some of the draw modes useful.

The first line drawn erases the previous one. Then the x,y coordinates are saved before the second line is drawn. If the hot spot has been drawn, the MouseMove event is used to check whether the mouse pointer is over a hot spot by calling nHotSpot:

Private Function nHotSpot(ByVal inX As Single, _
    ByVal inY As Single) As Integer
' Check whether the mouse is in the hot spot, and pass back the ID.
    Dim nCount As Integer

    For nCount = 0 To nPiHotIndex
        If WinPtInRegion(lPiHotSpots(nCount), inX, inY) Then
            picHot.MousePointer = vbCrosshair
            nHotSpot = nCount
            Exit Function
        Else
            picHot.MousePointer = vbDefault
            nHotSpot = -1
        End If ' End of region check
    Next nCount

End Function

As you can see, the function loops through the hot spot index, checking to see whether the mouse pointer is over a hot spot. If it is, the mouse pointer is altered, and the index of the hot spot is returned. By convention, the pointer shape used is that of a hand, but Visual Basic 5 still does not ship with this cursor as one of its choices, so you’ll need to create one yourself and load it as a custom cursor.

We’ll see this in use next in the MouseUp event:

Private Sub picHot_MouseUp(Button As Integer, _
    Shift As Integer, x As Single, y As Single)

    Dim nDiffX As Integer, nDiffY As Integer

    ' Check whether draw mode is off and hot spot clicked.
    If Not bPuDraw Then ' Hot spot clicked
        
        ' Put function here to decide what to do for each image.
        If nHotSpot(x, y) > -1 Then ' On a hot spot
            MsgBox "You have clicked hot spot " & _
                Str(nHotSpot(x, y))
        End If

        Exit Sub
    End If
          
    If bPuDraw And nPiPointIndex > 0 Then ' We are in drawing mode.
        
        ' Determine the distance from the the start of
        ' the polygon.
        nDiffX = Abs(PolyPoints(0).x - x)
        nDiffY = Abs(PolyPoints(0).y - y)

        ' Check to see whether we're within 10 pixels of the start
        ' of the polygon; if so, close the polygon.
        If nDiffX < 11 And nDiffY < 11 Then bPuDraw = False
    End If ' End of first draw check

End Sub

The checking of the return value of nHotSpot determines the hot spot that has been clicked. The test here is pretty basic, but it does illustrate how to test each of the hot spot regions.

In a full application, to take some action depending on the hot spot clicked will mean checking the image details in the database to find out which image is loaded and which action is required. We’ll see how this works in a bit.

When the application is in draw mode, there is a check to see whether the mouse pointer is within 10 pixels so that the draw mode can be switched off. (The Windows API call to create the polygon will close it for you.) If you need better accuracy for smaller hot spots, alter the check to the accuracy required. I must confess that I really hate drawing packages that insist on a pixel accuracy to close a polygon, especially if I’m working on a small monitor.

That’s pretty much all of the code covered except for the two tidy-up routines mentioned earlier:

Private Sub ResetHotSpots()
' This procedure clears the hot spots and resets the ID
' and the index.
    Dim nCount As Integer
    Dim lResult As Long

' Loop through the hot spots.
    For nCount = 0 To nPiHotIndex
        lResult = WinDeleteObject(lPiHotSpots(nCount))
    Next nCount

    nPiHotIndex = 0
    lblHotSpotID = nPiHotIndex
    lblLocation = ""

End Sub

The ResetHotSpots routine clears any currently defined hot spots from memory by looping through the hot spot array and deleting all of the objects. This is called by the following:

Private Sub HotSpotDBClose()
' This routine will destroy all of the current hot spots
' and close the database that's open.

    ResetHot spots

    rsPiSpot.Close ' Close the recordsets.
    rsPiPoint.Close
    dbPuHot.Close ' Close the open database.

End Sub

After deleting the hot spots, this code closes the currently open database and then closes the recordsets. And that’s all there is to it! If you look closely, you’ll see that we’ve managed to build a reasonable set of functionality for an irregular hot spot editor with very little code. OK, it’s not completely bulletproof, but it does demonstrate the techniques and methods required to be able to produce fully customizable user interfaces. It also demonstrates that what appears at first to be a very difficult and complex task is relatively simple once you understand the techniques.