Platinum Edition Using Visual Basic 5

Previous chapterNext chapterContents


- 12 -
Using Control Arrays

Control arrays are an extraordinary tool in the hands of creative VB programmers. This chapter shows how to make the best use of them.
You can create control arrays in many ways, and you can even add new items at run time, enabling you to "magically" create new controls from out of nowhere.
There are many reasons for using arrays of controls on your forms, ranging from the ability to write more concise and cleaner code to the opportunity to reduce the memory footprint of your applications.
Control arrays are often used in conjunction with container controls (controls that can contain other controls, such as frames and picture boxes).
Learn what you can do with control arrays, which range from groups of option buttons to simulating controls that change one or more read-only properties.

When you write large and complex forms, you probably end up with many controls of the same type (text boxes, command buttons, and so forth), like properties and similar event procedures. An example that is very common in all business applications is text fields that restrict their input to numbers only.

It is not difficult to create such text boxes. Simply write some code in their KeyPress event procedures to filter out any non-digit key. But you must write the same code over and over, which surely contrasts with the RAD (Rapid Application Development) nature of Visual Basic.

To help you in these programming chores, Visual Basic offers the capability to build a control array--a group of controls of the same type that share a single set of event procedures. Although the items in a control array have the same name and the same base type, no other restrictions are imposed on them. For instance, the same control array may host both regular and multi-select list boxes; each item may have a different value for its ForeColor, BackColor, or Sorted properties, and so on. However, it is not possible for this array to include controls of different types. For instance, you cannot add combo box controls to an array based on list boxes, even if these types of controls share most of their properties, methods, and events.

Control arrays are very handy on many occasions. Not only do they simplify the implementation of common functionality among multiple controls, but they also permit you to dynamically create new controls at run time, which is not otherwise possible in Visual Basic.

Creating a Control Array

You can create a control array in a number of ways. As different as they may appear, they ultimately deliver identical results. Therefore, you can freely choose any of the following approaches, according to your preferences and mood.

Changing the Name Property

Say you have two TextBox controls on your form, Text1 and Text2. You realize that they should be part of the same control array, named txtArray. Simply go to the Properties window and type the txtArray string for the Name property of both of them.


NOTE: If you're not sure whether you want or need to use control arrays, skip ahead to the "The Benefits of Control Arrays" section later in the chapter. Convinced, you then can return to this material and get about the business of creating your arrays.

As soon as you confirm the string for the second control, Visual Basic realizes that you might want to create a control array and shows the message box in Figure 12.1. Just press Y to confirm, and you'll have a brand new array of controls.

FIG. 12.1
This message box informs that you are about to create a control array.

This procedure is most appropriate when you are creating a control array that includes two or more controls that already exist on the form. If you know in advance that you want to create a control array, other procedures might be preferable.


CAUTION: You should always make your decisions about control arrays before writing the code that references the controls themselves. In fact, you must use a different syntax when referring to an item of a control array; therefore, if you gather two or more existing controls into an array, you are forced to revise all the code that references them before you are able to run the program without syntax errors. You could do some of this work by using a simple Search-and-Replace command, but you would have to rewrite all the relevant event procedures.

Worse yet, even if the program runs without any syntax errors, it probably still conceals a number of subtle bugs that will not appear until later in the development stage. Therefore, you are compelled to re-test every single block of statements that reference the controls.

You can avoid these troubles if you create your control arrays before writing the code that uses the individual controls.


Using the Clipboard

Using the Clipboard is the most common way to create control arrays. Say you place a text box on your form. If you want it to be the first item of your brand new control array, you can just follow this simple procedure:

1. Select the control by using the mouse.

2. Choose Edit, Copy; click the Copy button; or press Ctrl+C.

3. Choose Edit, Paste; click the Paste button; or press Ctrl+V.

4. Press Y to confirm the message box that appears in Figure 12.1.

When you copy a control to the Clipboard and then paste it back to the form, you are implicitly asking Visual Basic to create a replica of the original control. The new control inherits all the properties of the original one, including its name. When Visual Basic detects an attempt to create a control with the same name as another control on the same form, it assumes that you mean to create a control array and asks for a confirmation.


CAUTION: After you press Ctrl+C to copy the selected control to the Clipboard, the control is no longer selected. The selection handles move to the parent form instead. This happens because VB anticipates your intention to paste the copy on the form surface, which is exactly what you mean to do most of the time. In fact, the control in the Clipboard is pasted on the object--form or control--that is currently selected.

However, if you need to place the new control within another container control--a PictureBox or Frame control, for example--you need to select the container before pressing Ctrl+V.


If you reply No to the message box, VB creates a new control of the same type as the original; the new control inherits all the original properties except the name. Two controls may share a common name only if they belong to the same control array.


TIP: Experienced Visual Basic programmers often take advantage of this feature to create controls that are of the same type and share similar attributes, even if they don't belong to a control array. For instance, suppose that you must create several multi-lined TextBox controls of the same size, with their Text property set to a null string and their ScrollBars property set to 3 - Both. You may save a lot of typing and mouse activity by creating the "prototype" TextBox control, setting its properties as required, and then following the preceding procedure up to step 3. Then you answer No when the message box appears.

This procedure even works across forms. In fact, you can copy controls from one form to another by copying them to the Clipboard and pasting them wherever you need them. VB won't even try to create a control array (unless, of course, the target form already includes a control with the same name). This procedure even works with groups of controls, which you can copy and paste using a single action.


This procedure is most appropriate if you are placing new controls on the form, and you know in advance that they must belong to a control array. (You may sometimes decide that a control is part of a control array only after you have placed it on the form.) You cannot use this Clipboard procedure if you are building a control array that initially includes only one item.

Setting the Index Property

If you followed one of these suggested procedures, you may have noticed that Visual Basic automatically assigns a value to the Index property when you confirm the creation of a control array. This value is the numeric index that identifies an individual item belonging to a control array. Usually the first control of the array is assigned a zero index, the second control corresponds to Index = 1, and so on. However, indexes may assume any positive integer value, and they don't need to be consecutive (even if in most cases they are). The only obvious restriction is that two distinct controls in the same array can't receive the same index.

When you create a regular control that doesn't belong to a control array, its Index property is left blank. But if you go to the Properties window and manually change it to an integer value, you may turn it into an element of a control array. No message box appears in this case because Visual Basic assumes that you know what you're doing when you play with the Index property.

Setting the Index property is the only procedure that permits you to build control arrays consisting of a single element. Even if an array with only one element seems pretty useless, such arrays are very common if you plan to add items dynamically at run time.

See "The Load and Unload Commands" section in this chapter, for more information about creating controls at run time.

Adding Items

After you have created a control array, you may add as many items as you need. You can add an existing control to the array by changing its Name property, or you can apply the copy-and-paste procedure outlined previously. The latter technique is most effective when you are adding many items. Just copy a control to the Clipboard once, and then paste it repeatedly for the appropriate number of times.

Whatever procedure you are following, Visual Basic creates the control array and won't show the same warning again after you reply Yes to the first message box.


NOTE: When you paste a control from the Clipboard to your forms, its Index property is set to the lowest unused value, starting with 0. This does not pose a particular problem unless your program expects the lowest index of the control array to be 1 or any other non-null value.

If you need to create a one-based control array with N items, use the copy-and-paste procedure previously described to create N+1 items--with indexes ranging from zero to N--and then delete the first item.

When the first element of a control array has a non-null index, you may have problems when you later decide to add one or more controls to the control array. In fact, the usual copy-and-paste approach creates a zero-indexed element. You have to remember to manually change its index to a correct value. In most cases, using zero-based control arrays is preferable and tends to prevent a number of elusive bugs. If your programming logic expects a control array with a different initial index, at least add a remark in code.


Removing Items

If you want to remove an item from a control array and turn it into a regular, stand-alone control, Visual Basic doesn't permit you simply to reset the control's Index property to the null value (see Figure 12.2). Instead, you have to change its name first, and then blank its Index property value.

FIG. 12.2
This figure demonstrates what happens when you try to blank the Index property value of a control array item that is not the only element of the array.

However, Visual Basic does let you blank the Index property value without first changing the Name property if the current control is the only control in the array. This procedure lets you undo an accidental creation of a control array that you don't plan to use.

Using the Right Syntax

After you have created a control array, you can manipulate its elements from your code. This section explains how to do so and how to avoid a number of potential pitfalls.

Properties and Methods of Individual Items

You use an index to reference in code a control that belongs to a control array, just as you do for an item of a regular array:

` set a property of a control in an array
txtFields(0).Text = ""
` invoke a method of a control in an array
txtFields(0).Move 100, 200

However, unlike regular arrays, you cannot DIMension a control array, nor can you Erase it. Above all, you cannot create multi-dimensional control arrays: all control arrays are mono-dimensional.

Events

You can have the Visual Basic editor create an empty event procedure by using the same technique that you use for a regular control: you double-click the control (or select its name in the combo box in the upper-left corner of the Code window) and then click the event name in the combo box on the right. Here, for instance, is what you get if you double-click a text box that belongs to a control array:

