User-Friendly Field Advance in VB 4.0

by Michael E. Burke

Users of data forms sometimes need strong visual reinforcement to confirm their actions. For example, users may need visual clues to indicate that they're entering data in the correct field. In addition, many users are accustomed to using the [Enter] key to move from field to field while entering data. In this article, we'll show you how to implement these form techniques in your projects. To illustrate, we'll build the form shown in Figure A.

Figure A

We'll write a routine to change the color of the active field in a form.

What not to do

Meeting the needs we've described seems simple enough: Just change the ForeColor and BackColor properties of the active field and capture the [Enter] key, making it act like a tab advance. Implementing a solution is a little tricky, however. For example, you might begin by capturing the [Enter] key and translating it to [Tab] by using the procedure shown here:

Private Sub txt_TextFields_KeyPress _
(KeyAscii As Integer)
   If KeyAscii = 13 Then
      KeyAscii = 0
      SendKeys "{TAB}"
   End If
End Sub

Simple, right? The problem is that when your focus is on a Command button, the program doesn't detect the keypress, and the button fires instead of advancing the focus to the next field on the form.

While it's possible to design some methods to make the above code work correctly, all the capture and retranslating presents a potential maintenance problem for anyone else who doesn't know what you're doing. Fortunately, the Visual Basic Programmer's Guide presents an alternative technique that uses the TabIndex property. The Programmer's Guide code is only a skeleton—not taking into account Data controls, Timer controls, and some third-party controls—but you can build on it to create the example we'll look at here.

Building the example

To keep things simple, we'll build our example around the Biblio.mdp database that comes with Visual Basic. Copy this database from your Visual Basic directory to your working directory, then use the Data Form Designer to build the form. Use the Publishers table for enough fields to make the test worthwhile, and name the form Data1.

We'll leave the form exactly as the Data Form Designer generated it, with two exceptions. First, replace the Data1.Caption text in the Data1_Reposition procedure with the following:

Data1.Caption = "Record: " & _
(Data1.Recordset.AbsolutePosition + 1) & _
" of " & (Data1.Recordset.RecordCount)

Doing so will let us see how many records are in the database.

The second change is to make the Close button's code read

Private Sub cmdClose_Click()
   GsbCloseMe
End Sub

This will ensure that the pointer returns to its normal state instead of remaining as an hourglass. Since the hourglass typically indicates that the system is still engaged, leaving it onscreen would detract from our goal of making the input process less confusing.

In order for the Data control to determine how many records are in the database, you must add the following code to the Form_Activate event:

If data1.Recordset.RecordCount > 0 Then
data1.Recordset.MoveLast
data1.Recordset.MoveFirst
End If

If there are no records, nothing happens. Otherwise, this routine moves the pointer to the end of the record set, thus counting the records, then moves it back to the first record.

Adding a module

The real work of this technique takes place in Module1, shown in Listing A. Create this module now, then we'll see what the code does.

Listing A: Module1 code

Option Explicit
Public gsDBName As String
Public gsTable As String
Public go_dbObject As Database
Public go_rsObject As Recordset

Public Sub gsbBGChangeFocus()
' This routine highlights the active control.
   If TypeOf Screen.ActiveControl Is TextBox
      Or TypeOf Screen.ActiveControl Is ComboBox
      Or TypeOf Screen.ActiveControl Is ListBox
      Or TypeOf Screen.ActiveControl Is MaskEdBox
      Or TypeOf Screen.ActiveControl Is DBCombo
   Then
      Screen.ActiveControl.BackColor = &HFFFFFF
      Screen.ActiveControl.ForeColor = &HFF0000
   End If
End Sub

Public Sub gsbTextClicked()
'This routine changes the highlight and sets the focus if the user clicks the control.

   Dim lcAControl As Control
   For Each lcAControl In Screen.ActiveForm.Controls
      If TypeOf lcAControl Is TextBox 
   Or TypeOf lcAControl Is ComboBox 
   Or TypeOf lcAControl Is ListBox 
   Or TypeOf lcAControl Is MaskEdBox 
   Or TypeOf lcAControl Is DBCombo 
      Then
         lcAControl.BackColor = &HC0E0FF
         lcAControl.ForeColor = &H0
      End If
   Next lcAControl
   gsbBGChangeFocus
End Sub

Public Sub gsbCloseMe()
'Calling this routine makes sure that the form is unloaded and the mouse is reset to normal.
   Unload Screen.ActiveForm
   Screen.MousePointer = vbNormal
End Sub