Private Sub Text1_Change(index As Integer)
End Sub

As you see, this event procedure is passed an additional argument, which is the index of the element that is raising the event. This added argument always precedes any other value passed to the event:

Private Sub Text1_KeyPress(index As Integer, KeyAscii As Integer)
End Sub

Because you have the index, it is very easy to address the properties of the control that raised the event or to invoke one of its methods:

Private Sub Text1_KeyPress(index As Integer, KeyAscii As Integer)
     ` reject the spacebar if the textbox is currently empty
     If Text1(index).Text = "" And KeyAscii = 32 Then
          KeyAscii = 0
     End If
End Sub

The capability to gather all the code related to a group of controls in one place is one of the key features of control arrays. This capability is discussed in more in depth in a moment.

The Control Array Object

Even though it is not clear to many VB programmers, even to a few experts, control arrays are objects in themselves, and they expose a number of methods (see Figure 12.3):

Method What It Returns
Count The number of items in the control array
LBound The index of the first element in the array
UBound The index of the last element in the array
Item(n) The Nth element in the control array

FIG. 12.3
If the List Data Member option is on, you can have the VB IDE suggest all the methods of the Control Array object.

The List Data Member option is found by choosing Tools, Options, and then the Editor tab. The LBound and UBound methods are very similar to the functions with the same names that you use with arrays, but their syntax is different:

` show lower and upper indexes for a regular array
Print LBound(arr), UBound(arr)
` show lower and upper indexes for a control array
Print Command1.LBound, Command1.UBound

At a first glance, it might seem that the Count method is superfluous. With regular arrays you can evaluate the number of items by using LBound and UBound functions. However, always keep in mind that control arrays are not required to contain all the elements with indexes from LBound to UBound; therefore, you can only evaluate the number of elements in a control array by using the provided Count property:

` the number of items in a regular array
Print UBound(arr) - LBound(arr) + 1
` the number of items in a control array
Print Command1.Count

The Item method is almost always omitted, which is possible because it is the default method for the control array object. When you omit it, the usual syntax that we've been using so far results:

` an example of using an explicit Item method
Print Command1.Item(0).Caption
` the same statement, omitting the Item method
Print Command1(0).Caption

Iterating on All of the Controls in an Array

One of the biggest advantages of control arrays is that you can iterate on their items by using a regular For...Next loop. For instance, you can clear the contents of all the text boxes in a control array with just three lines even if the array itself contains dozens of controls:

For i = txtFields.LBound To txtFields.UBound
     txtFields(i).Text = ""
Next

However, the above code fragment generates an error if there are "holes" in the index numbering. If you try this loop on a control array of three elements that have indexes of 1, 2, and 4, for example, an error is raised: Control array element `3' doesn't exist. Fortunately, Visual Basic provides the For Each...Next variant, which works even in such a situation. Here is the correct way to perform the same loop:

Dim ctrl As Control
For Each ctrl In txtFields
     ctrl.Text = ""
Next

You may also declare the object variable used in the loop to be of the same type as the controls in the array (TextBox, in this case), which speeds up the loop:

Dim tb As TextBox
For Each tb In txtFields
     tb.Text = ""
Next

The Load and Unload Commands

Up to this point, this chapter has discussed what are often called static control arrays. The adjective static is used because the array does not change in size during the life of the program. However, a control array can actually grow at run time, in the sense that new controls can be added by code, without having the programmer create them at design time.

You can create new controls in code by using the Load command:

Load txtFields(4)

It is very important that the index between the brackets does not refer to a control that already exists. If it does, a runtime error occurs: Object already loaded, error code 360. If you are not sure about the first available index, you may resort to the UBound method:

Load txtFields(txtFields.UBound + 1)

If you dynamically created an element at run time you could also destroy it by using the Unload command:

Unload txtFields(4)

Keep in mind that the Unload command works exclusively with controls that were created by code earlier in the program. If you attempt to unload a control that appears on the form at design time, you get the error Can't unload controls created at design time - error code 362. And, of course, an error occurs if you try to unload a control that is not loaded at all.

The capability to dynamically create and destroy controls at run time is maybe the most interesting feature of control arrays. This feature offers an elegant and concise way to solve a number of programming problems that would otherwise be very difficult to face. This important topic is discussed later in the chapter in the section "Using Dynamic Control Arrays."

The Benefits of Control Arrays

Now that you understand the basics of control arrays, you learn why and when you should make use of control arrays. You are also introduced to a number of useful routines for centralized input data validation, which you can easily reuse in your future applications.

Easier Data Validation

Earlier you saw that one of the most common uses for control arrays is to create groups of controls of the same type that share similar properties. This capability is especially useful with text box controls, which often require a great deal of code in their event procedures to accomplish a particular behavior.

Say you have a number of fields and you need to automatically convert to uppercase whatever the user types in them. If you gather your textboxes in a control array, you have to write only one event procedure instead of many:

Private Text1_KeyPress(index As Integer, KeyAscii As Integer)
     ` convert the key to upper case
     KeyAscii = Asc(UCase$(Chr$(KeyAscii)))
End Sub

This approach enables you to save many lines of code. You can even add new fields to this control array, which automatically inherit the capability to convert their input to uppercase. Text boxes that reject non-numerical keys are another common example of this approach:

Private Text1_KeyPress(index As Integer, KeyAscii As Integer)
     ` accept only digits
     If KeyAscii >= 32 Then
          ` don't trap control keys like BackSpace and Enter (code < 32)
          If KeyAscii < 48 Or KeyAscii > 57 Then KeyAscii = 0
     End If
End Sub

The preceding routine works well for fields that are supposed to receive positive integer values. You should modify the code as shown in Listing 12.1 if you need to also take negative integers into account:

Listing 12.1 Filtering Out Non-Numerical Keys

Private Text1_KeyPress(index As Integer, KeyAscii As Integer)
     ` accept only positive or negative integer values
     Select Case
          Case Is < 32
               ` it's a control key, do nothing
          Case 48 To 57
               ` it's a digit, do nothing
          Case 45   ` = Asc("-")
               ` it's the minus sign - accept only if it is the first
               ` character in the textbox, and only if there are no other
               ` minus signs
               If Text1(index).SelStart > 0 Or InStr(Text1(index).Text, _
                  "-") > 0 Then
                          KeyAscii = 0
               End If
          Case Else
               ` reject any other character
               KeyAscii = 0
     End Select
End Sub

You can easily expand the code in Listing 12.1 to accommodate text boxes that accept numbers with decimal digits. For more information on this topic (which control arrays play a very marginal role in), see the following sidebar "Robust Data Validation in TextBox Controls."


Robust Data Validation in TextBox Controls
Useful code snippets explaining how to perform simple data validation in a textbox were provided in this chapter to illustrate how you can manage that validation code in a single place. In a real-world commercial application, you should follow a different, more robust approach to this problem.

Control arrays enable you to write one event procedure for similar fields on a given form. But this approach cannot be extended to multiple forms. You end up writing similar event routines in each of your application's forms.

To cope with this situation, you should prepare a number of generic procedures that validate the contents of a text box and call them from within the KeyPress or Change event of each field. A simple example of this routine is shown in Listing 12.2.

Then you can validate any textbox in this simple way:

Private Sub txtLeftMargin_KeyPress(KeyAscii As Integer)
     ValidateInteger txtLeftMargin, KeyAscii
End Sub

Of course, you can do the same if you want to validate the contents of a control in a control array:

Private Sub txtPaperSize_KeyPress(Index As Integer, KeyAscii As Integer)
     ValidateInteger txtPaperSize(Index), KeyAscii
End Sub

Note that you must pass a reference to the particular textbox to the ValidateInteger routine. Otherwise, the routine is unable to understand where it is called from and cannot access any property of the textbox control.

However, the previous routine is still insufficient for really robust applications. A user could still accidentally (or maliciously) insert an invalid value in the field--for instance, by pasting a value from the Clipboard, which does not cause a KeyPress event. In addition, a few particular strings pass the preceding test and still don't form a valid input to the field (a single minus sign, for instance).

It doesn't take long to realize that input validation is a complex matter. Usually you need to place code in more than one event. You should probably monitor the KeyPress, KeyDown, Change, and LostFocus events, which should cooperate to build up a reliable input validation routine.

To simplify the coding, many programmers prefer to perform all of the validation chores when the user clicks the OK or Save buttons or when the form is closed. This approach--called record-level or form-level validation--is usually simpler but has a number of drawbacks. One major drawback is that the user must be requested to go back to the offending field and change its contents if a value is found to be invalid. The record-level approach differs from key-level validation (in which you immediately reject invalid keys) and field-level validation (in which you validate the contents of a field when the user moves the input focus onto another control).

The key to truly robust data validation is adopting a mixture of all three forms of validation:

You can also use key-level validation to ensure that a field contains no more characters than allowed. Use the MaxLength property of textbox controls to reach this result.

You can also use field-level validation to ensure that a field contains exactly N characters--for instance, two-character abbreviations for U.S. states. Or you can use field-level validation to ensure that a field contains at least a given number of characters, which is often required the first time your user types a password.

In some cases, record-level validation is also used instead of field-level validation for those fields whose validation would take too long, and would therefore have the user wait when moving from one field to the next one. Suppose that you are validating a ZIP field using a lookup search into a database to ensure that the code entered by the user is a valid one. If you do a field-level validation, the database search--which is a relatively slow process--would occur anytime the user visits the ZIP code field and would slow down the program to an unacceptable degree.


Listing 12.2 A Generic Routine that Validates Keys Entered by the End User

Sub ValidateInteger(Txt As TextBox, KeyAscii As Integer)  

     ` accept only positive or negative integer values
     Select Case
          Case Is < 32
               ` it's a control key, do nothing
          Case 48 To 57
               ` it's a digit, do nothing
          Case 45   ` = Asc("-")
            ` it's the minus sign - accept only if it is the first
            ` character in the textbox, and only if there are no other
            ` minus signs
               If Text1(index).SelStart > 0 Or InStr(Text1(index).Text, _
                  "-") > 0 Then
                    KeyAscii = 0
               End If
          Case Else
               ` reject any other character
               KeyAscii = 0
     End Select
End Sub

More Concise Code

You have already seen that control arrays tend to reduce the amount of code that you must write to manage a group of homogeneous controls. Let me show what I mean with another example.

Many programmers like to highlight the contents of a text box as soon as the field gets the input focus. Besides offering a visual clue, this approach lets the user edit the current value by pressing a cursor key or replace it with a new string by pressing an alphanumerical key. If all of the text boxes on the form are grouped in one control array, you only have to write one line in their common GotFocus event procedure:

Private Sub Text1_GotFocus(index As Integer)
     ` highlight the field contents when it gets the input focus
     Text1(index).SelStart = 0
     Text1(index).SelLength = Len(Text1(index).Text)
End Sub

Here is another little, but useful, example. If you use a lot of ComboBox controls and want to automatically open the controls' list portion when they get the input focus (thus saving your users the trouble to do this activity themselves), just gather the controls in a control array and add this simple procedure:

Private Sub Combo1_GotFocus(index As Integer)
     ` send an Alt-Down key combination
     SendKeys "%{DOWN}"
End Sub

Reduced File Size and Memory Overhead

Although it may not be immediately apparent, control arrays also enable you to build shorter executable files and save memory during execution. To prove this point, I built a form with 11 text boxes on it and no code at all. I then compiled this form to native code, and obtained an EXE file of 10,752 bytes. I replaced the controls with a control array of 11 text boxes, recompiled the program, and the file shortened to 9,216 bytes. That's about 1.5K less then the original EXE, or 140 fewer bytes for each control. Not really impressive, I admit, when the average system now has 16 or 32 megabytes of RAM.

But what happens if you add some code in the event procedures related to the controls? Suppose that you wish to convert each key to uppercase. If you are dealing with regular TextBox controls, you have to write 11 distinct KeyPress event routines, such as the following:

Private Sub Text1_KeyPress(KeyAscii As Integer)
     KeyAscii = Asc(UCase$(Chr$(KeyAscii)))
End Sub

Of course, if you adopt the control array approach, you only need a single event routine. When compiled to native code, the program based on control arrays is about 5K shorter than the program that uses regular controls (9,728 versus 14,848 bytes), which is about 500 bytes per control.

With a real-world program consisting of dozens of forms and hundreds of controls, you can reasonably expect to save several hundreds of kilobytes, or even more, depending on how complex the code in shared event procedures is. Saving these kilobytes means fewer installation disks, faster download from the Internet, and--above all--more free memory at run time. If your application has a smaller memory footprint, it usually runs faster, especially on lowly machines that only have 8 or 16 megabytes of RAM. The reduced size of your memory footprint means that your application has a larger market--you may sell more of them and make more money.

OK, I got carried away. You probably won't get rich just because you decided to use control arrays. But I think you get the point: control arrays help you deliver smaller and more efficient applications--a fact that you should never overlook.

Readable and Elegant Code

Because they can help you gather in a few places the code associated with multiple controls, control arrays are a very useful means to reduce the huge number of event procedures often necessary in complex Visual Basic programs. Fewer procedures mean easier navigation through the program code, allowing the whole application to be more comfortably maintained.

Many programmers, especially beginners, tend to underestimate the importance of elegance in code and do not put any effort into adding a good amount of remarks, correctly indenting If blocks and loops, and so on. While the "quick and dirty" approach works well for prototypes and small-sized programs and utilities, you should not follow it for commercial quality applications, especially if you plan to maintain and update them for a number of years.

Control arrays give you cleaner code with fewer procedures, so they indirectly increase the readability of your listings. Of course, you still need a disciplined approach to code writing. You are requested to follow a (relatively small) number of guidelines and conventions, but at least you don't have to struggle with hundreds of distinct event procedures.

On the other hand, control arrays do have a readability defect, which is explained in the next section.

A Defect of Control Arrays

This is probably the wrong section for this discussion, but I thought it would be fair to balance the many advantages of control arrays with at least one defect. After all, nothing is perfect, and control arrays are no exception.

Meaningless Names for Controls If you gather more unrelated controls in one control array, you obviously lose the opportunity to assign a meaningful name to each individual control. This trait reduces the overall readability of the program because names like txtQuantity and txtUnitPrice are clearly more descriptive than, say, Text1(1) and Text(2). If you are experiencing this kind of problem, you may reduce its extent by using symbolic constants for indexes, as in:

Const QUANTITY = 1
Const UNITPRICE = 2
Const TOTAL = 3
Private Sub Text1_Change(index As Integer)
     If Text1(QUANTITY).Text <> "" And Text1(UNITPRICE).Text <> "" Then
          Text1(TOTAL).Text = Format$(Val(Text1(QUANTITY).Text) * _
               Val(Text1(UNITPRICE).Text))
     End If
End Sub

Experienced VB programmers know how to write concise code that relies on control arrays and still give controls meaningful names. To fully understand the technique they use, you need to know about object variables.

See "Implementing OOP with Classes in Visual Basic," Chapter 18 for more information about object variables.

If object variables are new to you, you should probably jump over the remainder of this section and come back after reading that chapter.

Improve the Readability of Code Among their many capabilities, object variables let you access controls--either regular controls or members of a control array--by using different, more descriptive names. In the previous example, you might declare three form-level object variables of type TextBox:

Dim txtQuantity As TextBox
Dim txtUnitPrice As TextBox
Dim txtTotal As TextBox

You could then assign the three text boxes to these variables. Be sure to perform the assignments before you reference the variables. The Form_Load event is a good place to do them:

Private Sub Form_Load()
     Set txtQuantity = Text1(0)
     Set txtUnitPrice = Text1(1)
     Set txtTotal = Text1(2)
End Sub

At this point, txtQuantity and Text1(0) actually point to the same control, so you can use them interchangeably:

Private Sub Text1_Change(index As Integer)
      If txtQuantity.Text <> "" And txtUnitPrice.Text <> "" Then
            txtTotal.Text = Format$(Val(txtQuantity.Text) * _
                  Val(txtUnitPrice.Text))
     End If
End Sub

Using the numerical index is still more convenient in certain cases--when writing generic validation code, for instance:

Private Sub Text1_KeyPress(Index As Integer, KeyAscii As Integer)
     ` reject a decimal point if the field already has one
     If Chr$(KeyAscii) = "." Then
          If Instr(Text1(Index).Text, ".") > 0 Then KeyAscii = 0
     End If
End Sub

Using Static Control Arrays

This section shows you a number of interesting uses for static control arrays.


NOTE: Please note that I use the terms Static and Dynamic control arrays only for didactical purposes, and to introduce advanced features in a gradual manner. Regardless of the method you use to create a control array and its elements, you use the same code to manipulate items created at design time and items created dynamically at run time.

Groups of Option Buttons

Option buttons--also known as radio buttons--are used for mutually exclusive choices. Say you have seven option buttons --optSunday, optMonday,...optSaturday. The user can select one button and store the corresponding value into a database. Somewhere in your program you presumably have the following procedure:

Function SelectedWeekDay()
     If optSunday.Value = True Then
          SelectedWeekDay = 0
     ElseIf optMonday.Value = True Then
          SelectedWeekDay = 1
     ` ...
     ` .. all the way down to ...
     ` ...
     ElseIf optSaturday.Value = True Then
          SelectedWeekDay = 6
     End If
End Function

The only good thing you can say about this approach is that it works. It is neither concise nor efficient. You can save some typing if you group all the seven option buttons into an optWeekday control array:

Function SelectedWeekDay() As Integer
     Dim i As Integer
     For i = 0 To 6
          If optWeekday(i).Value = True Then
               SelectedWeekDay = i
               Exit For
          End If
    Next
End Function

You can even use a single routine for all the arrays of option buttons used in your program, saving even more code, as shown in Listing 12.3.

Listing 12.3 A Reusable Routine to Determine which Button is Selected in an Array of Option Buttons

Function SelectedOption(optArray As Object) As Integer
     Dim opt As OptionButton
     SelectedOption = -1
     For Each opt In optArray
          If opt.Value = True Then
               SelectedOption = opt.Index
               Exit For
          End If
    Next
End Function

This second approach, which is based on the For...Each loops, is preferable because it also works with control arrays that have non-consecutive indexes. Note that the function returns -1 if no option button is currently selected, which should never occur in a well-designed program. You call the above procedure as follows:

chosenWeekDay = SelectedOption(optWeekday)

The SelectionOption routine saves you some development time and makes your programs shorter and easier to maintain. But it is as inefficient as the If...ElseIf approach used in the first version of the SelectedWeekDay routine because it has to loop on every item in the array of option buttons. You can avoid this overhead by writing a line of code in the Click event procedure:

` this is a form-level variable
Dim SelectWeekday As Integer
` ...
Private Sub optWeekday_Click(index As Integer)
     SelectWeekday = index
End Function

You can then query the SelectWeekday variable to quickly learn which option button is currently highlighted and thus avoid a relatively inefficient loop through all the items in the array.

Collections of Containers

Container controls are controls that can contain other controls. For example, PictureBox and Frame controls may work as container controls, because you can place any number of other controls on their surfaces. In this case the PictureBox or Frame control is said to be the parent control, and the contained controls are said to be children of that parent control. PictureBox and Frame controls are by no means the only container controls available, but they are probably the most common because they are native VB controls, found right in the standard control Toolbox. However, many third-party vendors sell container controls of a different nature, and you can create ActiveX controls that work as containers right in Visual Basic 5.

Control arrays are very useful when used together with container controls. A common practice among VB programmers is to put a number of controls into a container control in order to change the controls' Visible or Enabled statuses easily. If you set the Visible property of your PictureBox or Frame control to False, its child controls become invisible. Similarly, you can disable several controls in a single operation by setting their container's Enabled property to False.

You need only a few statements to achieve this result:

Private Sub Form_Load()
     Option1(0).Value = 1
     Frame1.Enabled = False
End Sub
Private Sub Option1_Click(Index As Integer)
     Frame1.Enabled = (Index = 1)
End Sub

Note that you don't need to initialize the Option1(0).Value and Frame1.Enabled properties in the Form_Load event because you can initialize them in the Properties window at design time.

You can refine this approach and prepare a pile of frames (or other container controls), only one of which will be visible in a given moment. Suppose you are writing a backup utility and want to implement the capability to perform backup chores once per day, per week, or per month. One way to implement a correct user interface is using an array of option buttons and a parallel array of Frame controls. Each of these Frame controls contains the fields that are relevant to a particular backup option. The code in the option buttons' Click event makes only the relevant frame visible and hides all the others (together with their contained controls).

While this approach is not complex from a conceptual standpoint, preparing the user interface is a nuisance in practice. All of the frames must overlap, which makes them difficult to manage at design time. I suggest using a form that is larger than its size as seen by the end user at runtime and placing the frames as shown in Figure 12.4.

FIG. 12.4
The Backup utility is shown at design time; such layout makes it easier to deal with individual frames.

You don't have to move all the frames--one frame over the other--at design time or set their Visible property in the Properties window. You can do everything with a few statements in the Form_Load event, as shown in Listing 12.4.

Listing 12.4 Initialize All of the Properties in the Form_Load Event

Option Explicit  
Private Sub Form_Load()
     ` pile the Frames one over the other and make
     ` them visible or invisible
     Dim i As Integer
     For i = 0 To 2
          Frame1(i).Move Frame1(0).Left, Frame1(0).Top
          Frame1(i).Visible = optFrequency(i).Value
    ext
     ` init combo boxes
     cboTime.ListIndex = 0
     cboWeekday.ListIndex = 0
     cboDay.ListIndex = 0
End Sub
Private Sub optFrequency_Click(Index As Integer)
     Dim i As Integer
     For i = 0 To 2
          Frame1(i).Visible = (Index = i)
    Next
End Sub

When you are sure about the final effect, you may shrink the form to a proper size. The neat final effect is shown in Figure 12.5.

FIG. 12.5
You can avoid screen clutter by showing only the choices that make sense for the desired frequency of backup.

Containers such as Frames and PictureBox controls are very useful in conjunction with TabStrip controls. The TabStrip is a Windows 95 Common Control that is similar to the SSTab control. It too lets you build tabbed dialog, but with an important difference: The TabStrip control is not a container. This control only offers a way for the end user to select which tab should be shown in a given moment.

To create the illusion of multiple tabs, you should use an array of borderless frames and make only one of them visible in a given moment (see Figure 12.6):

Private Sub TabStrip1_Click()
     Dim i As Integer
     For i = 1 To TabStrip1.Tabs.Count
          Frame1(i).Visible = (i = TabStrip1.SelectedItem.Index)
    Next
End Sub

Fig. 12.6
Arrays of borderless frames are very useful when used with TabStrip controls.

Decorative Controls

By "decorative controls" I mean those controls that are placed on a form exclusively for aesthetic reasons. They have no events associated to them and are never referenced in code. In this sense, many Label controls are decorative, even when they serve as captions for controls--tTextbBoxes, lListbBoxes, and ComboBoxes--that do not have captions of their own. Similarly, the only function of most Frame, Line, and Shape controls is to show themselves on the form.

I find it very convenient to create a number of control arrays on each form: one array for all the Label controls, another for all the Frame controls, and so on. This approach optimizes my applications' performance because control arrays consume less memory at run time. Another benefit of this approach is that when you open the Object drop-down list in the Code window, you don't have to scroll past dozens and dozens of controls that you will never use in code. As a result, the really important controls stand out more clearly.

Menus

You can create arrays of Menu controls too, but the procedure used to create them is completely different than the procedures used for other types of controls. In fact, you can only build an array of Menu controls from within the Menu Editor.

See "Creating a Menu Bar," Chapter 5

When creating an array of menu items, you simply use the same name for more than one item in a given menu and assign increasing values for the Index property. You don't need to use consecutive values (that is, you can leave "holes" in the numbering), and you don't need to start with a zero index. However, you must follow a couple of requirements:

You don't really need to remember these rules because Visual Basic promptly complains when you make a mistake. Please note that the Menu Editor--unlike the Properties window--won't automatically supply indexes for you.

What's the point in organizing your menu items in arrays? Well, doing so gives you cleaner code because you can gather the code of a number of menu commands in a single event procedure. For instance, I usually organize my File and Edit menus as outlined in Listing 12.5:

Listing 12.5 The Skeleton for the Event Procedures of the File and Edit Menus

Private Sub mnuFileItem_Click(Index As Integer)
     Select Case Index
          Case 1   ` New
          Case 2   ` Open
          Case 3   ` Save
          Case 4   ` Save As
          Case 5   ` ------
          Case 6   ` Print
          Case 7   ` Print Setup
          Case 8   ` -------
          Case 9   ` Exit
     End Select
End Sub
Private Sub mnuEditItem_Click(Index As Integer)
     Select Case Index
          Case 1   ` Undo
          Case 2   ` ------
          Case 3   ` Cut
          Case 4   ` Copy
          Case 5   ` Paste
          Case 6   ` Delete
          Case 7   ` Select All
          Case 8   ` -------
          Case 9   ` Word Wrap
     End Select
End Sub

I strongly suggest that you add descriptive remarks (as in the code in Listing 12.5) so that you don't have to go to the Menu Editor to find the correspondence between indexes and menu items (see Figure 12.7).

FIG. 12.7
Create arrays of menu items by explicitly assigning an increasing value to their Index property.


Copying and Pasting Menus
The Visual Basic IDE does ot offer an easy way to copy and paste menus and their associated code from one project to another, which forces you to write everything from scratch when you start a new project.

However, you can copy and paste menus, and even whole menu trees, from one form to another if you open both the source and the destination files using a regular ASCII text editor, such as Notepad. All of the menu entries are near the beginning of the file in a section that is invisible when the FRM file is loaded into the VB environment. Listing 12.6 shows what you see in a file that includes the File and Edit menus seen previously.

To copy the File menu and the associated event procedure to another form, follow these steps:

1. Open both the source file and the destination file, using two instances of the Notepad editor (or a word processor that lets you open multiple documents at the same time).

2. In the first instance of Notepad, select all the lines in the range that start with Begin VB.Menu mnuFile and end with the End directive that precedes the Begin VB.Menu mnuEdit line.

3. Press Ctrl+C and switch to the other instance of Notepad.

4. Move the cursor to the corresponding position in the code.

5. Press Ctrl+V to paste everything there.

6. Complete the copy of the File menu by copying the Sub mnuFile_Click event procedure from the source file and pasting it into the destination file.