Public Sub gsbFieldAdvance(KeyAscii As Integer)
'This routine moves the focus and the highlight to the next qualified field when the user presses
'the return key.

   Dim liNextTabIndex As Integer
   Dim liICount As Integer
   Dim liCycCount As Integer
   Dim lbDFlag As Boolean
   Dim lbMATCH As Boolean
   Dim lcAControl As Control
      If KeyAscii = 13 Then
         liICount = 0
         ' Checking to be sure that we don't set the counter beyond the number
         ' of controls on the form.
         If Screen.ActiveControl.TabIndex = Screen.ActiveForm.Controls().Count - 1 Then
            liNextTabIndex = 0
         Else
            liNextTabIndex = Screen.ActiveControl.TabIndex + 1
         End If
      Do Until lbDFlag = True
         lbDFlag = False
         If liNextTabIndex = Screen.ActiveForm.Controls().Count - 1 Then
            liNextTabIndex = 0
         End If
         For Each lcAControl In Screen.ActiveForm.Controls()
            lbMATCH = False
               If TypeOf lcAControl Is Data Or TypeOf lcAControl Is Timer Then
               'Catch controls without tabindexes here...See
               ‘Help for full list, but remember to check custom controls.
                  liICount = liICount
               Else
                  If TypeOf lcAControl Is MaskEdBox Or TypeOf lcAControl Is TextBox _   
                     Or TypeOf lcAControl Is OptionButton _
                     Or TypeOf lcAControl Is CheckBox Then
                     'What controls to highlight
                        If lcAControl.TabIndex = liNextTabIndex Then
                        'if they are next in sequence
                           lbMATCH = True
                           lcAControl.SetFocus
                           lbDFlag = True
                           Exit For
                        End If
                  Else
                     If lcAControl.TabIndex = liNextTabIndex Then
                     'If the index matches but the control is not right, we move on
                        liNextTabIndex = liNextTabIndex + 1
                     End If
                  End If
               End If
               liICount = liICount + 1
               If liICount > 1000 Then
               'Insurance...Maybe the user won't want to CTL-Alt-DELETE....
                  If liCycCount > 5 Then
                  ' and this can be adjusted as much as needed to account for
                  ‘ numerous controls without tab indexes
                     lbDFlag = True
                     MsgBox "Exceeded search cycle limit. Call support."
                     Exit For
                  Else
                     liICount = 0
                     liNextTabIndex = liNextTabIndex +1
                     liCycCount = liCycCount + 1
                  End If
               End If
         Next lcAControl
      Loop
      KeyAscii = 0
   End If
End Sub

The first routine in Module1, gsbBGChangeFocus, changes the foreground and background colors whenever a field on the form receives focus. The next routine, gsbTextClicked, ensures that when the user clicks an entry field, that field changes to an appropriate color. We use this routine as a master so we only have to maintain one routine. The third routine, gsbCloseMe, simply ensures that the form unloads properly.

Most of the real work on the module occurs in gsbFieldAdvance, which handles the movement from one field to another. Let's look at exactly what this routine does.

gsbFieldAdvance

VB will crash the application if it tries to evaluate a control that doesn't exist. However, forms can be expected to have differing numbers of controls. So, after declaring variables, we have the routine count the number of controls to avoid setting the counter beyond the number of controls available. Then we initialize the counter liNextTabIndex to be either one more than the current control, or if we are at the last of our controls, we set it to be 0 (the first control).

The search order will be different for each form; it will also change on any specific form each time you modify any control. Maintaining precise indexing would mean extra work for a programmer each time the form was modified. On the other hand, letting VB keep track of the indexing means we have to be sure that, no matter how the form has changed, the routine will eventually cycle through all the legal controls and change the ForeColor and BackColor to indicate the focus.

The Dataform Designer usually numbers the controls in the order it generates them (which is top-left to bottom-right), but I admit that I actually adjust the TabIndex properties of each form after I make alterations so the user doesn't have to follow eye-popping color changes all over the form. Using a Do loop, however, will ensure that the user can hit [Enter] enough times to land on the right control even if the programmer isn't as picky as I am.

It would've been better to use a Case statement in this routine, but the Select Case statement in VB4 doesn't recognize the Typeof function. Using nested If-Then statements, we trap the controls that would crash the program by making it look for a TabIndex that doesn't exist.

There's a list of these in the online help file under TabIndex, but be sure to check the properties of any third-party controls for other potential problems. You should also examine the controls for which you would normally allow users to enter data and, finally, make provisions for anything that you might miss.

To make sure we get a graceful exit, we count the trips through the Do loop (liICount). The more controls you have on the form, the higher your “insurance” number must be. (One of my forms has 61 controls, and the Do loop executes a minimum of 12 times and a maximum of 684, which explains why my insurance number is 1,000.)

Notice that we use a second counter, liCycCount. If there's more than one control without a TabIndex property, this second variable will allow the Do loop to execute until the process succeeds. It's not unusual to have two or more Data controls and a Timer control. You can adjust the second counter to account for a variety of your forms.

Coding the form

Before we can try out our project, we must make a few changes to the form. Add the code in Listing B to the txtFields object.

Listing B: Code for txtFields object

Private Sub txtFields_Click(Index As Integer)
   gsbTextClicked
End Sub

Private Sub txtFields_GotFocus(Index As Integer)
   gsbTextClicked
End Sub

Private Sub txtFields_KeyPress(Index As _
   Integer, KeyAscii As Integer)
   If KeyAscii = 13 Then
      KeyAscii = 0
      Form_KeyPress (13)
   End If
End Sub

Finally, add the following code to the Form_KeyPress event:

Private Sub Form_KeyPress(KeyAscii As Integer)
   If KeyAscii = 13 Then
      KeyAscii = 0
      gsbFieldAdvance (13)
   End If
End Sub

Notice that the KeyPress event in the fields calls the KeyPress event of the form. This allows the form to trap other behaviors or entry errors, or simply pass the ASCII value to the routine that does the work.

Conclusion

One way to reduce the number of errors that creep into your data is to make forms simple to understand and easy to follow. Using color or contrast to mark the current location during data entry can greatly assist users. In this article, we've shown you how to highlight the active field on a form and how to let users press [Enter] to move among fields.

_____________________

Mike Burke has been programming computers since 1965, when he learned to do cryptology on an IBM 1401 using an assembly language called Autocoder. He's currently consulting on Visual Basic and Java, writing technical articles, and doing technical illustration. You can reach Mike at meburke@electrotex.com.

____________________

This article is reproduced from the February 1998 issue of Inside Visual Basic. Inside Visual Basic is an independently produced publication of The Cobb Group. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of The Cobb Group. To contact The Cobb Group, please call (800) 223-8720 or (502) 493-3200.

Copyright © 1998 The Cobb Group, a division of Ziff-Davis Inc. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Inc. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis is prohibited.