The only mistake you can make while following this procedure is to paste the menu definition at the wrong point in the destination file. If the target file already contains one or more menus, understanding where the code should be pasted is rather simple. If the destination file does not contain any menu yet, just keep in mind that menu definitions always go after the definition of any other control on the form. If you make a mistake, you will lose some of the data in the original destination file; therefore, I strongly suggest preparing a backup copy of the destination FRM file before proceeding.


Listing 12.6 The File and Edit Menus, as Actually Stored in the FRM File on Disk

VERSION 5.00
Begin VB.Form Form1
     Caption          =   "Form1"
     ClientHeight    =   3990
     ClientLeft      =   60
     ClientTop       =   630
     ClientWidth     =   7275
     LinkTopic       =   "Form1"
     ScaleHeight     =   3990
     ScaleWidth      =   7275
     StartUpPosition =   3  `Windows Default
     Begin VB.Menu mnuFile
          Caption         =   "&File"
          Begin VB.Menu mnuFileItem
               Caption         =   "&New"
               Index           =   1
               Shortcut        =   ^N
          End
          Begin VB.Menu mnuFileItem
               Caption         =   "&Open ..."
               Index           =   2
               Shortcut        =   ^O
          End
          Begin VB.Menu mnuFileItem
               Caption         =   "&Save"
               Index           =   3
               Shortcut        =   ^S
          End
          Begin VB.Menu mnuFileItem
               Caption         =   "Save &As ..."
               Index           =   4
          End
          Begin VB.Menu mnuFileItem
               Caption         =   "-"
               Index           =   5
          End
          Begin VB.Menu mnuFileItem
               Caption         =   "&Print"
               Index           =   6
               Shortcut        =   ^P
          End
          Begin VB.Menu mnuFileItem
               Caption         =   "Print Set&up"
               Index           =   7
          End
          Begin VB.Menu mnuFileItem
               Caption         =   "-"
               Index           =   8
          End
          Begin VB.Menu mnuFileItem
               Caption         =   "E&xit"
               Index           =   9
          End
     End
     Begin VB.Menu mnuEdit
          Caption         =   "&Edit"
          Begin VB.Menu mnuEditItem
               Caption         =   "&Undo"
               Index           =   1
          End
          Begin VB.Menu mnuEditItem
               Caption         =   "-"
               Index           =   2
          End
          Begin VB.Menu mnuEditItem
               Caption         =   "Cu&t"
               Index           =   3
               Shortcut        =   ^X
          End
          Begin VB.Menu mnuEditItem
               Caption         =   "&Copy"
               Index           =   4
               Shortcut        =   ^C
          End
          Begin VB.Menu mnuEditItem
               Caption         =   "&Paste"
               Index           =   5
               Shortcut        =   ^V
          End
          Begin VB.Menu mnuEditItem
               Caption         =   "&Delete"
               Index           =   6
               Shortcut        =   {DEL}
          End
          Begin VB.Menu mnuEditItem
               Caption         =   "Select &All"
               Index           =   7
               Shortcut        =   ^A
          End
          Begin VB.Menu mnuEditItem
               Caption         =   "-"
               Index           =   8
          End
          Begin VB.Menu mnuEditItem
               Caption         =   "&Word Wrap"
               Index           =   9
          End
     End
End
Attribute VB_Name = "Form1"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Option Explicit
Private Sub mnuFileItem_Click(Index As Integer)
     Select Case Index
          Case 1   ` New
          Case 2   ` Open
          Case 3   ` Save
          Case 4   ` Save As
          Case 5   ` ------
          Case 6   ` Print
          Case 7   ` Print Setup
          Case 8   ` -------
          Case 9   ` Exit
     End Select
End Sub
Private Sub mnuEditItem_Click(Index As Integer)
     Select Case Index
          Case 1   ` Undo
          Case 2   ` ------
          Case 3   ` Cut
          Case 4   ` Copy
          Case 5   ` Paste
          Case 6   ` Delete
          Case 7   ` Select All
          Case 8   ` -------
          Case 9   ` Word Wrap
     End Select
End Sub

Simulating Controls that Change Read-Only Properties

Control arrays are also frequently used to simulate a control that dynamically changes one or more properties that can't normally be modified at run time. Take TextBox controls, for example. Their MultiLine and Scrollbars properties can only be set at design time, so you can't transform a single-line TextBox control into a multiline one--or vice versa--while the program is executing. For the same reason, you cannot change the MultiSelect or the Sorted property of a ListBox control at run time.

You can use control arrays to work around this limit. The trick is to create an array of two (or more) instances of the same control, but with different property settings. At run time you can switch from one to the other by simply hiding the one(s) you are not interested in. Of course, you must ensure that the two controls actually contain the same data. Otherwise, the end user will find that something is not working properly.

It is rather easy to keep list boxes in sync because you usually load them in the Form_Load event and don't change their contents afterwards. To execute the sample code shown in Listing 12.7, create a form, and then add a CheckBox control labeled Multi-Select and an array of two ListBox controls named List1(0) and List1(1), respectively. Don't modify any property of the first list box and set the second list box's MultiSelect property to 2-Extended. Finally, add the following code:

Listing 12.7 Faking a ListBox Control that Changes its MultiSelect Property at Run Time

Private Sub Form_Load()
     ` move the second listbox under the first one
     ` and make it invisible
     List1(1).Move List1(0).Left, List1(0).Top, _
          List1(0).Width, _List1(0).Height
     List1(1).Visible = False
     ` fill the two listboxes with random data
     Dim i
     For i = 1 To 100
          List1(0).AddItem "Item " & i
          List1(1).AddItem "Item " & i
    ext
End Sub
Private Sub chkMultiSelect_Click()
     Dim index As Integer, i As Integer
     ` this is the index of the visible listbox
     index = Check1.Value
     ` keep ListIndex and TopIndex in sync
     List1(index).TopIndex = List1(1 - index).TopIndex
     List1(index).ListIndex = List1(1 - index).ListIndex
   
     ` if the multiselect listbox is going to become visible
     ` clear all selected items in List1(1), except the current one
     If index = 1 Then
          For i = 0 To List1(1).ListCount - 1
               List1(1).Selected(i) = (i = List1(1).ListIndex)
         ext
     End If
   
     ` hide and show the listboxes as needed
     List1(index).Visible = True
     List1(1 - index).Visible = False
End Sub

The TextBox controls' contents are modified by the user. For that reason, you must take additional precautions to ensure that the two controls in the control array always contain the same text (see Listing 12.8). You can use a menu item to switch from a wp-like multiline text box (with ScrollBars set to 1-Vertical, which wraps lines longer than the control's width) to an editor-like text box (which causes longer lines to be wrapped at the control's right margin with ScrollBars=3-Both, which does not wrap long lines), as shown in Figure 12.8:

Listing 12.8 Implementing a TextBox that Can Work Either as a Word Processor (and Support the Wrapping of Long Lines) or as a Programmer's Editor (that Never Wraps Lines)

Private Sub mnuEditItem_Click(Index As Integer)
     Select Case Index
          Case 1   ` Undo
          Case 2   ` ------
          Case 3   ` Cut
          Case 4   ` Copy
          Case 5   ` Paste
          Case 6   ` Delete
          Case 7   ` Select All
          Case 8   ` -------
          Case 9   ` Word Wrap
               Dim index As Integer
               ` this is the index of the visible textbox
               index = Check1.Value
               ` copy the text from the text that is
               ` currently visible to the other one
               Text1(index).Text = Text1(1 - index).Text
               Text1(index).SelStart = Text1(1 - index).SelStart
               Text1(index).SelLength = Text1(1 - index).SelLength
               ` hide and show the textbox as needed
               Text1(index).Visible = True
               Text1(1 - index).Visible = False
     End Select
End Sub

Using Dynamic Control Arrays

Dynamic control arrays are control arrays that grow in size during execution. At design time there is no a real distinction between static and dynamic control arrays because you create them in exactly the same way. If you have a control array, you can dynamically create new items by issuing a Load command, as in:

Load Text1(n)

where n is the index of the element to be created. You should never attempt to create an element that already exists. If you do, a runtime error occurs. You can usually avoid this problem by keeping a form-level counter variable or by resorting to the UBound method of the control array object, as in:

n = Text1.UBound + 1
Load Text1(n)

New controls have the same size and position as the first element of the array but are invisible. You can move them where you need them and change their dimensions and other attributes before making them "magically" appear on the form:

` move the created control in the bottom right corner of the form
Text1(n).Move 0, ScaleHeight - Text1(n).Height
Text1(n).Visible = True

FIG. 12.8
Control arrays let you simulate ListBox and TextBox controls that change their read-only attributes at run time.

Apart from the Index and the Visible properties, controls that are created dynamically have the same attributes as the first control of the array. You should always keep this in mind because you won't be able to change attributes that are read-only at run time. For instance, if the first item of an array of TextBox controls is single-lined (in other words, its Multiline property is set to False), you cannot create a multiline text box out of it. If you need to dynamically create both kinds of TextBox controls, you have to prepare two distinct control arrays. If you also need to create wp-like multiline text boxes (for example, with ScrollBars set to 1-Vertical) and editor-like multiline text boxes (with ScrollBars set to 3-Both), you have to prepare three control arrays because the ScrollBars property can't be changed at run time.

List of Recent Files

You can build dynamic arrays of menu items in the same exact manner that you build static arrays of menu items. Just create one array item with a non-blank Index property using the Menu Editor. All of the dynamically created items are inserted after this one.

You can use this technique in many different situations. It is typically used to build a list of the files that have recently been opened by the application. Many commercial programs offer this list, including Microsoft Word, Microsoft Excel, and Visual Basic itself. Adding this feature to your programs will surely make your users happy. It saves them from having to navigate in their hard disk just to retrieve the files they were working with during the most recent session (or sessions).

To correctly implement a list of recent files, you need to write a number of cooperating procedures. However, this task will probably be more difficult than you anticipate. Therefore, I have prepared a complete program, which is also moderately useful in itself. This simple application is an image viewer built upon a single PictureBox control. The application only has two menus: File and Edit. You can load an image from disk, copy that image to the Clipboard, or paste the contents of the clipboard into the picture box. The simplicity of this application allows you to concentrate on the correct implementation of the list of the most recently opened files.

This list usually appears near the bottom of the File menu (just above the Exit command). Bars separate the list from the other sections of the menu. I created a mnuFileList(0) item that works as a separator. It is followed by a regular menu item, mnuFileBar, which keeps the Exit command apart from all of the preceding commands. If the list is empty, the program should make the mnuFileBar(0) item invisible so that the user doesn't see two consecutive separating bars (see Figure 12.9).

FIG. 12.9
This is the Image Viewer at design time, when all of the necessary menu items have already been prepared.

Use a form-level array of strings to hold the names of the most recently used files. This array is loaded when the application starts (typically in the Form_Load event of the main form) and saved to disk when the execution ends (in the Form_Unload event).

I suggest that the number of the files in the list be less or equal to nine, which helps in assigning a unique hotkey to each entry in the menu. The ReadRecentFiles and WriteRecentFiles routines have similar structures, as shown in Listing 12.9.

Listing 12.9 IMAGEVIEWER.FRM--The Code that Implements a List of Recently Used Files

Option Explicit
` max length of the recent files list (should be <= 9)
Const RECENTFILES_MAX = 9
Dim recentFiles(RECENTFILES_MAX) As String
Private Sub Form_Load()
     ` on entry, load the list of recent files
     ReadRecentFiles
End Sub
Private Sub Form_Unload(Cancel As Integer)
     ` on exit, save the list of recent files to disk
     WriteRecentFiles
End Sub
Private Sub ReadRecentFiles()
     ` read the list of recent files, and update the File menu
     Dim fnum As Integer
     Dim fileIsOpened As Boolean
     Dim Index As Integer
     Dim item As String
     On Error GoTo ReadRecentFiles_Err
     fnum = FreeFile()
     Open RecentFilePath For Input As #fnum
     fileIsOpened = True
     Do Until EOF(fnum)
          Line Input #fnum, item
          ` only store non-null strings
          If item <> "" Then
               Index = Index + 1
               recentFiles(Index) = item
          End If
     Loop
ReadRecentFiles_Err:
     If fileIsOpened Then Close #fnum
     ` build the menu
     UpdateRecentFileMenu
End Sub
Private Sub WriteRecentFiles()
     ` write the list of recent files
     Dim fnum As Integer
     Dim fileIsOpened As Boolean
     Dim Index As Integer
     On Error GoTo WriteRecentFiles_Err
     fnum = FreeFile()
     Open RecentFilePath For Output As #fnum
     fileIsOpened = True
     For Index = 1 To RECENTFILES_MAX
          ` only store non-blank items
          If recentFiles(Index) <> "" Then
               Print #fnum, recentFiles(Index)
          End If
    ext
WriteRecentFiles_Err:
     If fileIsOpened Then Close #fnum
End Sub
Private Function RecentFilePath() As String
     ` return the path of the text file that holds the list
     ` of most recently opened files
     RecentFilePath = App.Path & IIf(Right$(App.Path, 1) <> _
            "\", "\", "") & App.EXEName & ".mru"
End Function
Private Sub UpdateRecentFileMenu()
     ` update the menu with the list of recent files
     Dim Index As Integer
     ` unload any loaded items
     ` except the first one (index=0) that is a static element
     Do While mnuFileList.UBound > 0
          Unload mnuFileList(mnuFileList.UBound)
     Loop
     ` temporarily hide the separator at the
     ` beginning of the list
     mnuFileList(0).Visible = False
     
     ` load filenames into the menu array
     For Index = 1 To RECENTFILES_MAX
          ` take only non-null items into account
          If recentFiles(Index) = "" Then Exit For
          ` load the array item
          Load mnuFileList(Index)
          ` set its caption and hotkey
          mnuFileList(Index).Caption = "&" & Format$(Index) & _
                  ". " + recentFiles(Index)
          ` make it visible
          mnuFileList(Index).Visible = True
          ` if at least one item is visible, also the separator
          ` at the beginning of the list should be visible
          mnuFileList(0).Visible = True
    ext
End Sub
Private Sub AddToRecentFileList(ByVal filename As String)
     ` add a new file to the list of the recently opened files
     Dim found As Integer
     Dim Index As Integer
     Dim ercode As Integer
     ` do nothing if the file is already on top of the list
     If filename <> recentFiles(1) Then
          ` check if the file is already in the list
          ` if not found, use the last item of the list
          found = RECENTFILES_MAX
          For Index = 1 To RECENTFILES_MAX - 1
               If recentFiles(Index) = filename Or _
                  recentFiles(Index) = "" Then
                        found = Index
                        Exit For
               End If
         ext
          ` move all items in the range [1, found] one
          ` position toward higher indexes
          For Index = found To 2 Step -1
               recentFiles(Index) = recentFiles(Index - 1)
         ext
          ` store the file in the first position
          recentFiles(1) = filename
          ` update the menu
          UpdateRecentFileMenu
     End If
End Sub

When working with files, you should always add an error handler because many things could go wrong: another application or user might be reading the file, or the file might have been deleted or corrupted. If an error occurs while reading the individual lines of the file, the error handling routine should close the opened file. If the open command itself has failed, the file does not need to be closed. The two routines ReadRecentFiles and WriteRecentFiles use the fileIsOpened variable to differentiate between the two cases. Note that even if no error occurs, the execution flows into the error handling routine and correctly closes the file.

Both the ReadRecentFiles and the WriteRecentFiles routines call the RecentFilePath function that returns the location of the file that holds the names of recently opened images. In a fully Windows 95-compliant application, you would probably store this information in the system Registry. Because this point is not central to our discussion, I have simply used a text file stored in the same directory as the main application.


NOTE: The code in the RecentFilePath function shows how to avoid a common error that can be made when building file paths. Many programmers, in fact, believe that they could build a complete file path using a simpler statement:

RecentFilePath = App.Path & "\" & App.EXEName & ".mru"

Unfortunately, the previous line of code won't work if the application was installed or copied to the root directory. In that case, the string returned by the App.Path property contains a trailing backslash. Therefore, you end up with two consecutive backslashes, and your file name is not valid.


The UpdateRecentFileMenu routine is where you finally deal with the array of menu items. This routine takes a defensive approach to control arrays, in that it first unloads all the items that were dynamically loaded and then rebuilds the array in its final form. You could probably optimize the code by not unloading the items that you plan to re-load soon afterwards. But this operation executes so quickly that it doesn't really affect the performance of the program.

You also need a routine that keeps the array in order and sorted, with the most recent file on the top and the least recent file on the bottom:

It's up to you to call the AddToRecentFileList routine whenever you open a file. In this sample application, this is done in the OpenFile routine (see Listing 12.10). If you plan to include these routines in full-featured programs, you should also call the AddToRecentFileList routine whenever you save a new file or change the name of a loaded file.

Listing 12.10 IMAGEVIEWER.FRM--The Other Routines in the ImageViewer Program

Private Sub OpenFile(filename As String)
     ` load the picture
     Picture1.Picture = LoadPicture(filename)
     ` update the recent file list
     AddToRecentFileList filename
End Sub
Private Sub mnuFileList_Click(Index As Integer)
    ` load a file from the list of recent files
    OpenFile recentFiles(Index)
End Sub
Private Sub Form_Resize()
     ` resize the picture box along with the form
     Picture1.Move 0, 0, ScaleWidth, ScaleHeight
End Sub
Private Sub mnuFileNew_Click()
     ` clear the picture box
     Set Picture1.Picture = Nothing
End Sub
Private Sub mnuFileOpen_Click(Index As Integer)
     ` query the user for a new picture
     With CommonDialog1
          .Filter = "All Picture Files|*.bmp;*.dib:*.gif;" _
                  & "*.wmf;*.emf;*.jpg;*ico;*.cur|" _
               & "Bitmaps (*.bmp;*.dib)|*.bmp;*.dib|" _
               & "Icons (*.ico;*.cur)|*.ico;*.cur|" _
               & "GIF images (*.gif)|*.gif|" _
               & "JPEG images (*.jpg)|*.jpg|" _
               & "Metafiles (*.wmf;*.emf)|*.wmf;*.emf" _
               & "All Files|*.*"
          .Flags = cdlOFNFileMustExist + cdlOFNHideReadOnly
          .filename = ""
          .ShowOpen
          If .filename <> "" Then
               ` if the user didn't cancel the command, open the image
               OpenFile .filename
          End If
     End With
End Sub
Private Sub mnuFileExit_Click(Index As Integer)
     ` exit the program
     Unload Me
End Sub
Private Sub mnuEditCopy_Click()
     ` copy the current image to the clipboard
     Clipboard.SetData Picture1.Picture
End Sub
Private Sub mnuEditPaste_Click()
     ` paste the image currently in the clipboard
     Set Picture1.Picture = Clipboard.GetData
End Sub

If the end user selects one menu item from the control array, the application must load the appropriate file. This is a trivial operation because the control array and the recentFile() string array are always in sync.

The rest of the code for the ImageViewer sample application is mainly for file and clipboard commands (see Figure 12.10). You should not need any further commentary to understand how each routine works.

FIG. 12.10
Here is the ImageViewer application at run time.

Create a Professional Grid for Data Entry

In this last example, I show a complete, non-trivial application of the control array concept: a customized form for entering all the data related to an invoice. However complex this code is, always keep in mind that it is just an example. Many pieces are missing--a tax evaluation, just to name one. But my goal is to illustrate how control arrays can be used in a business application, not how to build a commercial invoicing software.

All the programs of this type must allow the user to enter both customer data (name, address, and so on) and details on each item in the invoice. Data that fall into the latter category is usually presented in the form of a table or grid. Because Visual Basic 5.0 comes with two ActiveX grids--DBGrid and FlexGrid--you might wonder why you should use a different approach.

The problem with the grids provided in the VB package is that they are not powerful and flexible enough for many practical applications. The FlexGrid ActiveX control, for example, does not permit you to edit individual cells; therefore, it is not very useful for data entry forms. The DBGrid control is better, but it supports only textual cells and cannot be customized with other types of controls, such as combo boxes, check boxes, or images. If you want to write cool apps, you need to buy enhanced grids from third-party vendors or build your own. In the rest of this chapter, I will show you that building your own grids is probably less difficult than you think.

Let's have a look at Figure 12.11, which shows the running Invoices application.

FIG. 12.11
The Invoices application in action.

The Advantages of a Homemade Grid There are many interesting details of the grid used by this simple program:

The Grid at Design Time If you are wondering where the control arrays are, look at Figure 12.12, which shows the same form at design time.

FIG. 12.12
The Invoices app at design time. There are exactly six control arrays for the grid, plus one for all the fields in the invoice header.

There are exactly nine control arrays in this form:

Array What It Contains
lblColumn The six labels used as column headers.
Label1 Holds all the other descriptive labels on the form.
txtHeader Holds all the textbox controls used for the header of the invoice (invoice number and date, customer name, and so on).
txtQty, cboProductID, The six control arrays, one for each column of the grid.
txtDescription, txtUnitPrice, lblTotal, and chkBackorder These are the only dynamic arrays in the program.

Initializing the Data in the Grid It's time to study the complete listing of the Invoices application. Information on categories of products is initialized in the Form_Load event and stored in a regular array of user-defined types (see Listing 12.11). This array is used to fill the Description and Unit Price fields when the end user selects a Product ID. In a real-world application, this data would probably be loaded from a file or a database table.

Listing 12.11 INVOICES.FRM--Form-Level Variables and the Form_Load event Procedure of the Invoices Program

Option Explicit
Private Type TProductInfo
     ID As String
     Description As String
     UnitPrice As Currency
End Type
` max number of detail lines in the invoice
Const LINES_MAX = 8
` this array holds information on each product
Const PRODUCT_NUM = 10
Dim ProductInfo() As TProductInfo
` this variable tracks which line the cursor is currently on
` zero means that it is on the upper portion of the form
Dim currentLine As Integer
Private Sub Form_Load()
     ` load ID, description and price unit for a bunch of products
     ` (in a real application this information would be loaded
     `  from a file or a database table)
     Dim i As Integer
     ReDim ProductInfo(1 To PRODUCT_NUM) As TProductInfo
     ProductInfo(1).ID = "Mouse/S"
     ProductInfo(1).Description = "Serial Mouse"
     ProductInfo(1).UnitPrice = 39.5
     ProductInfo(2).ID = "Mouse/PS2"
     ProductInfo(2).Description = "Mouse with PS/2 connector"
     ProductInfo(2).UnitPrice = 49.99
     ProductInfo(3).ID = "Modem /I"
     ProductInfo(3).Description = "Internal 28.800 baud modem"
     ProductInfo(3).UnitPrice = 105
     ProductInfo(4).ID = "Modem/E"
     ProductInfo(4).Description = "Internal 28.800 baud modem"
     ProductInfo(4).UnitPrice = 105
     ProductInfo(5).ID = "Video/V"
     ProductInfo(5).Description = "VGA Video Card"
     ProductInfo(5).UnitPrice = 45
     ProductInfo(6).ID = "Video/SV"
     ProductInfo(6).Description = "Super-VGA Video Card"
     ProductInfo(6).UnitPrice = 75
     ProductInfo(7).ID = "CdRom/6"
     ProductInfo(7).Description = "CDROM Driver 6x"
     ProductInfo(7).UnitPrice = 109
     ProductInfo(8).ID = "CdRom/1 2"
     ProductInfo(8).Description = "CDROM Driver 12x"
     ProductInfo(8).UnitPrice = 225
     ProductInfo(9).ID = "Cable/P"
     ProductInfo(9).Description = "Parallel Cable"
     ProductInfo(9).UnitPrice = 7.99
     ProductInfo(10).ID = "Cable/S"
     ProductInfo(10).Description = "Serial Cable"
     ProductInfo(10).UnitPrice = 7.99
     ` load product IDs into the combo box
     For i = 1 To PRODUCT_NUM
          cboProductID(1).AddItem ProductInfo(i).ID
    Next
End Sub

Highlighting the Current Line The n e xt bunch of routines keeps track of the current line in the grid--that is, where the input caret currently is. This value is stored in the currentLine variable, which is set to zero when the input focus is outside of the grid (see Listing 12.12). It is very easy to update this variable because all the fields are gathered in control arrays:

Listing 12.12 INVOICES.FRM--The Routines that Manage the Appearance of the Grid When the Input Focus Moves to Another Line of Controls

Private Sub txtHeader_GotFocus(Index As Integer)
    ewCurrentLine 0
End Sub
Private Sub txtQty_GotFocus(Index As Integer)
    ewCurrentLine Index
End Sub
Private Sub cboProductID_GotFocus(Index As Integer)
    ewCurrentLine Index
End Sub
Private Sub txtDescription_GotFocus(Index As Integer)
    ewCurrentLine Index
End Sub
Private Sub txtUnitPrice_GotFocus(Index As Integer)
    ewCurrentLine Index
End Sub
Private Sub chkBackorder_GotFocus(Index As Integer)
    ewCurrentLine Index
End Sub
Private Sub NewCurrentLine(newLine As Integer)
     ` set a yellow background for the controls on the
     ` current line, and white for all the others
     Dim Index As Integer
     Dim foColor As Long, bkColor As Long
     currentLine = newLine
     For Index = txtQty.LBound To txtQty.UBound
          If Index = currentLine Then
               foColor = vbHighlightText
               bkColor = vbHighlight
          Else
               foColor = vbWindowText
               bkColor = vbWindowBackground
          End If
          txtQty(Index).ForeColor = foColor
          txtQty(Index).BackColor = bkColor
          cboProductID(Index).ForeColor = foColor
          cboProductID(Index).BackColor = bkColor
          txtDescription(Index).ForeColor = foColor
          txtDescription(Index).BackColor = bkColor
          txtUnitPrice(Index).ForeColor = foColor
          txtUnitPrice(Index).BackColor = bkColor
          lblTotal(Index).ForeColor = foColor
          lblTotal(Index).BackColor = bkColor
          ` don't touch checkbox's colors
    Next
End Sub

The NewCurrentLine procedure, as shown in Listing 12.12, does more than simply update the currentLine variable. It quickly scans all of the cells in the grid and sets their foreground and background colors so that they highlight the current line.


NOTE: The preceding code uses VB color constants instead of numerical values. vbHighlight is usually blue, and vbHighlightText is usually white. The default value for vbWindowsBackground is white, and the default value for vbWindowsText is black.

The advantage of this approach is that it delivers coherent results even if the user changes the system colo r scheme, and the grid continues to be perceived as well integrated in the user interface. You should always use system colors when possible.


Adding and Removing Grid Lines The core routine of this sample program is the cmdAddItem_Click event procedure that adds a new line of invoice details (see Listing 12.13).

Listing 12.13 INVOICES.FRM--The Routine that Adds a New Line of Controls to the Grid

Private Sub cmdAddItem_Click()
     ` add a new line for Invoice details
     Dim newLine As Integer
     Dim lineTop As Single
     Dim i As Integer
    ewLine = txtQty.UBound + 1
     ` exit if too many lines
     If newLine > LINES_MAX Then Exit Sub
     ` load all the controls that make up the row
     ` it is preferable to load all controls *before* acting
     ` on their properties, because otherwise a change event might
     ` rise an error since it would refer to a non existing control
     Load txtQty(newLine)
     Load cboProductID(newLine)
     Load txtDescription(newLine)
     Load txtUnitPrice(newLine)
     Load lblTotal(newLine)
     Load chkBackorder(newLine)
     ` then move controls in the correct position, make
     ` them visible and clear them
     lineTop = txtQty(newLine - 1).top + txtQty(newLine - 1).Height
     ` we don't need to modify the Left property, whose
     ` value is inherited by the control in the above line
     txtQty(newLine).top = lineTop
     txtQty(newLine).Visible = True
     txtQty(newLine).text = ""
     cboProductID(newLine).top = lineTop
     cboProductID(newLine).Visible = True
     cboProductID(newLine).text = ""
     txtDescription(newLine).top = lineTop
     txtDescription(newLine).Visible = True
     txtDescription(newLine).text = ""
     txtUnitPrice(newLine).top = lineTop
     txtUnitPrice(newLine).Visible = True
     txtUnitPrice(newLine).text = ""
     lblTotal(ne wLine).top = lineTop
     lblTotal(newLine).Visible = True
     lblTotal(newLine).Caption = ""
     chkBackorder(newLine).top = lineTop
     chkBackorder(newLine).Visible = True
     chkBackorder(newLine).Value = 0
     ` load product IDs into the combo box
     cboProductID(newLine).Clear
     For i = 1 To PRODUCT_NUM
          cboProductID(newLine).AddItem ProductInfo(i).ID
    ext
     ` set input focus to the Qty textbox
     DoEvents
     txtQty(newLine).SetFocus
End Sub

It is worth noting t h at the routine loads all the control arrays' items before acting on their properties. This order is necessary because as soon as you touch the Text property of the txtQty or txtUnitPrice controls, that control's Change event tries to update the contents of the lblTotal field on the same line. Of course, if the lblTotal control hasn't been created yet, a runtime error occurs.

Deleting the current line is a bit tricky because you have to move the contents of all the subsequent lines one position upward first. Only then you can use a set of Unload commands to safely delete the last line of controls, as shown in Listing 12.14. Note that you cannot delete the first line of the grid: it cannot be unloaded because it contains controls that were created at design time:

Listing 12.14 INVOICES.FRM--The Routine that Deletes One Row of Controls in the Grid

Private Sub cmdDeleteItem_Click()
     ` delete the current line
     Dim Index As Integer
     Dim lastLine As Integer
     lastLine = txtQty.UBound
     ` exit if the cursor is not on an invoice item or if there
     ` is only one line (these controls are created at design time
     ` and cannot be unloaded)
     If currentLine = 0 Or lastLine = 1 Then Exit Sub
     ` move all values up one row
     For Index = currentLine To lastLine - 1
          txtQty(Index).text = txtQty(Index + 1).text
          cboProductID(Index).text = cboProductID(Index + 1).text
          txtDescription(Index).text = txtDescription(Index + 1).text
          txtUnitPrice(Index).text = txtUnitPrice(Index + 1).text
          lblTotal(Index).Caption = lblTotal(Index + 1).Caption
          chkBackorder(Index).Value = chkBackorder(Index + 1).Value
    Next
     ` clear the lblTotal value for the last line
     `  (this forces the evaluation of grand total)
     lblTotal(lastLine).Caption = ""
     ` if we are about to delete the control that has the
     ` input focus, move the focus elsewhere
     If currentLine = lastLine Then
          txtQty(lastLine - 1).SetFocus
     End If
     ` unload the last line of controls
     Unload txtQty(lastLine)
     Unload cboProductID(lastLine)
     Unload txtDescription(lastLine)
     Unload txtUnitPrice(lastLine)
     Unload lblTotal(lastLine)
     Unload chkBackorder(lastLine)
End Sub

Customizing the Grid's Behavior The event routines that customize the behavior of your grid are next. As soon as the user selects a Product ID, the Description and Unit Price fields are filled with corresponding data, as illustrated in Listing 12.15.

Listing 12.15 INVOICES.FRM--The Routine that Fires when the User Selects a Product ID

Private Sub cboProductID_Click(Index As Integer)
     ` the user has selected a product
     Dim i As Integer
     i = cboProductID(Index).ListIndex
     If i >= 0 Then
          txtDescription(Index).text = _
                  ProductInfo(i + 1).Description
          txtUnitPrice(Index).text = _
                  Format$(ProductInfo(i + 1).UnitPrice, "###.00")
     End If
End Sub

Similarly, when the user modifies the value in the Q.ty or Unit Price fields, the Total field is immediately updated to reflect the total value for that line. The code that implements this behavior is shown in Listing 12.16.

Listing 12.16 INVOICES.FRM--The Code that Evaluates the Total for the Current Line in the Invoice and the Grand Total in the Right Corner of the Form

Private Sub txtQty_Change(Index As Integer)
     UpdateLineTotal Index
End Sub
Private Sub txtUnitPrice_Change(Index As Integer)
     UpdateLineTotal Index
End Sub
Private Sub UpdateLineTotal(Index As Integer)
     ` update the total value of current line
     If txtQty(Index).text <> "" And txtUnitPrice(Index).text <> "" Then
          lblTotal(Index).Caption = Format$(CCur(txtQty(Index).text) * _
          CCur(txtUnitPrice(Index).text), "###,###.00")
     Else
          lblTotal(Index).Caption = ""
     End If
End Sub
Private Sub lblTotal_Change(Index As Integer)
     ` update the grand total
     Dim i As Integer, result As Currency
     For i = lblTotal.LBound To lblTotal.UBound
          If lblTotal(i).Caption <> "" Then
               result = result + CCur(lblTotal(i).Caption)
          End If
    ext
     lblGrandTotal.Caption = Format$(result, "###,###.00")
End Sub

Whenever one of the Total fields is modified, its Change event updates the Grand Total field in the bottom-right corner of the form.

Listing 12.17 shows the routine that is executed when the user clicks the column headers:

Listing 12.17 INVOICES.FRM--This Routine Asks for a Label for the Column Header when the User Clicks the lblColumn Label Controls

Private Sub lblColumn_Click(Index As Integer)
     ` change the header of this column
     Dim newCaption As String
    ewCaption = InputBox$ _
            ("Enter a new label for this column", "My Grid", _
            lblColumn(Index).Caption)
     If newCaption <> "" Then
          lblColumn(Index).Caption = newCaption
     End If
End Sub

Validating the Data Entered by the User The last group of routines implements a limited form of data validation on the many fields on the form, both within and outside the grid. Again, this is a rather simple task, thanks to control arrays (see Listing 12.18).

Listing 12.18 INVOICES.FRM--This Code Rejects Invalid Keys in a Few Fields in the Invoice Header

Private Sub txtHeader_KeyPress(Index As Integer, KeyAscii As Integer)
     ` ensure that numeric fields only get numeric keys
     If KeyAscii < 32 Then Exit Sub
     Select Case Index
          Case 0, 5   ` Invoice number & ZIP code
               If KeyAscii < 48 Or KeyAscii > 57 Then 
                        KeyAscii = 0
                  End If
          Case 1       ` invoice date
               If (KeyAscii < 48 Or KeyAscii > 57) Then
                    If KeyAscii <> Asc("/") Then KeyAscii = 0
               End If
     End Select
     ` protest loudly if necessary
     If KeyAscii = 0 Then Beep
End Sub
Private Sub txtQty_KeyPress(Index As Integer, KeyAscii As Integer)
     ` ignore non-numeric input
     If (KeyAscii < 48 Or KeyAscii > 57) And KeyAscii >= 32 Then
          KeyAscii = 0
          Beep
     End If
End Sub
Private Sub txtUnitPrice_KeyPress(Index As Integer, KeyAscii As Integer)
     ` ignore non-numeric input, but accept decimal separator
     If (KeyAscii < 48 Or KeyAscii > 57) And KeyAscii >= 32 _
            And KeyAscii <> Asc(".") Then
                  KeyAscii = 0
                  Beep
     End If
End Sub

Note that you need more robust routines for data validation in a commercial-quality program. For instance, you should check that the date of the invoice is valid, that the State name is correct, and so on. Now that you have the necessary tools, you can create routines that are more robust than the ones used in this example.

From Here...

This chapter has shown you what control arrays are and you have seen a few possible control array applications. However, as you continue to read this book, you will surely find other ways to put them to good use. Here's a short list of chapters you might refer to for inspiration:


Previous chapterNext chapterContents


Macmillan Computer Publishing USA

© Copyright, Macmillan Computer Publishing. All rights reserved.