Visual Basic
Visual Basic
Visual Basic
When you run the Visual Basic Setup program, it allows you to place the program items in an existing program group or create a new program group and new program items for Visual Basic in Windows. You are then ready to start Visual Basic from Windows. To start Visual Basic from Windows
Figure 1.1The Visual Basic integrated development environment
Integrated Development Environment Elements
Menu Bar
Displays the commands you use to work with Visual Basic. Besides the standard File, Edit, View, Window, and Help menus, menus are provided to access functions specific to programming such as Project, Format, or Debug. Context Menus
Contain shortcuts to frequently performed actions. To open a context menu, click the right mouse button on the object you're using. The specific list of shortcuts available from context menus depends on the part of the environment where you click the right mouse button. For example, the context menu displayed when you right click on the Toolbox lets you display the Components dialog box, hide the Toolbox, dock or undock the Toolbox, or add a custom tab to the Toolbox. Toolbars
Provide quick access to commonly used commands in the programming environment. You click a button on the toolbar once to carry out the action represented by that button. By default, the Standard toolbar is displayed when you start Visual Basic. Additional toolbars for editing, form design, and debugging can be toggled on or off from the Toolbars command on the View menu. Toolbars can be docked beneath the menu bar or can "float" if you select the vertical bar on the left edge and drag it away from the menu bar.
Toolbox
Provides a set of tools that you use at design time to place controls on a form. In addition to the default toolbox layout, you can create your own custom layouts by selecting Add Tab from the context menu and adding controls to the resulting tab. For More Information To learn more about specific controls, see "Forms, Controls, and Menus" and "Using Visual Basic's Standard Controls." For information on how to add controls to the Toolbox, see "Adding Controls to a Project" in "Managing Projects."
Project Explorer Window
Lists the forms and modules in your current project. A project is the collection of files you use to build an application.
For More Information For information on projects, see "Managing Projects."
Properties Window
Lists the property settings for the selected form or control. A property is a characteristic of an object, such as size, caption, or color.
For More Information For information on properties, see "Understanding Properties, Methods, and Events" in "Forms, Controls, and Menus."
Object Browser
Lists objects available for use in your project and gives you a quick way to navigate through your code. You can use the Object Browser to explore objects in Visual Basic and other applications, see what methods and properties are available for those objects, and paste code procedures into your application.
For More Information For more information on using the Object Browser to view procedures, see "Finding Out About Objects" in "Programming with Objects." For details on using add-ins to extend the Visual Basic programming environment, see "Using Wizards and Add-ins" in "Managing Projects."
Form Designer
Serves as a window that you customize to design the interface of your application. You add controls, graphics, and pictures to a form to create the look you want. Each form in your application has its own form designer window.
For More Information To learn how to add controls to an application, see "Your First Visual Basic Application" later in this chapter. To learn more about designing an interface, see "Creating a User Interface."
Code Editor Window
Serves as an editor for entering application code. A separate code editor window is created for each form or code module in your application.
For More Information To learn more about entering code and using the code editor, see "Programming Fundamentals."
Form Layout Window
The Form Layout window (Figure 2.2) allows you to position the forms in your application using a small graphical representation of the screen
Figure 1.2 Form Layot window
Immediate, Locals, and Watch Windows
These additional windows are provided for use in debugging your application. They are only available when you are running your application within the IDE.
SDI or MDI Interface
Two different styles are available for the Visual Basic IDE: single document interface (SDI) or multiple document interface (MDI). With the SDI option, all of the IDE windows are free to be moved anywhere on screen; as long as Visual Basic is the current application, they will remain on top of any other applications. With the MDI option, all of the IDE windows are contained within a single resizable parent window.
To switch between SDI and MDI modes
Run Visual Basic from the command line with a /sdi or /mdi parameter.
Docking Windows
Many of the windows in the IDE can be docked, or connected, to each other or to the edge of the screen. These include the Toolbox, Form Layout Window, Project Explorer, Properties window, Color Palette, and Immediate, Locals, and Watch windows.
With the MDI option, windows can be docked to any side of the parent window; with SDI they can only be docked beneath the menu bar. Docking capabilities can be toggled on or off for a given window by selecting the appropriate check box on the Docking tab of the Options dialog box, available from the Options command on the Tools menu.
To dock or undock a window
There are three main steps to creating an application in Visual Basic:
Forms are the foundation for creating the interface of an application. You can use forms to add windows and dialog boxes to your application. You can also use them as containers for items that are not a visible part of the application's interface. For example, you might have a form in your application that serves as a container for graphics that you plan to display in other forms.
The first step in building a Visual Basic application is to create the forms that will be the basis for your application's interface. Then you draw the objects that make up the interface on the forms you create. For this first application, you'll use two controls from the Toolbox.
Button
Control Text Box Button To draw a control using the Toolbox
Setting Properties The next step is to set properties for the objects you've created. The Properties window (Figure 1.4) provides an easy way to set properties for all objects on a form. To open the Properties window, choose the Properties Window command from the View menu, click the Properties Window button on the toolbar, or use the context menu for the control.
Figure 1.4 The Properties window
The Properties window consists of the following elements:
Object box — Displays the name of the object for which you can set properties. Click the arrow to the right of the object box to display the list of objects for the current form.
Sort tabs — Choose between an alphabetic listing of properties or a hierarchical view divided by logical categories, such as those dealing with appearance, fonts, or position.
Properties list — The left column displays all of the properties for the selected object. You can edit and view settings in the right column.
To set properties from the Properties window
Object Property Setting Form Caption Hello, world! Text box Text (Empty) Command button Caption OK
Setting the Icon Property
All forms in Visual Basic have a generic, default icon that appears when you minimize that form. However, you will probably change this icon to one that illustrates the use of the form or your application. To assign an icon to a form, set the Icon property for that form. You can use 32 x 32 pixel icons that were standard in 16-bit versions of Microsoft Windows and are also used in Windows 95/98 and Windows NT, as well as the 16 x 16 pixel icons used in Windows 95/98.
Writing Code
The Code Editor window is where you write Visual Basic code for your application. Code consists of language statements, constants, and declarations. Using the Code Editor window, you can quickly view and edit any of the code in your application.
To open the Code window
You can choose to display all procedures in the same Code window, or display a single procedure at a time.
To display all procedures in the same Code window
Creating a Project
You begin creating this application by choosing New Project from the File menu, then selecting Standard EXE in the New Project dialog box (when you first start Visual Basic, the New Project dialog box is presented). Visual Basic creates a new project and displays a new form. To draw the interface, you use a data control, a MSFlexGrid control, a list box control, and two command buttons. The MSFlexGrid control isn't in the default toolbox, so you'll need to add it:
To add a control to the toolbox
Setting Properties
In the Properties window, set properties for the objects according to the following table. Use the default settings for all other properties.
Object Property Setting Form Caption Products Data1 DatabaseName
RecordSource path \Nwind.mdb
Products MSFlexGrid1 DataSource Data1 Command1 Caption Clear Command2 Caption Exit The DatabaseName property of the data control must include the actual path to the database. By default, the Nwind.mdb database is installed in the same directory as Visual Basic. When you select the DatabaseName property in the Properties window, you can click the button to the right of the property to display a standard File Open dialog box to browse for the file. Once the DatabaseName property has been set, the RecordSource property in the Properties window will contain a list of tables or recordsets for the selected database. Setting the DataSource property of the MSFlexGrid control to Data1 automatically links the grid to the data control.
Writing Event Code
All the code for the application is contained in the Command1_Click, Command2_Click, Data1_Reposition, and MSFlexGrid1_DblClick event procedures. Double-click the form or control to display the Code window, and then type the code for each event procedure.
Add this code to the Command1_Click event procedure to clear the list box when the user clicks the button:
Private Sub Command1_Click ()
List1.Clear ' Clears the list box.
End Sub
In the above statement, you are invoking the Clear method of the list box, List1. The Clear method deletes the contents of the list box.
Add this code to the Command2_Click event procedure to unload the form from memory and end the application:
Private Sub Command2_Click ()
Unload Form1
End ' Ends application.
End Sub
In the above procedure, the first statement invokes the Unload event for the form. If you needed to perform a function at shutdown, such as saving a file, you could place that code in the form's Unload event procedure. The second statement calls the End function, which ends the application.
Add this code to the Data1_Reposition event procedure to update the caption each time a record is selected:
Private Sub Data1_Reposition ()
Data1.Caption = Data1.Recordset("ProductName")
End Sub
In the above statement, you are assigning the value on the right (the contents of the Title field in the Recordset of the data control) to the property on the left (the Caption property of the data control object).
Add this code to the MSFlexGrid_DblClick event procedure to add an item to the list box when the user double-clicks a selected row:
Private Sub MSFlexGrid1_DblClick ()
List1.AddItem MSFlexGrid1.Text
End Sub
In the above statement, you are invoking the AddItem method of the list box (List1). The text to be added to the list box is contained in the argument of the method, in this case, the value of the title field in the recordset of the data control. Passing a value to an argument is similar to assigning a value to a property; unlike the assignment statement, the equal sign isn't required.
Saving a Project You finish your work on the application by choosing Save Project from the File menu. Visual Basic will prompt you separately to save the form and then the project. One possible name for the project is "Northwind Shopping List." Windows 95, Windows 98, and Windows NT allow you to use file names up to 255 characters in length, and file names can include spaces. Older versions of Microsoft Windows limited you to file names of eight characters, with a three-character extension.
Enhancing Your Application You have just completed your first Visual Basic application: one that performs a simple but useful function. You can use this application as a basis for adding similar functionality in your own applications, substituting your own data instead of Nwind.mdb. Of course, to make this application truly useful, you might want to add functionality to save or print the contents of the list box, to add additional information such as price and availability, and even to gather credit card information and transmit an order across the Internet. As you continue on through the rest of the Programmer's Guide, you will find examples of doing all that and a lot more.
Understanding Properties, Methods and Events
Visual Basic forms and controls are objects which expose their own properties, methods and events. Properties can be thought of as an object's attributes, methods as its actions, and events as its responses.
An everyday object like a child's helium balloon also has properties, methods and events. A balloon's properties include visible attributes such as its height, diameter and color. Other properties describe its state (inflated or not inflated), or attributes that aren't visible such as its age. By definition, all balloons have these properties; the settings of these properties may differ from one balloon to another.
A balloon also has inherent methods or actions that it might perform. It has an inflate method (the action of filling it with helium), a deflate method (expelling its contents) and a rise method (if you were to let go of it). Again, all balloons are capable of these methods.
Balloons also have predefined responses to certain external events. For instance, a balloon would respond to the event of being punctured by deflating itself, or to the event of being released by rising into the air.
If you were able to program a balloon, the Visual Basic code might look like the following. To set the balloon's properties:
Balloon.Color = Red
Balloon.Diameter = 10
Balloon.Inflated = True
Note the syntax of the code — the object (Balloon) followed by the property (.Color) followed by the assignment of the value (Red). You could change the color of the balloon from code by repeating this statement and substituting a different value. Properties can also be set in the Properties window while you are designing your application.
A balloon's methods are invoked like this:
Balloon.Inflate
Balloon.Deflate
Balloon.Rise 5
The syntax is similar to the property — the object (a noun) followed by the method (a verb). In the third example, there is an additional item, called an argument, which denotes the distance to rise. Some methods will have one or more arguments to further describe the action to be performed.
The balloon might respond to an event as follows:
Sub Balloon_Puncture()
Balloon.Deflate
Balloon.MakeNoise "Bang"
Balloon.Inflated = False
Balloon.Diameter = 1
End Sub
In this case, the code describes the balloon's behavior when a puncture event occurs: invoke the Deflate method, then invoke the MakeNoise method with an argument of "Bang" (the type of noise to make). Since the balloon is no longer inflated, the Inflated property is set to False and the Diameter property is set to a new value.
While you can't actually program a balloon, you can program a Visual Basic form or control. As the programmer, you are in control. You decide which properties should be changed, methods invoked or events responded to in order to achieve the desired appearance and behavior.
Designing a Form
Form objects are the basic building blocks of a Visual Basic application, the actual windows with which a user interacts when they run the application. Forms have their own properties, events, and methods with which you can control their appearance and behavior.
Figure 1.6 Forms and controls have their own properties, events, and methods
The first step in designing a form is to set its properties. You can set a form's properties at design time in the Properties window, or at run time by writing code.
The Properties window
Form Events and Methods
As objects, forms can perform methods and respond to events. The Resize event of a form is triggered whenever a form is resized, either by user interaction or through code. This allows you to perform actions such as moving or resizing controls on a form when its dimensions have changed.
The Activate event occurs whenever a form becomes the active form; the Deactivate event occurs when another form or application becomes active. These events are convenient for initializing or finalizing the form's behavior. For example, in the Activate event you might write code to highlight the text in a particular text box; in the Deactivate event you might save changes to a file or database.
To make a form visible, you would invoke the Show method:
Form2.Show
Invoking the Show method has the same effect as setting a form's Visible property to True.
Many of a form's methods involve text or graphics. The Print, Line, Circle, and Refresh methods are useful for printing or drawing directly onto a form's surface. These methods and more are discussed in "Working with Text and Graphics."
Using Command Buttons
Most Visual Basic applications have command buttons that allow the user to simply click them to perform actions. When the user chooses the button, it not only carries out the appropriate action, it also looks as if it's being pushed in and released. Whenever the user clicks a button, the Click event procedure is invoked. You place code in the Click event procedure to perform any action you choose.
There are many ways to choose a command button at run time:
You use the Caption property to display text on the button to tell the user what the button does. In Figure 3.4, the Test Buttons example from the Controls sample application contains a command button with its Caption property set to "Change Signal." (For a working version of this example, see Button.frm in the Controls.vbp sample application.)
Notice that 'S' is the access key for this button, denoted by an underline. Inserting an ampersand (&) in the text of the Caption property makes the character following it the access key for that button (for example, Change &Signal).
When a user clicks the command button, the code in the command button's Click event procedure is executed. In the example, a different traffic light icon is displayed each time the button is clicked. Using Labels to Display Text
A label control displays text that the user cannot directly change. You can use labels to identify controls, such as text boxes and scroll bars, that do not have their own Caption property. The actual text displayed in a label is controlled by the Caption property, which can be set at design time in the Properties window or at run time by assigning it in code. By default, the caption is the only visible part of the label control. However, if you set the BorderStyle property to 1 (which you can do at design time), the label appears with a border — giving it a look similar to a text box. You can also change the appearance of the label by setting the BackColor, BackStyle, ForeColor, and Font properties.
The WordWrap property causes the label to grow vertically to fit its contents, while retaining the same width, as shown in Figure 3.6. For a working version of this example, see Wordwrap.frm in the Controls.vbp sample application.
WordWrap example
Working with Text in a Text Box
You can control the insertion point and selection behavior in a text box with the SelStart, SelLength and SelText properties. These properties are only available at run time.
When a text box first receives the focus, the default insertion point or cursor position within the text box is to the left of any existing text. It can be moved by the user from the keyboard or with the mouse. If the text box loses and then regains the focus, the insertion point will be wherever the user last placed it.
In some cases, this behavior can be disconcerting to the user. In a word processing application, the user might expect new characters to appear after any existing text. In a data entry application, the user might expect their typing to replace any existing entry. The SelStart and SelLength properties allow you to modify the behavior to suit your purpose
The SelLength property is a numeric value that sets the width of the insertion point. Setting the SelLength to a number greater than 0 causes that number of characters to be selected and highlighted, starting from the current insertion point. Figure 3.8 shows the selection behavior.
If the user starts typing while a block of text is selected, the selected text will be replaced. In some cases, you might want to replace a text selection with new text by using a paste command. The SelText property is a string of text that you can assign at run time to replace the current selection. If no text is selected, SelText will insert its text at the current insertion point. Option with check Box
A check box indicates whether a particular condition is on or off. You use check boxes in an application to give users true/false or yes/no options. Because check boxes work independently of each other, a user can select any number of check boxes at the same time.
Check box example
Events in the Check Box Application
The Click event for the check box occurs as soon as you click the box. This event procedure tests to see whether the check box has been selected (that is, if its Value = vbChecked). If so, the text is converted to bold or italic by setting the Bold or Italic properties of the Font object returned by the Font property of the text box.
Private Sub chkBold_Click ()
If ChkBold.Value = vbChecked Then ' If checked.
txtDisplay.Font.Bold = True
Else ' If not checked.
txtDisplay.Font.Bold = False
End If
End Sub
Private Sub chkItalic_Click ()
If ChkItalic.Value = vbChecked Then ' If checked.
txtDisplay.Font.Italic = True
Else ' If not checked.
txtDisplay.Font.Italic = False
End If
End Sub
Grouping Options with Option Buttons
Option buttons present a set of two or more choices to the user. Unlike check boxes, however, option buttons should always work as part of a group; selecting one option button immediately clears all the other buttons in the group. Defining an option button group tells the user, "Here is a set of choices from which you can choose one and only one."
For example Selecting an option button
Each time a new option button is selected, the code in its Click event updates the appropriate variable:
Private Sub opt586_Click()
strComputer = "Pentium"
Call DisplayCaption
End Sub
It then calls a sub procedure, called DisplayCaption, that concatenates the two variables and updates the label's Caption property:
Sub DisplayCaption()
lblDisplay.Caption = "You selected a " & _
strComputer & " running " & strSystem
End Sub
A sub procedure is used because the procedure of updating the Caption property is essentially the same for all five option buttons, only the value of the variables change from one instance to the next. This saves you from having to repeat the same code in each of the Click events.
Using List Boxes and Combo Boxes
List boxes and combo boxes present a list of choices to the user. By default, the choices are displayed vertically in a single column, although you can set up multiple columns as well. If the number of items exceeds what can be displayed in the combo box or list box, scroll bars automatically appear on the control. The user can then scroll up and down or left to right through the list.
Single-column list box
A combo box control combines the features of a text box and a list box. This control allows the user to select either by typing text into the combo box or by selecting an item from its list.
Combo box
In contrast to some other controls that contain a single value; for example the label's Caption property or the text box's Text property, list boxes and combo boxes contain multiple values or a collection of values. They have built-in methods for adding, removing and retrieving values from their collections at run time. To add several items to a list box named List1, the code would look like this:
List1.AddItem "Paris"
List1.AddItem "New York"
List1.AddItem "San Francisco"
List boxes and combo boxes are an effective way to present a large number of choices to the user in a limited amount of space.
Using Scroll Bars as Input Devices
Although scroll bars are often tied to text boxes or windows, you'll sometimes see them used as input devices. Because these controls can indicate the current position on a scale, scroll bar controls can be used individually to control program input — for example, to control the sound volume or to adjust the colors in a picture. The HScrollBar (horizontal) and VScrollBar (vertical) controls operate independently from other controls and have their own set of events, properties, and methods. Scroll bar controls are not the same as the built-in scroll bars that are attached to text boxes, list boxes, combo boxes, or MDI forms (text boxes and MDI forms have a ScrollBars property to add or remove scroll bars that are attached to the control).
Windows interface guidelines now suggest using slider controls as input devices instead of scroll bars. Examples of slider controls can be seen in the Windows 95/98 control panel. A slider control of this type is included in the Professional and Enterprise editions of Visual Basic.
Working With the Picture Box Control
The primary use for the picture box control is to display a picture to the user. The actual picture that is displayed is determined by the Picture property. The Picture property contains the file name (and optional path) for the picture file that you wish to display.
To display or replace a picture at run time, you can use the LoadPicture function to set the Picture property. You supply the name (and optional path) for the picture and the LoadPicture function handles the details of loading and displaying it: picMain.Picture = LoadPicture("VANGOGH.gif")
The picture box control has an AutoSize property that, when set to True, causes the picture box to resize automatically to match the dimensions of its contents. Take extra care in designing your form if you plan on using a picture box with the AutoSize enabled. The picture will resize without regard to other controls on the form, possibly causing unexpected results, such as covering up other controls. It's a good idea to test this by loading each of the pictures at design time.
Using the Picture Box as a Container
The picture box control can also be used as a container for other controls. Like the frame control, you can draw other controls on top of the picture box. The contained controls move with the picture box and their Top and Left properties will be relative to the picture box rather than the form.
A common use for the picture box container is as a toolbar or status bar. You can place image controls on it to act as buttons, or add labels to display status messages. By setting the Align property to Top, Bottom, Left, or Right, the picture box will "stick" to the edge of the form.
Picture box used as a status bar
The Images Application
The form shown in Figure 3.18 uses four image controls, a shape control, a picture box, and a command button. When the user selects a playing card symbol, the shape control highlights the symbol and a description is displayed in the picture box. For a working version of this example, see Images.frm in the Controls.vbp sample application.
Image and shape control example
The code in the image control Click event looks like this:
Private Sub imgHeart_Click()
shpCard.Left = imgClub.Left
picStatus.Cls
picStatus.Print "Selected: Club"
shpCard.Visible = True
End Sub
Additional Controls
Several other standard controls are included in the Visual Basic toolbox. Some controls are useful for working with large amounts of data contained in an external database. Other controls can be used to access the Windows file system. Still other controls defy categorization, but are useful nonetheless.
You can also use ActiveX controls, previously called custom or OLE controls, in a Visual Basic application in the same way that you use the standard controls. The Professional and Enterprise editions of Visual Basic include several ActiveX controls as well as the capability to build your own controls. Additional ActiveX controls for just about any purpose imaginable are available for purchase from numerous vendors.
Data Access Controls
In today's business, most information is stored in one or more central databases. Visual Basic includes several data access controls for accessing most popular databases, including Microsoft Access and SQL Server.
Visual Basic includes three controls for adding file handling capabilities to your application. These controls are normally used together to provide a view of drives, directories and files; they have special properties and events that tie them together.
Several other standard controls are included in Visual Basic. Each serves a unique purpose.
Understanding Focus
Focus is the ability to receive user input through the mouse or keyboard. When an object has the focus, it can receive input from a user. In the Microsoft Windows interface, several applications can be running at any time, but only the application with the focus will have an active title bar and can receive user input. On a Visual Basic form with several text boxes, only the text box with the focus will display text entered by means of the keyboard.
The GotFocus and LostFocus events occur when an object receives or loses focus. Forms and most controls support these events.
You can give focus to an object by:
An object can receive focus only if its Enabled and Visible properties are set to True. The Enabled property allows the object to respond to user-generated events such as keyboard and mouse events. The Visible property determines whether an object is visible on the screen.
Validate Event of Controls
Controls also have a Validate event, which occurs before a control loses focus. However, this event occurs only when the CausesValidation property of the control that is about to receive the focus is set to True. In many cases, because the Validate event occurs before the focus is lost, it is more appropriate than the LostFocus event for data validation. For more information, see "Validating Control Data By Restricting Focus" in "Using Visual Basic's Standard
Controls That Can't Receive Focus
Some controls, such as the lightweight controls, cannot receive focus. Lightweight controls include the following:
Setting the Tab Order
The tab order is the order in which a user moves from one control to another by pressing the TAB key. Each form has its own tab order. Usually, the tab order is the same as the order in which you created the controls.
Tab example
To change the tab order for a control, set the TabIndex property. The TabIndex property of a control determines where it is positioned in the tab order. By default, the first control drawn has a TabIndex value of 0, the second has a TabIndex of 1, and so on. When you change a control's tab order position, Visual Basic automatically renumbers the tab order positions of the other controls to reflect insertions and deletions. For example, if you make Command1 first in the tab order, the TabIndex values for the other controls are automatically adjusted upward, as shown in the following table.
The highest TabIndex setting is always one less than the number of controls in the tab order (because numbering starts at 0). Even if you set the TabIndex property to a number higher than the number of controls, Visual Basic converts the value back to the number of controls minus 1. Removing a Control from the Tab Order
Usually, pressing TAB at run time selects each control in the tab order. You can remove a control from the tab order by setting its TabStop property to False (0).
A control whose TabStop property has been set to False still maintains its position in the actual tab order, even though the control is skipped when you cycle through the controls with the TAB key.
Note An option button group has a single tab stop. The selected button (that is, the button with its Value set to True) has its TabStop property automatically set to True, while the other buttons have their TabStop property set to False
Menu Basics
If you want your application to provide a set of commands to users, menus offer a convenient and consistent way to group commands and an easy way for users to access them.
The elements of a menu interface on a Visual Basic form
The menu bar appears immediately below the title bar on the form and contains one or more menu titles. When you click a menu title (such as File), a menu containing a list of menu items drops down. Menu items can include commands (such as New and Exit), separator bars, and submenu titles. Each menu item the user sees corresponds to a menu control you define in the Menu Editor (described later in this chapter).
To make your application easier to use, you should group menu items according to their function. In Figure 3.21, for example, the file-related commands New, Open, and Save As… are all found on the File menu.
Some menu items perform an action directly; for example, the Exit menu item on the File menu closes the application. Other menu items display a dialog box — a window that requires the user to supply information needed by the application to perform the action. These menu items should be followed by an ellipsis (…). For example, when you choose Save As… from the File menu, the Save File As dialog box appears.
A menu control is an object; like other objects it has properties that can be used to define its appearance and behavior. You can set the Caption property, the Enabled and Visible properties, the Checked property, and others at design time or at run time. Menu controls contain only one event, the Click event, which is invoked when the menu control is selected with the mouse or using the keyboard.
Pop-up Menus A pop-up menu is a floating menu that is displayed over a form, independent of the menu bar, as shown in Figure 3.22. The items displayed on the pop-up menu depend on the location of the pointer when the right mouse button is pressed; therefore, pop-up menus are also called context menus. (In Windows 95 or later, you activate context menus by clicking the right mouse button.) You should use pop-up menus to provide an efficient method for accessing common, contextual commands.
A pop-up menu
Any menu that has at least one menu item can be displayed at run time as a pop-up menu. To display a pop-up menu, use the PopupMenu method
Using the Menu Editor
With the Menu Editor, you can add new commands to existing menus, replace existing menu commands with your own commands, create new menus and menu bars, and change and delete existing menus and menu bars. The main advantage of the Menu Editor is its ease of use. You can customize menus in a completely interactive manner that involves very little programming.
To display the Menu Editor
While most menu control properties can be set using the Menu Editor; all menu properties are also available in the Properties window. You would normally create a menu in the Menu Editor; however, to quickly change a single property, you could use the Properties window.
Prompting the User with Dialog Boxes In Windows-based applications, dialog boxes are used to prompt the user for data needed by the application to continue or to display information to the user. Dialog boxes are a specialized type of form object that can be created in one of three ways:
This dialog is displayed when you invoke the MsgBox function in code. The code for displaying the dialog box shown in Figure 3.24 looks like this:
MsgBox "Error encountered while trying to open file," & vbCrLf & "please retry.", vbExclamation, "Text Editor"
You supply three pieces of information, or arguments, to the MsgBox function: the message text, a constant (numeric value) to determine the style of the dialog box, and a title. Styles are available with various combinations of buttons and icons to make creating dialog boxes easy.
Because most dialog boxes require user interaction, they are usually displayed as modal dialog boxes. A modal dialog box must be closed (hidden or unloaded) before you can continue working with the rest of the application. For example, a dialog box is modal if it requires you to click OK or Cancel before you can switch to another form or dialog box.
Modeless dialog boxes let you shift the focus between the dialog box and another form without having to close the dialog box. You can continue to work elsewhere in the current application while the dialog box is displayed. Modeless dialog boxes are rare; you will usually display a dialog because a response is needed before the application can continue. From the Edit menu, the Find dialog box in Visual Basic is an example of a modeless dialog box. Use modeless dialog boxes to display frequently used commands or information.
Introduction to Visual Basic Controls
The Visual Basic toolbox contains the tools you use to draw controls on your forms
The Visual Basic toolbox
Control Categories
Intrinsic controls, such as the command button and frame controls. These controls are contained inside the Visual Basic .exe file. Intrinsic controls are always included in the toolbox, unlike ActiveX controls and insertable objects, which can be removed from or added to the toolbox.
ActiveX controls, which exist as separate files with a .ocx file name extension. These include controls that are available in all editions of Visual Basic (DataCombo, DataList controls, and so on) and those that are available only in the Professional and Enterprise editions (such as Listview, Toolbar, Animation, and Tabbed Dialog). Many third-party ActiveX controls are also available.
Note Controls with the .vbx file name extension use older technology and are found in applications written in earlier versions of Visual Basic. When Visual Basic opens a project containing a .vbx control, the default behavior is to replace the .vbx control with an .ocx control, but only if an .ocx version of the control is available. See "Updating Older Versions of Visual Basic Controls" later in this chapter for information on updating controls to the .ocx format.
Insertable Objects, such as a Microsoft Excel Worksheet object containing a list of all your company's employees, or a Microsoft Project Calendar object containing the scheduling information for a project. Since these can be added to the toolbox, they can be considered controls. Some of these objects also support Automation (formerly called OLE Automation), which allows you to program another application's objects from within a Visual Basic application.
Intrinsic Controls
The following table summarizes the intrinsic controls found in the Visual Basic toolbox.
Icon
Control name Class name Description Check box CheckBox Displays a True/False or Yes/No option. You can check any number of check boxes on a form at one time. Combo box ComboBox Combines a text box with a list box. Allows a user to type in a selection or select an item from a drop-down list. Command button CommandButton Carries out a command or action when a user chooses it. Data Data Enables you to connect to an existing database and display information from it on your forms. Directory list box DirListBox Displays and allows a user to select directories and paths. Drive list box DriveListBox Displays and allows a user to select valid disk drives. File list box FileListBox Displays and allows a user to select from a list of files. Frame Frame Provides a visual and functional container for controls.
Horizontal and vertical scroll bars HScrollBar and VScrollBar Allow a user to add scroll bars to controls that do not automatically provide them. (These are not the same as the built-in scroll bars found with many controls.) Image Image Displays bitmaps, icons, or Windows metafiles, JPEG, or GIF files; acts like a command button when clicked. Label Label Displays text a user cannot interact with or modify Line Line Adds a straight-line segment to a form. List box ListBox Displays a list of items that a user can choose from. OLE container OLE Embeds data into a Visual Basic application. Option button OptionButton The Option Button control, as part of an option group with other option buttons, displays multiple choices, from which a user can choose only one. Picture box PictureBox Displays bitmaps, icons, or Windows metafiles, JPEG, or GIF files. It also displays text or acts as a visual container for other controls. Shape Shape Adds a rectangle, square, ellipse, or circle to a form, frame, or picture box. Text box TextBox Provides an area to enter or display text. Timer Timer Executes timer events at specified time intervals.
Standard ActiveX Controls
The Learning edition of Visual Basic contains a number of ActiveX controls (referred to as standard ActiveX controls) that allow you to add advanced features to your applications. ActiveX controls have the file name extension .ocx and can be used in your project by manually adding them to the toolbox.
The following table summarizes the standard ActiveX controls available in the Learning edition of Visual Basic.
Icon Control name Class name Description ADO Data Control ADODC Creates a connection to a database using ADO. Assignable to the DataSource property of other controls such as the DataGrid. Common dialog CommonDialog Provides a standard set of dialog boxes for operations such as opening and saving files, setting print options, and selecting colors and fonts. DataCombo DataCombo Provides most of the features of the standard combo box control, plus increased data access capabilities. DataGrid DataGrid A grid control that allows can be data-bound to a data source such as the ADO Data Control. Reading and editing the recordset is possible. DataList DataList Provides most of the features of the standard list box control, plus increased data access capabilities. Microsoft Hierarchical FlexGrid MSHFlexGrid A read-only grid control that can be bound the Data Environment designer to show hierarchical recordsets.
Adding and Removing ActiveX Controls
You move ActiveX controls to and from the toolbox using the following procedures.
From the Project menu, choose Components.
Select the check box next to the name of the .ocx control, and then choose OK. Once a control is placed in the toolbox, you can add it to a form just as you would an intrinsic control.
To remove an ActiveX control
Remove all instances of the control from the forms in your project. Delete any references to the control in the project's code. If references to a deleted control are left in your code, an error message will display when you compile the application.
From the Project menu, choose Components.
Clear the check box next to the name of the .ocx control, and then choose OK. An error message will display if there are remaining instances of the control in your project.
Using the Value of a Control
All controls have a property that you can use to store or retrieve values just by referring to the control, without using the property name. This is called the value of the control and is usually the most important or most commonly used property of the control. The following table lists the property that is considered to be the value for each control.
Control Value Check box Value Combo box Text Command button Value Common dialog Action Data Caption DataCombo Text DataGrid Text DataList Text Directory list box Path Drive list box Drive File list box FileName FlexGrid Text Frame Caption Horizontal scroll bar Value Image Picture Label Caption Line Visible List box Text Option button Value Picture box Picture Shape Shape Text box Text Timer Enabled Vertical scroll bar Value
Whenever you want to refer to a property on a control that happens to be the value of that control, you can do so without specifying the property name in your code. For example, this line sets the value of the Text property of a text box control: Text1 = "This text is assigned to the Text property _
of Text1"
In this example, the Caption property of Label1 is set to the FileName property of File1 whenever the user clicks a file in the file list box:
Private Sub File1_Click ()
Label1 = File1
End Sub
Note Because using the value of a control makes your code somewhat less readable, the examples in this guide do not use it but instead refer explicitly to the properties on all controls. You may want to try writing your code both ways, and decide to use the value of controls in your code if you have no trouble reading it.
Working with Control Arrays
A control array is a group of controls that share the same name and type. They also share the same event procedures. A control array has at least one element and can grow to as many elements as your system resources and memory permit; its size also depends on how much memory and Windows resources each control requires. The maximum index you can use in a control array is 32767. Elements of the same control array have their own property settings. Common uses for control arrays include menu controls and option button groupings.
Note Visual Basic includes the ability to dynamically add unreferenced controls to the Controls collection at run time. This topic refers only to referenced controls added at design time by cutting and pasting a control onto the form. For more information about adding controls at run time, see the reference topic "Add Method (Controls Collection)" and "Add Method (Licenses Collection)."
Why Use Control Arrays?
Adding controls with control arrays uses fewer resources than simply adding multiple controls of the same type to a form at design time. Control arrays are also useful if you want several controls to share code. For example, if three option buttons are created as a control array, the same code is executed regardless of which button was clicked.
If you want to create a new instance of a control at run time, that control must be a member of a control array. With a control array, each new element inherits the common event procedures of the array.
Using the control array mechanism, each new control inherits the common event procedures already written for the array. For example, if your form has several text boxes that each receive a date value, a control array can be set up so that all of the text boxes share the same validation code.
Sample Application: Calc.vbp
The Name and Index property values for the control arrays in the Calculator example are listed in the following table.
Number(n) Operator(n) 0 = Number(0) + = Operator(1) 1 = Number(1) – = Operator(2) 2 = Number(2) X = Operator(3) 3 = Number(3) / = Operator(4) 4 = Number(4) = = Operator(5) 5 = Number(5) 6 = Number(6) 7 = Number(7) 8 = Number(8) 9 = Number(9)
Notice how each control is referred to with the syntax object(index). You specify the index of a control when you create it. In fact, specifying any index for a control at design time makes that control part of an array.
The Index property distinguishes one element of the control array from another. When one of the controls in the array recognizes an event, Visual Basic calls a common event procedure and passes an argument (the value of the Index property) to identify which control actually recognizes the event.
For example, the first line of the Number_Click event procedure is:
Private Sub Number_Click (Index As Integer)
If Number(0) recognizes the event, Visual Basic passes 0 as the index argument, and if Number(1) recognizes the event, Visual Basic passes 1 as the index argument. Other than the index value, the remainder of the Number_Click code that is executed is the same for both Number(0) through Number(9).
Using the ADO Data Control
The ADO Data control uses Microsoft ActiveX Data Objects (ADO) to quickly create connections between data-bound controls and data providers. Data-bound controls are any controls that feature a DataSource property. Data providers can be any source written to the OLE DB specification. You can also easily create your own data provider using Visual Basic's class module.
Although you can use the ActiveX Data Objects directly in your applications, the ADO Data control has the advantage of being a graphic control (with Back and Forward buttons) and an easy-to-use interface that allows you to create database applications with a minimum of code.
The ADO Data Control
Several of the controls found in Visual Basic's Toolbox can be data-bound, including the CheckBox, ComboBox, Image, Label, ListBox, PictureBox, and TextBox controls. Additionally, Visual Basic includes several data-bound ActiveX controls such as the DataGrid, DataCombo, Chart, and DataList controls. You can also create your own data-bound ActiveX controls, or purchase controls from other vendors.
Previous versions of Visual Basic featured the intrinsic Data control and the Remote Data control (RDC) for data access. Both controls are still included with Visual Basic for backward compatibility. However, because of the flexibility of ADO, it's recommended that new database applications be created using the ADO Data Control.
Creating a Front-end Database Application with Minimal Code
It's possible to create a database application using a minimum of code by setting a few properties at design time. If you are using an OLE DB data source, the Microsoft Data Link Name (.UDL) must be created on your machine. See "Creating the Northwind OLE DB Data Link" for a step-by-step example.
To create a simple front-end database application Draw an ADO Data Control on a form. (The icon's ToolTip is "ADODC.")
If the control is not available in the Toolbox, press CTRL+T to display the Components dialog box. In the Components dialog, click Microsoft ADO Data Control.
On the Toolbox, click the ADO Data Control to select it. Then press F4 to display the Properties window.
In the Properties window, click ConnectionString to display the ConnectionString dialog box.
If you have created a Microsoft Data Link file (.UDL), select Use OLE DB File and click Browse to find the file on the computer. If you use a DSN, click Use ODBC Data Source Name and select a DSN from the box, or click New to create one. If you wish to use create a connection string, select Use ConnectionString, and then click Build, and use the Data Link Properties dialog box to create a connection string. After creating the connection string, click OK. The ConnectionString property will be filled with a string like:
driver={SQL Server};server=bigsmile;uid=sa;pwd=pwd;database=pubs
In the Properties window, set the RecordSource property to a SQL statement. For example,
SELECT * FROM Titles WHERE AuthorID = 72
You should always include a WHERE clause when accessing a table. Failing to do so will lock the entire table, which would be a major hindrance to other users.
Draw a TextBox control on the form to display the database information.
In the Properties window, set the DataSource property for Text1 to the name of the ADO Data control (ADODC1). This binds the text box to the ADO Data control.
In the Properties window, click DataField and a list of available fields will drop down. Click the name of the field you want to display.
Repeat steps 6, 7, and 8 for each additional field you want to access.
Press F5 to run the application. You can use the four arrow buttons on the ADO Data control to move to the beginning of the data, to the end of the data, or from record to record through the data.
Setting the ConnectionString, Source, DataSource, and DataField Programmatically
The code below shows how to set these four properties programmatically. Note that setting the DataSource property requires the Set statement.
Private Sub Form_Load()
With ADODC1
.ConnectionString = "driver={SQL Server};" & _
"server=bigsmile;uid=sa;pwd=pwd;database=pubs"
.RecordSource = "Select * From Titles Where AuthorID = 7"
End With
Set Text1.DataSource = ADODC1
Text1.DataField = "Title"
End Sub
ADO Data Control Events
The ADO Data control features several events that you can program. The table below shows the events and when they occur; however the table is not meant to be a complete list all of the conditions when the events occur.
WillMove
On Recordset.Open, Recordset.MoveNext, Recordset.Move, Recordset.MoveLast, Recordset.MoveFirst, Recordset.MovePrevious, Recordset.Bookmark, Recordset.AddNew, Recordset.Delete, Recordset.Requery, Recordset.Resync
MoveComplete
After WillMove
WillChangeField
Before the Value property changes
FieldChangeComplete
After WillChangeField
WillChangeRecord
On Recordset.Update, Recordset.Delete, Recordset.CancelUpdate, Recordset.UpdateBatch, Recordset.CancelBatch
RecordChangeComplete
After WillChangeRecord
WillChangeRecordset
On Recordset.Requery, Recordset.Resync, Recordset.Close, Recordset.Open, Recordset.Filter
RecordsetChangeComplete
After WillChangeRecordset
InfoMessage
When the data provider returns a result
Setting Database-Related Properties of the ADO Data Control
When creating a connection, you can use one of three sources: a Connection String, an OLE DB file (.UDL), or an ODBC Data Source Name (DSN). When using a DSN, it's likely you will not have to alter any of the other properties of the control.
However, if you are familiar with database technology, you can alter some of the other properties exposed on the ADO Data Control. The following list describes database-related properties of the control. The list also suggests a logical order for setting the properties.
Note Database technology is complex, and the suggestions below are not meant to be taken as rules.
ConnectionString — The ConnectionString property is a string that can contain all the settings necessary to make a connection. The parameters passed in the string are driver-dependent. For example, ODBC drivers allow the string to contain driver, provider, default database, server, username, and password.
UserName — The name of the user, necessary if the database is password-protected. Like the Provider property, this property can be specified in the ConnectionString. If you provide both a ConnectionString and a UserName, the ConnnectionString value will override the UserName property's value.
Password — Also needed when accessing a protected database. Like Provider and UserName, this property's value will be overridden if the value is specified in the ConnectionString.
RecordSource — This property generally contains a statement that determines what will be retrieved from the database.
CommandType — The CommandType property instructs the data provider if the Source is a SQL statement, a table name, a stored procedure, or an unknown type.
CursorLocation — This property specifies where the cursor is located, on the client or on the server. The consequences of this decision affect the next few properties you set.
CursorType — The CursorType property determines if the recordset is static, dynamic, or a keyset cursor type.
LockType — The LockType determines how the data is locked when others attempt to change data you are editing. How you set the LockType is a complex decision, dependent on many factors.
Mode — The Mode property determines what you intend to do with the recordset. For example, you can achieve some performance gains by setting it to read-only, if you are only interested in creating reports.
MaxRecords — This property determines how large the cursor will be. How you determine this depends on the size of the records you are retrieving and the resources available on your computer (memory). A large record (one with many columns and large strings) would take more resources than a smaller record. Consequently the MaxRecords property should be no larger than necessary.
ConnectionTimeout — Set the ConnectionTimeout to the number of seconds to wait while establishing the connection. An error is returned if the connection times out.
CacheSize — The CacheSize property specifies how many records can be retrieved from the cursor. If you've set the CursorLocation to client side, then this property can be set to a smaller number (as small as 1) with no adverse effects. If it's on the server side, you should optimize this figure to suit the number of rows you want to view at any one time. For example, if you use the DataGrid control to view 30 rows, set the CacheSize to 60, allowing you to scroll without retrieving more data.
BOFAction, EOFAction — These two properties determine what will happen when the control is at the beginning and end of the cursor. Choices include staying at the beginning or end, moving to the first or last record, or adding a new record (at the end only).
Using the Check Box Control
The check box control displays a check mark when it is selected. It is commonly used to present a Yes/No or True/False selection to the user. You can use check box controls in groups to display multiple choices from which the user can select one or more.
Using the Common Dialog Control
The common dialog control provides a standard set of dialog boxes for operations such as opening and saving files, setting print options, and selecting colors and fonts. The control also has the ability to display Help by running the Windows Help engine.
The common dialog control provides an interface between Visual Basic and the procedures in the Microsoft Windows dynamic-link library Commdlg.dll. To create a dialog box using this control, Commdlg.dll must be in your Microsoft Windows \System directory.
You use the common dialog control in your application by adding it to a form and setting its properties. The dialog displayed by the control is determined by the methods of the control. At run time, a dialog box is displayed or the Help engine is executed when the appropriate method is invoked; at design time, the common dialog control is displayed as an icon on a form. This icon can't be sized.
The common dialog control allows you to display these commonly used dialog boxes:
Displaying Open and Save As Dialog Boxes
The Open dialog box allows the user to specify a drive, a directory, a file name extension, and a file name.
The Save As dialog box is identical to the Open dialog in appearance, except for the dialog's caption, and file names appearing dimmed out. At run time, when the user chooses a file and closes the dialog box, the FileName property is used to get the selected file name.
To display the Open dialog box
With all the common dialog boxes, when the CancelError property is True, an error is generated when the user clicks the dialog box's Cancel button. You detect that the Cancel button was pressed by trapping the error when the dialog box is displayed.
The following code displays an Open dialog box and uses the selected file name as an argument to a procedure that opens a file:
Private Sub mnuFileOpen_Click ()
' CancelError is True.
On Error GoTo ErrHandler
' Set filters.
CommonDialog1.Filter = "All Files (*.*)|*.*|Text _
Files (*.txt)|*.txt|Batch Files (*.bat)|*.bat"
' Specify default filter.
CommonDialog1.FilterIndex = 2
' Display the Open dialog box.
CommonDialog1.ShowOpen
' Call the open file procedure.
OpenFile (CommonDialog1.FileName)
Exit Sub
ErrHandler:
' User pressed Cancel button.
Exit Sub
End Sub
Using the Color Dialog Box
The Color dialog box allows the user to select a color from a palette or to create and select a custom color. At run time, when the user chooses a color and closes the dialog box, you use the Color property to get the selected color.
The Color dialog box
To display the Color dialog box
Private Sub Command1_Click ()
' Set Cancel to True.
CommonDialog1.CancelError = True
On Error GoTo ErrHandler
' Set the Flags property.
CommonDialog1.Flags = cdlCCRGBInit
' Display the Color dialog box.
CommonDialog1.ShowColor
' Set the form's background color to the selected
' color.
Form1.BackColor = CommonDialog1.Color
Exit Sub
ErrHandler:
' User pressed Cancel button.
Exit Sub
End Sub
Using the Font Dialog Box The Font dialog box allows the user to select a font by its size, color, and style. Once the user makes selections in the Font dialog box, the following properties contain information about the user's selection.
Property
Determines Color The selected color. To use this property, you must first set the Flags property to cdlCFEffects FontBold Whether bold was selected. FontItalic Whether italic was selected. FontStrikethru Whether strikethrough was selected. FontUnderline Whether underline was selected. FontName The selected font name. FontSize The selected font size. The Font dialog box
To display the Font dialog box
Private Sub Command1_Click ()
' Set Cancel to True.
CommonDialog1.CancelError = True
On Error GoTo ErrHandler
' Set the Flags property.
CommonDialog1.Flags = cdlCFBoth Or cdlCFEffects
' Display the Font dialog box.
CommonDialog1.ShowFont
' Set text properties according to user's
' selections.
Text1.Font.Name = CommonDialog1.FontName
Text1.Font.Size = CommonDialog1.FontSize
Text1.Font.Bold = CommonDialog1.FontBold
Text1.Font.Italic = CommonDialog1.FontItalic
Text1.Font.Underline = CommonDialog1.FontUnderline
Text1.FontStrikethru = CommonDialog1.FontStrikethru
Text1.ForeColor = CommonDialog1.Color
Exit Sub
ErrHandler:
' User pressed Cancel button.
Exit Sub
End Sub
Using the Print Dialog Box
The Print dialog box allows the user to specify how output should be printed. The user can specify a range of pages to be printed, a print quality, a number of copies, and so on. This dialog box also displays information about the currently installed printer and allows the user to configure or reinstall a new default printer.
Note This dialog box does not actually send data to a printer. It allows users to specify how they want data printed. You must write code to print the data in the format they select.
At run time, when the user makes selections in the Print dialog box, the following properties contain information about the user's selection. Property
Determines Copies The number of copies to print. FromPage The page to start printing. ToPage The page to stop printing. hDC The device context for the selected printer. Orientation The page's orientation (portrait or landscape).
The Print dialog box
To display the Print dialog box
Private Sub Command1_Click ()
Dim BeginPage, EndPage, NumCopies, Orientation, i
' Set Cancel to True.
CommonDialog1.CancelError = True
On Error GoTo ErrHandler
' Display the Print dialog box.
CommonDialog1.ShowPrinter
' Get user-selected values from the dialog box.
BeginPage = CommonDialog1.FromPage
EndPage = CommonDialog1.ToPage
NumCopies = CommonDialog1.Copies
Orientation = CommonDialog1.Orientation
For i = 1 to NumCopies
' Put code here to send data to your printer.
Next
Exit Sub
ErrHandler:
' User pressed Cancel button.
Exit Sub
End Sub
Note If the PrinterDefault property is set to True, you can print to the Visual Basic Printer object. In addition, when the PrinterDefault property is True, any changes the user makes in the Setup portion of the Print dialog box are used to change the printer settings in the user's Printer setup.
Using the ShowHelp Method to Display a Help File
The ShowHelp method of the common dialog control allows you to display a Help file.
To display a Help file using the ShowHelp method
Private Sub Command1_Click()
' Set Cancel to True.
CommonDialog1.CancelError = True
On Error GoTo ErrHandler
' Set the HelpCommand Property
CommonDialog1.HelpCommand = cdlHelpForceFile
' Specify the Help file.
CommonDialog1.HelpFile = "c:\Windows\Cardfile.hlp"
' Display the Windows Help engine.
CommonDialog1.ShowHelp
Exit Sub
ErrHandler:
' User pressed Cancel button.
Exit Sub
End Sub
Using the Data Control
The intrinsic Data control implements data access by using the Microsoft Jet Database engine –the same database engine that powers Microsoft Access. This technology gives you seamless access to many standard database formats and allows you to create data-aware applications without writing any code. The intrinsic Data control is best suited to smaller (desktop) databases, such as Access and ISAM databases.
You can use the intrinsic Data control to create applications that display, edit, and update information from many types of existing databases, including Microsoft Access, Btrieve, dBASE, Microsoft FoxPro®, and Paradox. You can also use it to access Microsoft Excel, Lotus 1-2-3, and standard ASCII text files as if they were true databases. In addition, the data control allows you to access and manipulate remote Open Database Connectivity (ODBC) databases such as Microsoft SQL Server and Oracle.
Note Both the Data control and Remote Data control are included with Visual Basic for backward compatibility. However, because of the flexibility of ActiveX Data Objects (ADO), it's recommended that you create new database applications using the ADO Data control. For more details, see "Using the ADO Data Control." The Data control, Remote Data control, and the ADO Data control are all conceptually similar: all three are "data controls" that connect a data source to a data-bound control. All three also share the same appearance — a set of four buttons that allow the user to go immediately to the beginning of the recordset, end of the recordset, and scroll backwards and forwards through the recordset.
Creating a Simple Database Application with the Data Control
To create a simple database application with the Data control
The following data-related properties can be set at design time. The list suggests a logical order for setting the properties:
Note Database technology is a complex science, and the suggestions below are not meant to be taken as rules.
The DataGrid control is a spreadsheet-like bound control that displays a series of rows and columns representing records and fields from a Recordset object. You can use the DataGrid to create an application that allows the end user to read and write to most databases. The DataGrid control can be quickly configured at design time with little or no code. When you set the DataGrid control's DataSource property at design time, the control is automatically filled and its column headers are automatically set from the data source's recordset. You can then edit the grid's columns; delete, rearrange, add column headers to, or adjust any column's width.
At run time, the DataSource can be programmatically switched to view a different table, or you can modify the query of the current database to return a different set of records.
Note The DataGrid control is code-compatible with the DBGrid control that shipped in Visual Basic 5.0 with one exception: the DataGrid control doesn't support the DBGrid notion of "unbound mode." The DBGrid control is included with Visual Basic in the Tools directory.
Possible Uses
You can create a database application with the DataGrid control without writing a line of code by taking advantage of its design-time features. The following instructions outline the general steps needed to implement the DataGrid control in a typical use. For complete step-by-step instructions, see the topic "DataGrid Scenario 1: Create a Simple Database Application with the DataGrid Control."
To implement a DataGrid control at design-time
Once you have created a grid using the design-time features, you may also wish to dynamically change the data source of the grid at run time. The general methods for accomplishing this are discussed below.
Changing the RecordSource of the DataSource
The most common method of changing displayed data is to alter the query of the DataSource. For example, if the DataGrid control uses an ADO Data control as its DataSource, rewriting the RecordSource and refreshing the ADO Data control will change the data displayed. ' The ADO Data control is connected to the Northwind database's
' Products table. The new query asks for all records which have
' the SupplierID = 12.
Dim strQuery As String
strQuery = "SELECT * FROM Suppliers WHERE SupplierID = 12"
Adodc1.RecordSource = strQuery
Adodc1.Refresh
Changing the DataSource
At run-time you can reset the DataSource property to a different data source. For example, you may have several ADO Data controls, each connected to different databases, or set to different RecordSource properties. Simply reset the DataSource from one ADO Data control to another: ' Reset the DataSource to an ADO Data control that is connected to
' the Pubs database, using the Authors table.
Set DataGrid1.DataSource = adoPubsAuthors
Rebind the DataSource
When using the DataGrid control with a remote database such as SQLServer, it's possible that the structure of the table may become altered. For example, a field may be added to the table. In that case, you can invoke the Rebind method to recreate the grid from the new structure. Note that if you have altered the columns' layout of the grid at design-time, the DataGrid control will attempt to recreate the current layout, including any empty columns. You can, however, force the grid to reset all columns by first invoking the ClearFields method. Returning Values from the DataGrid
Once the DataGrid is connected to a database, you may want to monitor which cell the user has clicked. Use the RowColChange event — not the Click event — as shown below: Private Sub DataGrid1_RowColChange(LastRow As Variant, ByVal LastCol As Integer)
' Print the Text, row, and column of the cell the user clicked.
Debug.Print DataGrid1.Text; DataGrid1.Row; DataGrid1.Col
End Sub
Using the CellText and CellValue Methods
The CellText and CellValue properties are useful when a column has been formatted using the NumberFormat property. The NumberFormat property changes the format of any column that contains a number without changing the format of the actual data. For example, given a grid with a column named ProductID that contains integers, the code below will cause the DataGrid to display the values in the format "P-0000." In other words, although the actual value held in the ProductID field is "3," the value displayed by the grid will be "P-0003." Private Sub Form_Load()
DataGrid1.Columns("ProductID").NumberFormat = "P-0000"
End Sub
To return the actual value contained in the database, use the CellValue method, as shown below:
Private Sub DataGrid1_RowColChange(LastRow As Variant, ByVal LastCol As Integer)
Debug.Print _
DataGrid1.Columns("ProductID").CellValue(DataGrid1.Bookmark)
End Sub
Note Both the CellValue used above, and the CellText value used below, require the bookmark property as an argument to function correctly.
Conversely, if you want to return the formatted value of the field, use the CellText method:
Private Sub DataGrid1_RowColChange(LastRow As Variant, ByVal LastCol As Integer)
Debug.Print _
DataGrid1.Columns("ProductID").CellText(DataGrid1.Bookmark)
End Sub
Note The CellText method above is equivalent to using the Text property of the DataGrid control. Creating the Northwind OLE DB Data Link
An essential step in accessing data is creating an OLE DB data source for each database you want to access. The steps below create such an object for the Nwind.mdb (Northwind), supplied with Visual Basic. This data source is used in some of the example procedures supplied with the Visual Basic documentation. You need to create the OLE DB data source only once on a computer. To create the Northwind OLE DB data source
Using only a DataGrid and an ADO Data control you can create a database application that allows the end user to read and write to a recordset.
Create a simple database application using the ADO Data Control
A common use of the DataGrid is to show "details" supplied by one table in a database. For example, the Northwind (Nwind.mdb) database includes two tables, one named "Suppliers," and the other named "Products." In this example, we'll use the DataList control to show the company names of suppliers from the "Suppliers" table. When the user clicks on any company name, the DataList control will furnish the SupplierID for the company. Using that ID, a query can be constructed to retrieve all records in the "Products" table which have a matching SupplierID. In other words, when the user clicks on a company (in the DataList control), all of the products produced by that company will appear in the DataGrid control.
To fill a DataGrid Control with products from a particular supplier
The DataGrid control has several methods and menu commands that affect both the layout of the grid, and how the control is bound to data. These methods and commands include:
One feature of the DataGrid is its design-time features. You can configure a data source, such as the ADO Data control, connect it to the DataGrid control, invoke the Retrieve Structure menu command, and customize the layout of the grid.
Many applications must present information about disk drives, directories, and files. To allow users of your applications to explore the file system, Visual Basic provides two alternatives. You can use the standard dialog boxes provided by the common dialog control, or you can build custom dialogs using your own combinations of three specialized controls: the drive list box, the directory list box, and the file list box.
You can use the file-system controls to allow users to investigate and choose among available disk files in your applications. Consider using the common dialog control if you just need a standard File Open or Save dialog box.
Examining the File System
Each of the file-system controls has been carefully designed to combine flexible and sophisticated file-system inspection capabilities with easy programming. Each control performs its file-data retrieval tasks automatically, but you can write code both to customize their appearance and to specify which information they display.
The file-system controls
You can use file-system controls singly or in combination. With combinations, you can write code in each control's event procedures to determine how they interact. Or you can let them act independently.
The file-system controls used together
File-system controls obtain all their information from the operating system automatically; you can access this information or determine what is displayed by each control through its properties. For example, the contents of the current working directory is displayed by default (that is, the directory from which the application was launched, or what became the current directory as the result of a ChDir statement).
Your application can also display a list of the files with names matching a pattern, such as *.frm. Simply draw a file list box on the form and set its Pattern property to *.frm. You can specify the Pattern property at run time with the following code:
File1.Pattern = "*.FRM"
The file-system controls give you the flexibility that is not available with the common dialog control. You can mix and match them in a variety of ways, and you control their appearance and how they interact.
If your goal is simply to allow users to open and save files, a common dialog control provides a ready-to-run set of dialog boxes for these and other operations. These are the same dialog boxes used by many other Microsoft Windows – based applications, so they provide a standardized look-and-feel. They also recognize network drives when they're available.
The Drive List Box
The drive list box is a drop-down list box. By default, the current drive is displayed on the user's system. When this control has the focus, the user can type in any valid drive designation or click the arrow at the right of the drive list box. When the user clicks the arrow, the list box drops down to list all valid drives. If the user selects a new drive from the list, that drive appears at the top of the list box.
You can use code to examine the Drive property of the drive list box to determine which drive is currently selected. Your application can also specify which drive appears at the top of the list box with this simple assignment:
Drive1.Drive = "c:\"
The drive list box displays valid available drives. Choosing a drive from the list box doesn't automatically change the current working drive; however, you can use the Drive property to change drives at the operating system level by specifying it as an argument to the ChDrive statement:
ChDrive Drive1.Drive
The Directory List Box The directory list box displays the directory structure of the current drive on the user's system, beginning with the top-level directory. Initially, the name of the current directory appears highlighted and indented from directories above it in the hierarchy, back to the root. Subdirectories are indented beneath the current directory in the directory list box. As a user moves up or down the list, each of the items is highlighted in turn.
Identifying Individual Directories Each directory in the box has an integer identifier associated with it that allows you to identify individual directories. This capability is not provided by the common dialog control. The directory specified by the Path property (Dir1.Path) always has the ListIndex value of – 1. The directory immediately above it has the ListIndex value of – 2, the one above that of – 3, and so on up to the root. The first subdirectory of Dir1.Path has the ListIndex 0. If there are multiple directories at the first subdirectory level, the next has the ListIndex value of 1, then 2, and so on.
A directory structure displayed in the directory list box
Setting the Current Directory
Use the Path property of the directory list box to set or return the current directory in the box (ListIndex = – 1). For example, if you assign "c:\payroll" to Drive1.Path in Figure 7.18, the \Payroll directory becomes selected as the current working directory.
Similarly, you can assign the Drive property of the drive list box to the Path property of the directory list box:
Dir1.Path = Drive1.Drive
When this assignment is executed, the directory list box displays all the available directories and subdirectories on that drive. By default, the directory list box also displays all directories above, and any subdirectories immediately below, the current directory of a drive assigned to the Dir1.Path property. The directory list box doesn't set the current directory at the operating system level; it merely highlights the directory and gives it the ListIndex value of – 1.
To set the current working directory, use the ChDir statement. For example, the following statement changes the current directory to the one displayed in the directory list box:
ChDir Dir1.Path
In an application that uses file controls, you can set the current directory to the directory where the application's executable (.exe) file is located with the Application object:
ChDrive App.Path ' Set the drive.
ChDir App.Path ' Set the directory.
Note The Path property is available only at run time, not at design time.
Clicking a Directory Item
When a user clicks an item in a directory list box, that item is highlighted. When an item is double-clicked, it is assigned to the Path property, its ListIndex property gets the value –1, and the directory list box is redrawn to show its immediate subdirectories.
Finding a Directory's Relative Position
The ListCount property returns the number of directories below the currently expanded directory, not the total number of items in the directory list box. Because the ListIndex value of the currently expanded directory is always –1, you can write code to determine how far down from the root the currently expanded directory is in the hierarchy. For example:
' Initialize for currently expanded directory.
GoHigher = 0
' Dir1.List(x) returns empty string if the directory
' doesn't exist.
Do Until Dir1.List(GoHigher) = ""
GoHigher = GoHigher - 1
Loop
' Convert to positive number, if desired.
LevelsAbove = Abs(GoHigher)
The File List Box
The file list box displays files contained in the directory specified by the Path property at run time. You can display all the files in the current directory on the current drive using the following statement:
File1.Path = Dir1.Path
You can then display a subset of these files by setting the Pattern property — for example, *.frm displays only files with that extension. The Pattern property can also accept a list delimited by semicolons. For example, a line with the following code displays all files with the extensions .frm and .bas:
File1.Pattern = "*.frm; *.bas"
Visual Basic supports the ? wildcard character. For instance, ???.txt displays files that have base names of only three characters with the extension .txt.
Working with File Attributes The attributes of the currently selected file (Archive, Normal, System, Hidden, and ReadOnly) are also available through file list box properties. You use these properties to specify which kinds of files to display in a file list box. The default value for the System and Hidden attributes is False. The default value for the Normal, Archive, and ReadOnly attributes is True.
To display only read-only files in the list box, for example, simply set the ReadOnly property to True and the other attribute properties to False:
File1.ReadOnly = True
File1.Archive = False
File1.Normal = False
File1.System = False
File1.Hidden = False
When Normal = True, those files without the System or Hidden attribute are displayed. When Normal = False, you can still display files with ReadOnly and/or Archive attributes by setting these attributes to True.
Note You cannot use the attribute properties to set file attributes. To set file attributes, use the SetAttr statement.
By default, you can highlight only a single selection in a file list box. To select multiple files, use the MultiSelect property.
Using File-System Controls Together
If you use a combination of file-system controls, you can synchronize the information they display. For example, if you have a drive list box, a directory list box, and a file list box with the default names Drive1, Dir1, and File1, the sequence of events might work like this:
Because users often want to find a file or group of files available to an application quickly, many applications provide capabilities for investigating the file system. The Winseek.vbp sample application helps the user browse drives and directories, and displays any category of files
File-system controls in the WinSeek application
The following table summarizes the controls in Seek.frm from the WinSeek application.
Control Property Setting Drive list box Name drvList Directory list box Name dirList File list box Name
Pattern filList
*.* First command button Name
Caption
Default cmdSearch
&Search
True Second command button Name
Caption cmdExit
E&xit List box Name lstFoundFiles
Note The file-system controls do not have caption properties, although you can label them and give them access keys. For more information on using labels this way, see "Using the Label Control" later in this chapter. The Drive List Box's Change Event
When the user clicks an item in the drive list box, its Change event is generated. The drvList_Change event procedure is invoked, and the following code is run:
Private Sub drvList_Change ()
On Error GoTo DriveHandler
' If new drive was selected, the Dir1 box
' updates its display.
dirList.Path = drvList.Drive
Exit Sub
' If there is an error, reset drvList.Drive with the
' drive from dirList.Path.
DriveHandler:
drvList.Drive = dirList.Path
Exit Sub
End Sub
Notice that the Change event in a drive list box occurs when a new drive is selected, either with a single mouse click or when the user moves the selection (for example, with an arrow key). The error handler is triggered by actions such as attempting to access a floppy disk drive while the drive door is open or selecting a network drive that has been inadvertently disconnected. Because the error prevents the original assignment, dirList.Path still contains the previous valid drive. Reassigning dirList.Path to drvList.Drive corrects this error.
For More Information See "Debugging Your Code and Handling Errors" for more information.
The Directory List Box's Change Event
If the user double-clicks an item in the directory list box, or if the Path property of dirList is changed in code (as in the drvList_Change procedure), the dirList_Change event is initiated. The following code responds to that event:
Private Sub dirList_Change ()
' Update file list box to synchronize with the
' directory list box.
filList.Path = dirList.Path
End Sub
This event procedure assigns the Path property of the dirList box to the Path property of the filList box. This causes a PathChange event in the filList list box, which is redrawn; you don't need to add code to the filList_PathChange procedure, because in this application, the event chain ends in the filList list box.
The Command Button's Click Event
This event procedure determines whether the highlighted item in the dirList list box is the same as the dirList.Path. If the items are different, then dirList.Path is updated. If the items are the same, then the search is performed.
Private Sub cmdSearch_Click ()
.
.
.
' If the dirList.Path is different from the
' currently selected directory, update it;
' otherwise perform the search.
If dirList.Path <> dirList.List _
(dirList.ListIndex) Then
dirList.Path = dirList.List(dirList.ListIndex)
Exit Sub
End If
' Continue with search.
.
.
.
End Sub
Note You can enhance the WinSeek application with additional features. For example, you might want to use a file control's attribute properties. You could use check boxes to allow the user to set different combinations of file attributes so that the file list box displays files that are Hidden, System, and so on. This would restrict a search to conforming files.
Using the Frame Control Frame controls are used to provide an identifiable grouping for other controls. For example, you can use frame controls to subdivide a form functionally — to separate groups of option button controls.
The frame control
In most cases, you will use the frame control passively — to group other controls — and will have no need to respond to its events. You will, however, most likely change its Name, Caption, or Font properties.
Adding a Frame Control to a Form
When using the frame control to group other controls, first draw the frame control, and then draw the controls inside of it. This enables you to move the frame and the controls it contains together. Drawing Controls Inside the Frame
To add other controls to the frame, draw them inside the frame. If you draw a control outside the frame, or use the double-click method to add a control to a form, and then try to move it inside the frame control, the control will be on top of the frame and you'll have to move the frame and controls separately.
Controls inside a frame
Note If you have existing controls that you want to group in a frame, you can select all the controls, cut them to the clipboard, select the frame control, and then paste them into the frame control.
Selecting Multiple Controls in a Frame
To select multiple controls in a frame , hold down the CTRL key while using the mouse to draw a box around the controls. When you release the mouse, the controls inside the frame will be selected.
Selecting controls inside a frame
Using the Horizontal and Vertical Scroll Bar Controls
Scroll bars provide easy navigation through a long list of items or a large amount of information by scrolling either horizontally or vertically within an application or control. Scroll bars are a common element of the Windows and Windows NT interface.
The horizontal and vertical scroll bar controls
The horizontal and vertical scroll bar controls are not the same as the built-in scroll bars found in Windows or those that are attached to text boxes, list boxes, combo boxes, or MDI forms within Visual Basic. Those scroll bars appear automatically whenever the given application or control contains more information than can be displayed in the current window size (or, in the case of text boxes and MDI forms, when the ScrollBars property is also set to True). In previous versions of Visual Basic, scroll bars were most often used as input devices. Windows interface guidelines now suggest, however, that slider controls be used as input devices instead of scroll bar controls. A slider control (of the type found in 32-bit Windows operating systems) is included in the Professional and Enterprise versions of Visual Basic.
Scroll bar controls are still of value in Visual Basic because they provide scrolling to applications or controls that do not provide them automatically. See "Scroll Bar Controls Scenario: Creating a Scrollable Graphics Viewport" for information on using scroll bars in this manner.
How the Scroll Bar Controls Work
The scroll bar controls use the Scroll and Change events to monitor the movement of the scroll box (sometimes referred to as the thumb) along the scroll bar.
Event Description Change Occurs after the scroll box is moved Scroll Occurs as the scroll box is moved. Does not occur if the scroll arrows or scroll bar is clicked. Using the Scroll event provides access to the scroll bar value as it is being dragged. The Change event occurs after the scroll box is released or when the scroll bar or scroll arrows are clicked.
The Value Property
The Value property (which, by default, is 0) is an integer value corresponding to the position of the scroll box in the scroll bar. When the scroll box position is at the minimum value, it moves to the leftmost position (for horizontal scroll bars) or the top position (for vertical scroll bars). When the scroll box is at the maximum value, the scroll box moves to the rightmost or bottom position. Similarly, a value halfway between the bottom and top of the range places the scroll box in the middle of the scroll bar.
In addition to using mouse clicks to change the scroll bar value, a user can also drag the scroll box to any point along the bar. The resulting value depends on the position of the scroll box, but it is always within the range of the Min to Max properties set by the user.
Note Min can be larger than Max if you want your scroll bar to display information changing from a larger to a smaller value.
The LargeChange and SmallChange Properties
To specify the amount of change to report in a scroll bar, use the LargeChange property for clicking in the scroll bar, and the SmallChange property for clicking the arrows at the ends of the scroll bar. The scroll bar's Value property increases or decreases by the values set for the LargeChange and SmallChange properties. You can position the scroll box at run time by setting Value between 0 and 32,767, inclusive.
Using the Image Control
The image control is used to display graphics. Image controls can display graphics in the following formats: bitmap, icon, metafile, enhanced metafile, or as JPEG or GIF files.
In addition, image controls respond to the Click event and can be used as a substitute for command buttons, as items in a toolbar, or to create simple animations.
When to Use an Image Control Instead of a Picture Box Control
The image control uses fewer system resources and repaints faster than a picture box control, but it supports only a subset of the picture box control's properties, events, and methods. Both controls support the same picture formats. However, you can stretch pictures in an image control to fit the control's size. You cannot do this with the picture box control. Supported Graphic Formats
The image control can display picture files in any of the following standard formats.
Picture format Description Bitmap A bitmap defines an image as a pattern of dots (pixels). A bitmap has the file name extensions .gif or .dib. Bitmaps are also called "paint-type" graphics.
You can use bitmaps of various color depths, including 2, 4, 8, 16, 24, and 32-bits, but a bitmap only displays correctly if the display device supports the color depth used by the bitmap. For example, an 8-bit-per-pixel (256 color) bitmap only displays in 16 colors when shown on a 4-bit-per-pixel (16 color) device.
Icon An icon is a special kind of bitmap. Icons have a maximum size of 32 pixels by 32 pixels, but under Microsoft Windows 95 or later, icons are also found in 16 by 16 pixel size. An icon has the file name extension .ico. Cursor Cursors, like icons, are essentially bitmaps. Cursors, however, also contain a hot spot, a pixel that tracks the location of the cursor by its x and y coordinates. Cursors have the file name extension .cur. Metafile coded lines and shapes. Conventional metafiles have the file name extension .wmf. Enhanced metafiles have the file name extension .emf. Only files that are compatible with Microsoft Windows can be loaded. Metafiles are also called "draw-type" graphics. JPEG JPEG (Joint Photographic Experts Group) is a compressed bitmap format which supports 8- and 24-bit color. It is a popular file format on the Internet. GIF GIF (Graphic Interchange Format) is a compressed bitmap format originally developed by CompuServe. It supports up to 256 colors and is a popular file format on the Internet.
Loading a Graphic Into the Image Control
Pictures can be loaded into the image control at design time by selecting the Picture property from the control's Properties window, or at run time by using the Picture property and the LoadPicture function.
Set Image1.Picture = LoadPicture("c:\Windows\Winlogo.cur", vbLPLarge, vbLPColor)
When a picture is loaded into the image control, the control automatically resizes to fit the picture — regardless of how small or large the image control was drawn on the form.
You may want to use icon (.ico) and cursor (.cur) files containing separate images at different sizes and color depths to support a range of display devices. The LoadPicture function's settings allow you to select images with specific color depths and sizes from an .ico or .cur file. In cases where an exact match to the requested settings isn't available, LoadPicture loads the image with the closest match available.
To clear the graphic from the image control, use the LoadPicture function without specifying a file name. For example:
Set Image1.Picture = LoadPicture
This will clear the image control even if a graphic was loaded into the Picture property at design time.
Using the Clipboard
You can also add a graphic to an image control at design time by pasting it from another application. For example, you may want to add a bitmap image that was created in Windows Paint. Simply copy the image to the Clipboard, select the image control, and either use the keyboard shortcut CTRL+V or the Paste command from the Edit menu.
The Stretch Property
The Stretch property determines whether the picture is stretched when the image control is resized at design time. If set to True, the picture loaded into the image control via the Picture property is stretched. Stretching a picture (especially a bitmap format) can produce a loss in image quality, as shown in Figure 7.28. Metafiles, which are "draw-type" graphics, are better suited for stretching. Stretching a bitmap image
Using the Label Control
Label controls are used to display text and cannot be edited by the user. They are used to identify objects on a form — provide a description of what a certain control will do if clicked, for example — or at run time, they can display information in response to an event or process in your application.
Labels are used in many instances, for many different purposes. Most commonly, they are used to label controls that don't have their own Caption properties. For example, you can use the label control to add descriptive labels to text boxes, list boxes, combo boxes and so on. They can also be used to add descriptive text to a form, for example, to provide the user with Help information.
You can also write code that changes the text displayed by a label control in response to events at run time. For example, if your application takes a few minutes to process a change, you can display a processing-status message in a label.
Because the label control cannot receive the focus, it can also be used to create access keys for other controls.
Setting the Label's Caption
To change the text displayed in the label control, use the Caption property. At design time, you can set this property by selecting it from the control's Properties window.
You can set the length of the Caption property up to a maximum of 1024 bytes.
Aligning Text The Alignment property allows you to set the alignment of the text within the label control to either Left Justify (0, the default) , Center (1), or Right Justify (2).
The AutoSize and WordWrap Properties
By default, when text entered into the Caption property exceeds the width of the control, the text wraps to the next line and is clipped if it exceeds the control's height.
To allow the control to automatically adjust to the size of its contents, set the AutoSize property to True. The control will expand horizontally to fit the entire contents of the Caption property. To allow the contents to wrap down and expand vertically, set the WordWrap property to True.
Using Labels to Create Access Keys Set the UseMnemonic property to True if you want to define a character in the Caption property of the label as an access key. When you define an access key in a label control, the user can press and hold down ALT+ the character you designate to move the focus to the next control in the tab order.
You can also create access keys for any other controls that have a Caption property by adding an ampersand (&) before the letter you want to use as the access key. To assign an access key to controls that don't have captions, use a label with the control. Because labels can't receive focus, focus automatically moves to the next control in the tab order. Use this technique to assign access keys to text boxes, picture boxes, combo boxes, list boxes, drive list boxes, directory list boxes, grids, and images.
To assign an access key to a control with a label
The line control is used to create simple line segments on a form, a frame, or in a picture box.
The line control
You can control the position, length, color, and style of line controls to customize the look of applications. Figure 7.31 shows a line control used to graphically separate the label containing the text "Company Name" from the rest of the form.
A line control on a form
The line control has limited functionality and is intended for simple uses — display and printing. Line segments cannot be joined to form other shapes, for instance. For more advanced uses you need to use the line method.
Setting Border Style and Color
You set the color and style of a line segment by using the BorderStyle and BorderColor properties.
The BorderStyle property provides you with six line styles:
The BackColor property is used to specify the color of the line.
At design time, you can set the line color by choosing the BorderColor property from the Properties window of the line control and then selecting from the available palette or system colors.
To set colors at run time, use the Visual Basic color constants (vbGreen, for example) or the system color constants (vbWindowBackground, for example) or the RGB function to specify border colors.
Note When BorderStyle is set to 0 (Transparent), the BorderColor property is ignored.
Moving and Sizing a Line Segment
You can move or resize the line control at run time by altering its X1, X2, Y1, and Y2 properties. The X1 and Y1 properties set the horizontal and vertical positions of the left end of the line segment. The X2 and Y2 properties set the horizontal and vertical positions of the right end of the line segment. You can't move a line segment using the Move method.
Drawing Lines on a Form You can use the line control to draw simple lines on forms.
To draw a line on a form
List boxes present a list of choices to the user. By default, the choices are displayed vertically in a single column, although you can set up multiple columns as well. If the number of items exceeds what can be displayed in the list box, scroll bars automatically appear on the control. The user can then scroll up and down, or left to right through the list.
The Click and Double-Click Events
A recommended practice for list box events, especially when the list box appears as part of a dialog box, is to add a command button to use with the list box. The Click event procedure for this button should make use of the list-box selection, carrying out whatever action is appropriate for your application.
Double-clicking an item in the list should have the same effect as selecting the item and then clicking the command button. To do this, have the DblClick procedure for the list box call the Click procedure for the command button:
Private Sub List1_DblClick ()
Command1_Click
End Sub
Or, set the value of the command button's Value property to True, which will automatically invoke the event procedure:
Private Sub List1_DblClick ()
Command1.Value = True
End Sub
This provides mouse users with a shortcut, yet does not prevent keyboard users from performing the same action. Note that there is no keyboard equivalent for the DblClick event.
Adding Items to a List
To add items to a list box, use the AddItem method, which has the following syntax:
box.AddItem item[, index]
Argument Description box Name of the list box item String expression to add to the list. If item is a literal constant, enclose it in quotation marks. index Specifies where the new item is to be inserted in the list. An index of 0 represents the first position. If index is omitted, the item is inserted at the end (or in the proper sorted order).
While list items are commonly added in the Form_Load event procedure, you can use the AddItem method at any time. This gives you the ability to add items to the list dynamically (in response to user actions).
The following code places "Germany," "India," "France," and "USA" into a list box named List1:
Private Sub Form_Load ()
List1.AddItem "Germany"
List1.AddItem "India"
List1.AddItem "France"
List1.AddItem "USA"
End Sub
Whenever the form is loaded at run time, the list appears as shown in Figure
Adding an Item at a Specified Position To add an item to a list at a specific position, specify an index value for the new item. For example, the next line of code inserts "Japan" into the first position, adjusting the position of the other items downward:
List1.AddItem "Japan", 0
Notice that it is 0, not 1, that specifies the first item in a list
Adding an item to a list
Sorting a List
You can specify that items be added to a list in alphabetical order by setting the Sorted property to True and omitting the index. The sort is not case-sensitive; thus, the words "japan" and "Japan" are treated the same.
When the Sorted property is set to True, using the AddItem method with the index argument can lead to unpredictable, unsorted results.
Removing Items from a List You can use the RemoveItem method to delete items from a list box. RemoveItem has one argument, index, which specifies the item to remove:
box.RemoveItem index
The box and index arguments are the same as for AddItem.
For example, to remove the first entry in a list, you would add the following line of code:
List1.RemoveItem 0
To remove all list entries in bound or standard versions of the list and combo boxes, use the Clear method:
List1.Clear
Getting List Contents with the Text Property
Usually, the easiest way to get the value of the currently selected item is to use the Text property. The Text property always corresponds to a list item a user selects at run time.
For example, the following code displays information about the population of Canada if a user selects "Canada" from a list box:
Private Sub List1_Click ()
If List1.Text = "Canada" Then
Text1.Text = "Canada has 24 million people."
End If
End Sub
The Text property contains the currently selected item in the List1 list box. The code checks to see if "Canada" has been selected and, if so, displays the information in the Text box.
Accessing List Items with the List Property
The List property provides access to all items in the list. This property contains an array in which each item in the list is an element of the array. Each item is represented in string form. To refer to an item in the list, use this syntax:
box.List(index)
The box argument is a reference to a list box, and index is the position of the item. The top item has an index of 0, the next has an index of 1, and so on. For example, the following statement displays the third item (index = 2) in a list in a text box:
Text1.Text = List1.List(2)
Determining Position with the ListIndex Property If you want to know the position of the selected item in a list, use the ListIndex property. This property sets or returns the index of the currently selected item in the control and is available only at run time. Setting the ListIndex property for a list box also generates a Click event for the control.
The value of this property is 0 if the first (top) item is selected, 1 if the next item down is selected, and so on. ListIndex is – 1 if no item is selected.
Note The NewIndex property allows you to keep track of the index of the last item added to the list. This can be useful when inserting an item into a sorted list.
Returning the Number of Items with the ListCount Property
To return the number of items in a list box, use the ListCount property. For example, the following statement uses the ListCount property to determine the number of entries in a list box:
Text1.Text = "You have " & List1.ListCount & " _
entries listed"
Creating Multiple-Column and Multiple-Selection List Boxes
The Columns property allows you to specify the number of columns in a list box. This property can have the following values:
Value Description 0 Single-column list box with vertical scrolling 1 Single-column list box with horizontal scrolling. >1 Multiple-column list box with horizontal scrolling Visual Basic takes care of wrapping list items to the next line and adding a horizontal scroll bar to the list if needed; if the list fills a single column, no scroll bar is added. Wrapping to the next column also occurs automatically as needed. Note that if a list box entry is wider than the width of a column, the text is truncated.
You can allow users to select multiple items from a list. Multiple selection in standard list boxes is handled by setting the MultiSelect property, which can have the following values
Value Type of selection Description 0 None Standard list box. 1 Simple multiple selection A click or the SPACEBAR selects or deselects additional items in the list. 2 Extended multiple
selection The SHIFT+ click or SHIFT+ an arrow key extends the selection to include all the items between the current and previous selections. CTRL+ click selects or deselects an item in the list. List Box Control Scenario 1: Adding and Deleting Items
This example shows how you can use the AddItem, RemoveItem, and Clear methods with the ListIndex and ListCount properties to add and remove list entries at run time. The example in Figure 7.36 lets a user type a client's name in a text box, which can be added to the list box if the Add button is clicked. A user can remove a current list item by selecting the item and choosing the Remove button, or by choosing Clear to clear all list entries.
Figure A list box using the AddItem, RemoveItem, and Clear methods
The number of clients in the list box is displayed in a label that looks like a text box (BorderStyle is set to 1-Fixed Single). This label is updated every time a client name is added or removed. Because the Sorted property for the list box is set to True, items are added to the list box in alphabetical order.
Create a form with a text box, a list box, three labels, and four command buttons. The following table lists the property settings for the objects in the application.
Object Property Setting Top text box Name Text txtName
(Empty) Top label Name Caption lblName
&Name to add List box Name Sorted lstClient
True Bottom label Name
Caption lblClients
# Clients Number of clients label
(looks like a text box) Name
Caption
BorderStyle lblDisplay
(Empty)
1-Fixed Single First command button Name
Caption cmdAdd
&Add Second command button Name
Caption cmdRemove
&Remove Third command button Name
Caption cmdClear
C&lear Fourth command button Name
Caption cmdClose
&Close Events in the List Box Application
Add this code to the cmdAdd_Click event procedure:
Private Sub cmdAdd_Click ()
lstClient.AddItem txtName.Text ' Add to list.
txtName.Text = "" ' Clear text box.
txtName.SetFocus
' Display number.
lblDisplay.Caption = lstClient.ListCount
End Sub
Add this code to the cmdRemove_Click event procedure:
Private Sub cmdRemove_Click ()
Dim Ind As Integer
Ind = lstClient.ListIndex ' Get index.
' Make sure list item is selected.
If Ind >= 0 Then
' Remove it from list box.
lstClient.RemoveItem Ind
' Display number.
lblDisplay.Caption = lstClient.ListCount
Else
Beep
End If
' Disable button if no entries in list.
cmdRemove.Enabled = (lstClient.ListIndex <> -1)
End Sub
Add this code to the cmdClear_Click event procedure:
Private Sub cmdClear_Click ()
' Empty list box.
lstClient.Clear
' Disable Remove button.
cmdRemove.Enabled = False
' Display number.
lblDisplay.Caption = lstClient.ListCount
End Sub
Add this code to the cmdClose_Click event procedure:
Private Sub cmdClose_Click ()
Unload Me
End Sub
Add this code to the lstClient_Click event procedure: Private Sub lstClient_Click ()
cmdRemove.Enabled = lstClient.ListIndex <> -1
End Sub
Add this code to the txtName_Change event procedure:
Private Sub txtName_Change ()
' Enable the Add button if at least one character
' in the name.
cmdAdd.Enabled = (Len(txtName.Text) > 0)
End Sub
List Box Control Scenario 2: Creating Multiple-Column List Boxes
To create a multiple-column, multiple-selection list box, you need to set both the Columns and the MultiSelect properties of a list box. In the following example, these properties are used to create such a list box.
You'll notice that when you run the application, the list box contains two columns, as shown in Figure
If you draw the list box large enough to hold all the items in one column, the second column will be empty; the other items will wrap, and horizontal scroll bars will appear automatically only if the list box is not long enough. Try resizing the top list box and adding additional list items to see how Visual Basic automatically handles multiple columns. The example uses the Selected property — a Boolean array containing the selection status of a list box — to determine which items are selected. Each entry in the array corresponds to a list item and is set to True if the item is selected, or False if it is not selected. After the user selects items from the list, each array entry is checked to see if it is set (True). If so, the entry is added to the second list, a normal single-column list box, using the AddItem method.
Set the properties for the example as indicated in the following table.
Object
Property Setting Form Caption Multiple-Column List Box Top list box Name
Columns
MultiSelect lstTop
2
2-Extended Bottom list box Name lstBottom First command button Name
Caption cmdTransfer
&Transfer Second command button Name
Caption cmdClear
C&lear Third command button Name
Caption cmdClose
&Close The MultiSelect property allows you to select a range of values in a list box. If you click the first list item, and then press SHIFT and click the last item in the range (or use the SHIFT+ DOWN ARROW keys), all the items in the range are selected.
Events in the Multiple-Column List Box Application
Add code to the Form_Load procedure to initialize the top list, 1stTop:
Private Sub Form_Load ()
lstTop.AddItem "Paris"
lstTop.AddItem "New Orleans"
lstTop.AddItem "San Francisco"
lstTop.AddItem "Chicago"
lstTop.AddItem "Seattle"
lstTop.AddItem "Toronto"
lstTop.AddItem "New York"
lstTop.AddItem "Tbilisi"
lstTop.AddItem "Moscow"
lstTop.AddItem "Portland"
' Select a couple of items.
1stTop.Selected(0) = True
1stTop.Selected(1) = True
End Sub
Note You can add items to list boxes without repeatedly using the AddItem method by typing items in the List property of the Properties window. After entering each item, press CTRL+ENTER to go to the next line. This allows you to type multiple entries in a multiple-column list box.
Add the following code to the 1stTop_DblClick event procedure:
Private Sub 1stTop_DblClick ()
cmdTransfer.Value = True ' Press transfer button.
End Sub
Add the following code to the Click event procedure for the Transfer command button:
Private Sub cmdTransfer_Click ()
For n = 0 To (lstTop.ListCount - 1)
' If selected, add to list.
If lstTop.Selected(n) = True Then
lstBottom.AddItem lstTop.List(n)
End If
Next
cmdClear.Enabled = True
End Sub
Notice how the array index values start from 0 and go to ListCount -1.
Add the following code to the Click event procedure for the Clear command button:
Private Sub cmdClear_Click ()
lstBottom.Clear
cmdClear.Enabled = False
End Sub
Add the following code to the Click event procedure for the Close command button.
Private Sub cmdClose_Click ()
Unload Me
End Sub
Using the Option Button Control
Option button controls are used to display options, usually in option button groups, from which the user can choose one.
Figure:The option button control
While option button controls and check box controls may appear to function similarly, there is an important difference: when a user selects an option button, the other option button controls in the same group are automatically unavailable. In contrast, any number of check box controls can be selected.
Creating Option Button Groups
You group option button controls by drawing them inside a container such as a frame control, a picture box control, or a form. At run time, the user may select a single option button from each distinct group of option buttons. For example, if you add option buttons to a form and option buttons to a frame control on the form, you have created two distinct groups of option buttons. Figure:Creating option button groups
All option buttons that are added directly to the form become one group. To add additional groups, you need to place them inside frame or picture box controls.
To group option button controls in a frame or picture box, draw the frame or picture box first, and then draw the option button controls inside. At design time, option buttons contained within a frame or picture box control may be selected and moved as a single unit.
To select multiple controls contained inside a frame control, a picture box control, or a form, hold down the CTRL key while using the mouse to draw a box around the controls.
Selecting an Option Button at Run Time
An option button may be selected at run time in several ways: clicking it with the mouse, using the TAB key to shift the focus to the control, using the TAB key to select a group of option button controls and then using the arrow keys to select one within the group, creating a shortcut key in the option button's caption, or, by setting its Value property to True in code. The Click Event
When an option button is selected, its Click event is triggered. Depending upon the functionality of your application, you may or may not need to respond to this event. Responding to this event is useful when you want to update a label control's caption to provide the user with information about the option that has been selected, for example. The Value Property
The Value property of the option button control indicates whether the option button is selected. When selected, the value is changed to True. You can select an option button in code by setting its Value property. For example:
optPentium.Value = True
To make an option button the default within an option button group, set the Value property at design time using the Properties window or at run time in code, as shown above.
When you present the user with a dialog box containing option buttons, you are asking them to select options that will determine what your application will do next. You can use the Value property of each of the option button controls to determine which option or options were selected and then respond accordingly.
Creating Keyboard Shortcuts You can use the Caption property to create access key shortcuts for your option buttons by adding an ampersand (&) before the letter you want to use as the access key. For example, to create an access key for the option button caption "Pentium" you add an ampersand before the letter "P": "&Pentium". At run time, the letter "P" will be underlined and the user can select the command button by simultaneously pressing ALT+P.
Note To include an ampersand in a caption without creating an access key, include two ampersands (&&). A single ampersand is displayed in the caption and no characters are underlined.
Disabling an Option Button To disable an option button, set its Enabled property to False. At run time, the option button will appear dimmed, meaning that it is unavailable.
Using the Picture Box Control
The picture box control is used to display graphics, to act as a container for other controls, and to display output from graphics methods or text using the Print method.
Figure:The picture box control
The picture box control is similar to the image control in that each can be used to display graphics in your application — each supports the same graphic formats. The picture box control, however, contains functionality which the image control does not, for example: the ability to act as a container for other controls and support for graphics methods.
Loading a Graphic Into the Picture Box Control
Pictures can be loaded into the picture box control at design time by selecting the Picture property from the control's Properties window, or at run time by using the Picture property and the LoadPicture function.
Set Picture1.Picture = _
LoadPicture("c:\Windows\Winlogo.cur", vbLPLarge, vbLPColor)
You may want to use icon (.ico) and cursor (.cur) files containing separate images at different sizes and color depths to support a range of display devices. The LoadPicture function's settings allow you to select images with specific color depths and sizes from an .ico or .cur file. In cases where an exact match to the requested settings isn't available, LoadPicture loads the image with the closest match available.
To clear the graphic from the picture box control, use the LoadPicture function without specifying a file name. For example:
Set Picture1.Picture = LoadPicture
This will clear the picture box control even if a graphic was loaded into the Picture property at design time.
Using the Clipboard
You can also add a graphic to a picture box control at design time by pasting it from another application. For example, you may want to add a bitmap image that was created in Windows Paint. Simply copy the image to the clipboard, select the picture box control, and either use the keyboard shortcut CTRL+V or the Paste command from the Edit menu. Sizing a Picture
By default, graphics are loaded into a picture box at their original size, meaning that if the graphic is larger than the control, the image will be clipped — the picture box control does not provide scroll bars. To make a picture box control automatically resize to display an entire graphic, set its AutoSize property to True. The control will then size to the graphic — growing or shrinking.
Unlike the image control, the picture box control cannot stretch the image to fit the size of the control.
Using the Picture Box Control as a Container
You can use the picture box control as a container for other controls. For example, since the picture box can be placed inside the internal area of a MDI form, it is often used to manually create a toolbar or status bar.
Graphics Methods
Picture boxes, like forms, can be used to receive the output of graphics methods such as Circle, Line, and Point. For example, you can use the Circle method to draw a circle in a picture box by setting the control's AutoRedraw property to True.
Picture1.AutoRedraw = True
Picture1.Circle (1200, 1000), 750
Setting AutoRedraw to True allows the output from these methods to be drawn to the control and automatically redrawn when the picture box control is resized or redisplayed after being hidden by another object.
Using the Print Method
You can use the picture box control to output text by using the Print method and setting the AutoRedraw property to True. For example:
Picture1.Print "A text string"
When using the Print method you can also modify the font style and size or use the CurrentX, CurrentY, Height, and Width properties to align text within a picture box.
Using the RemoteData Control
The Remote Data control implements data access by using the Microsoft Remote Data Objects. This technology gives you seamless access to many standard database formats and allows you to create data-aware applications without writing any code. The Remote Data control is suited to larger client-server databases, including Open Database Connectivity (ODBC) databases such as Microsoft SQL Server and Oracle.
It should be noted that the RemoteData control does not work with Image data types.
Note Both the Remote Data control and Data control are included with Visual Basic for backward compatibility. However, because of the flexibility of ActiveX Data Objects (ADO), it's recommended that you create new database applications using the ADO Data control. For more details, see "Using the ADO Data Control.".
The Data control, Remote Data control, and the ADO Data control are all conceptually similar: all three are "data controls" that connect a data source to a data-bound control. All three also share the same appearance — a set of four buttons that allow the user to go immediately to the beginning of the recordset, end of the recordset, or scroll backwards and forwards through the recordset.
Creating a Simple Database Application with the Remote Data Control
The following procedures create a simple database application using the Data control
To create a simple database application with the Remote Data Control
The following data-related properties can be set at design time. The list suggests a logical order for setting the properties:
The shape control is used to create the following predefined shapes on forms, frames, or picture boxes: rectangle, square, oval, circle, rounded rectangle, or rounded square.
Figure:The shape control
You can set the shape style, color, fill style, border color, and border style of any of the shapes you draw on a form.
For simple uses, the shape control allows you to create a variety of shapes without writing any code. For more advanced functionality you need to use the Line and Circle methods.
Predefined Shapes
The Shape property of the shape control provides you with six predefined shapes. The following table lists all the predefined shapes, their values and equivalent Visual Basic constants:
Shape Style Constant Rectangle 0 vbShapeRectangle Square 1 vbShapeSquare Oval 2 vbShapeOval Circle 3 vbShapeCircle Rounded Rectangle 4 vbShapeRoundedRectangle Rounded Square 5 vbShapeRoundedSquare
Figure Predefined shapes
Fill and Line Styles
You can use the FillStyle and BorderStyle properties to set the fill style and border style of any of the shapes you draw on a form.
The FillStyle property, like the Style property, provides you with a number of predefined fill style patterns. These include: Solid, Transparent, Horizontal Line, Vertical Line, Upward Diagonal, Downward Diagonal, Cross, and Diagonal Cross.
The BorderStyle property provides you with a number of predefined border styles. These include: Transparent, Solid, Dash, Dot, Dash-Dot, Dash-Dot-Dot, and Inside Solid.
Setting Color Attributes
The BackColor and FillColor properties allow you to add color to the shape and its border.
At design time, you can set the fill or border colors by choosing either property from the Properties window of the shape control and then selecting from the available palette or system colors.
To set colors at run time, use the Visual Basic color constants (vbGreen, for example) or the system color constants (vbWindowBackground, for example) or the RGB function to specify fill colors.
Note When the FillStyle or BackStyle properties are set to 1 (Transparent), the FillColor and BackColor properties are ignored.
Drawing Shapes on a Form
You can use the shape control to draw rectangles (regular or rounded corners), squares (regular or rounded corners), ovals, and circles on a form.
To draw a shape on a form
Using the Text Box Control
The text box control is used to display information entered by the user at run time, or assigned to the Text property of the control at design or run time.
Figure :The text box control
In general, the text box control should be used for editable text, although you can make it read-only by setting its Locked property to True. Text boxes also allow you to display multiple lines, to wrap text to the size of the control, and to add basic formatting.
The Text Property
Text entered into the text box control is contained in the Text property. By default, you can enter up to 2048 characters in a text box. If you set the MultiLine property of the control to True, you can enter up to 32K of text.
Formatting Text
When text exceeds the boundaries of the control, you can allow the control to automatically wrap text by setting the MultiLine property to True and add scroll bars by setting the ScrollBars property to add either a horizontal or vertical scroll bar, or both. Automatic text wrapping will be unavailable, however, if you add a horizontal scroll bar because the horizontal edit area is increased by the presence of the scroll bar.
When the MultiLine property is set to True, you can also adjust the alignment of the text to either Left Justify, Center, or Right Justify. The text is left-justified by default. If the MultiLine property is False, setting the Alignment property has no effect.
Selecting Text
You can control the insertion point and selection behavior in a text box with the SelStart, SelLength and SelText properties.
Creating a Password Text Box
A password box is a text box that allows a user to type in his or her password while displaying placeholder characters, such as asterisks. Visual Basic provides two text box properties, PasswordChar and MaxLength, which make it easy to create a password text box.
PasswordChar specifies the character displayed in the text box. For example, if you want asterisks displayed in the password box, you specify * for the PasswordChar property in the Properties window. Regardless of what character a user types in the text box, an asterisk is displayed
Figure:Password example
With MaxLength, you determine how many characters can be typed in the text box. After MaxLength is exceeded, the system emits a beep and the text box does not accept any further characters.
Canceling Keystrokes in a Text Box
You can use the KeyPress event to restrict or transform characters as they are typed. The KeyPress event uses one argument, keyascii. This argument is an integer that represents the numeric (ASCII) equivalent of the character typed in the text box.
The next example demonstrates how to cancel keystrokes as they are typed. If the character typed is not within the specified range, the procedure cancels it by setting KeyAscii to 0. The text box for this example is named txtEnterNums, and the procedure prevents the text box from receiving any characters other than digits. Compare KeyAscii directly to the numeric (Asc) values of various characters.
Private Sub txtEnterNums_KeyPress (KeyAscii As Integer)
If KeyAscii < Asc("0") Or KeyAscii > Asc("9") Then
KeyAscii = 0 ' Cancel the character.
Beep ' Sound error signal.
End If
End Sub
Creating a Read-Only Text Box editing text box contents. Set the Locked property to True to allow users to scroll and highlight text in a text box without allowing changes. With the Locked property set to True, a Copy command will work in a text box, but Cut and Paste commands will not. The Locked property only affects user interaction at run time. You can still change text box contents programmatically at run time by changing the Text property of the text box.
Printing Quotation Marks in a String
Sometimes quotation marks (" ") appear in a string of text.
She said, "You deserve a treat!"
Because strings assigned to a variable or property are surrounded by quotation marks (" "), you must insert an additional set of quotation marks for each set to display in a string. Visual Basic interprets two quotation marks in a row as an embedded quotation mark.
For example, to create the preceding string, use the following code:
Text1.Text = "She said, ""You deserve a treat!"" "
To achieve the same effect, you can use the ASCII character (34) for a quotation mark:
Text1.Text = "She said, " & Chr(34) + "You deserve a treat!" & Chr(34)
Using the Timer Control
Timer controls respond to the passage of time. They are independent of the user, and you can program them to take actions at regular intervals. A typical response is checking the system clock to see if it is time to perform some task. Timers also are useful for other kinds of background processing.
Figure:The timer control
Each timer control has an Interval property that specifies the number of milliseconds that pass between one timer event to the next. Unless it is disabled, a timer continues to receive an event (appropriately named the Timer event) at roughly equal intervals of time.
The Interval property has a few limitations to consider when you're programming a timer control:
Note The word "timer" is used in several ways in Visual Basic, each closely related to the workings of the timer control. In addition to the control name and control type, "timer" is used in the Timer event and the Timer function.
Placing a Timer Control on a Form
Placing a timer control on a form is like drawing any other control: Click the timer button in the toolbox and drag it onto a form.
The timer appears on the form at design time only so you can select it, view its properties, and write an event procedure for it. At run time, a timer is invisible and its position and size are irrelevant.
Initializing a Timer Control
A timer control has two key properties.
Property Setting Enabled If you want the timer to start working as soon as the form loads, set it to True. Otherwise, leave this property set to False. You might choose to have an outside event (such as a click of a command button) start operation of the timer. Interval Number of milliseconds between timer events. Note that the Enabled property for the timer is different from the Enabled property for other objects. With most objects, the Enabled property determines whether the object can respond to an event caused by the user. With the Timer control, setting Enabled to False suspends timer operation.
Remember that the Timer event is periodic. The Interval property doesn't determine "how long" as much as it determines "how often." The length of the interval should depend on how much precision you want. Because there is some built-in potential for error, make the interval one-half the desired amount of precision.
Note The more often a timer event is generated, the more processor time is used in responding to the event. This can slow down overall performance. Don't set a particularly small interval unless you need it.
Timer Control Scenario: Responding to the Timer Event
When a timer control's interval elapses, Visual Basic generates the Timer event. Typically, you respond to this event by checking some general condition, such as the system clock.
A digital clock is a very simple but very useful application involving a timer control. Once you understand how the application works, you can enhance it to work as an alarm clock, stopwatch, or other timing device.
The Digital Clock application includes a timer and a label with a border. At design time, the application looks like Figure
At run time, the timer is invisible. The following table lists the property settings for the Digital Clock application.
Control Property Setting Label1 BorderStyle Fixed Single Timer1 Interval 500 (half a second) Timer1 Enabled True
The only procedure in this application is an event procedure for the timer: Private Sub Timer1_Timer ()
If lblTime.Caption <> CStr(Time) Then
lblTime.Caption = Time
End If
End Sub
The procedure displays the system time by calling the intrinsic Time function. This function returns a Variant containing the current time as a date/time value (VarType 7). When you assign it to a string variable or property, such as the Caption property in this case, Visual Basic converts it to a string using the time format specified in the Control Panel. If you want to display it using a different format, you can use the Format function.
Multiple-Document Interface (MDI) Applications
The multiple-document interface (MDI) allows you to create an application that maintains multiple forms within a single container form. Applications such as Microsoft Excel and Microsoft Word for Windows have multiple-document interfaces.
An MDI application allows the user to display multiple documents at the same time, with each document displayed in its own window. Documents or child windows are contained in a parent window, which provides a workspace for all the child windows in the application. For example, Microsoft Excel allows you to create and display multiple-document windows of different types. Each individual window is confined to the area of the Excel parent window. When you minimize Excel, all of the document windows are minimized as well; only the parent window's icon appears in the task bar.
A child form is an ordinary form that has its MDIChild property set to True. Your application can include many MDI child forms of similar or different types.
Child forms displayed within the workspace of the MDI form
Note Your application can also include standard, non-MDI forms that are not contained in the MDI form. A typical use of a standard form in an MDI application is to display a modal dialog box. An MDI form is similar to an ordinary form with one restriction. You can't place a control directly on a MDI form unless that control has an Align property (such as a picture box control) or has no visible interface (such as a timer control).
Creating an MDI Application
Use the following procedure to create an MDI form and its child forms.
To create an MDI application
At design time, child forms are not restricted to the area inside the MDI form. You can add controls, set properties, write code, and design the features of child forms just as you would with any other Visual Basic form.
You can determine whether a form is an MDI child by looking at its MDIChild property, or by examining the Project Explorer. If the form's MDIChild property is set to True, it is a child form. Visual Basic displays special icons in the Project Explorer for the MDI and MDI child forms
Icons in the Project Explorer identify MDI child, standard, and MDI forms
Run-Time Features of MDI Forms At run time, an MDI form and all of its child forms take on special characteristics
The MDI NotePad Application
The MDI NotePad sample application is a simple text editor similar to the NotePad application included with Microsoft Windows. The MDI NotePad application, however, uses a multiple-document interface (MDI). At run time, when the user requests a new document (implemented with the New command on the application's File menu), the application creates a new instance of the child form. This allows the user to create as many child forms, or documents, as necessary.
To create a document-centered application in Visual Basic, you need at least two forms — an MDI form and a child form. At design time, you create an MDI form to contain the application and a single child form to serve as a template for the application's document.
To create your own MDI NotePad application
In addition to the basics of form design, you need to think about the beginning and end of your application. There are several techniques available for determining what the user will see when your application starts. It's also important to be aware of the processes that occur when an application is unloaded.
Setting the Startup Form
By default, the first form in your application is designated as the startup form. When your application starts running, this form is displayed (so the first code to execute is the code in the Form_Initialize event for that form). If you want a different form to display when your application starts, you must change the startup form.
To change the startup form
Sometimes you might want your application to start without any form initially loaded. For example, you might want to execute code that loads a data file and then displays one of several different forms depending on what is in the data file. You can do this by creating a Sub procedure called Main in a standard module, as in the following example:
Sub Main()
Dim intStatus As Integer
' Call a function procedure to check user status.
intStatus = GetUserStatus
' Show a startup form based on status.
If intStatus = 1 Then
frmMain.Show
Else
frmPassword.Show
End If
This procedure must be a Sub procedure, and it cannot be in a form module. To set the Sub Main procedure as the startup object, from the Project menu, choose Project Properties, select the General tab, and select Sub Main from the Startup Object box.
Displaying a Splash Screen on Startup If you need to execute a lengthy procedure on startup, such as loading a large amount of data from a database or loading several large bitmaps, you might want to display a splash screen on startup. A splash screen is a form, usually displaying information such as the name of the application, copyright information, and a simple bitmap. The screen that is displayed when you start Visual Basic is a splash screen.
To display a splash screen, use a Sub Main procedure as your startup object and use the Show method to display the form:
Private Sub Main()
' Show the splash screen.
frmSplash.Show
' Add your startup procedures here.
…
' Show the main form and unload the splash screen.
frmMain.Show
Unload frmSplash
End Sub
The splash screen occupies the user's attention while your startup procedures are executing, giving the illusion that the application is loading faster. When the startup procedures are completed, you can load your first form and unload the splash screen.
In designing a splash screen, it's a good idea to keep it simple. If you use large bitmaps or a lot of controls, the splash screen itself may be slow to load.
Ending an Application An event-driven application stops running when all its forms are closed and no code is executing. If a hidden form still exists when the last visible form is closed, your application will appear to have ended (because no forms are visible), but will in fact continue to run until all the hidden forms are closed. This situation can arise because any access to an unloaded form's properties or controls implicitly loads that form without displaying it.
The best way to avoid this problem when closing your application is to make sure all your forms are unloaded. If you have more than one form, you can use the Forms collection and the Unload statement. For example, on your main form you could have a command button named cmdQuit that lets a user exit the program. If your application has only one form, the Click event procedure could be as simple as this:
Private Sub cmdQuit_Click ()
Unload Me
End Sub
If your application uses multiple forms, you can unload the forms by putting code in the Unload event procedure of your main form. You can use the Forms collection to make sure you find and close all your forms. The following code uses the forms collection to unload all forms:
Private Sub Form_Unload (Cancel As Integer)
Dim i as integer
' Loop through the forms collection and unload
' each form.
For i = Forms.Count – 1 to 0 Step - 1
Unload Forms(i)
Next
End Sub
There may be cases where you need to end your application without regard for the state of any existing forms or objects. Visual Basic provides the End statement for this purpose.
The End statement ends an application immediately: no code after the End statement is executed, and no further events occur. In particular, Visual Basic will not execute the QueryUnload, Unload or Terminate event procedures for any forms. Object references will be freed, but if you have defined your own classes, Visual Basic will not execute the Terminate events of objects created from your classes.
In addition to the End statement, the Stop statement halts an application. However, you should use the Stop statement only while debugging, because it does not free references to objects
Creating Menus with the Menu Editor
You can use the Menu Editor to create new menus and menu bars, add new commands to existing menus, replace existing menu commands with your own commands, and change and delete existing menus and menu bars. To display the Menu Editor
From the Tools menu, choose Menu Editor.
–or–
Click the Menu Editor button on the toolbar.
The Menu Editor
While most menu control properties can be set using the Menu Editor, all menu properties are available in the Properties window. The two most important properties for menu controls are:
Using the List Box in the Menu Editor
The menu control list box (the lower portion of the Menu Editor) lists all the menu controls for the current form. When you type a menu item in the Caption text box, that item also appears in the menu control list box. Selecting an existing menu control from the list box allows you to edit the properties for that control.
For example, Figure 6.7 shows the menu controls for a File menu in a typical application. The position of the menu control in the menu control list box determines whether the control is a menu title, menu item, submenu title, or submenu item:
To create menu controls in the Menu Editor
A separator bar is displayed as a horizontal line between items on a menu. On a menu with many items, you can use a separator bar to divide items into logical groups. For example, the File menu in Visual Basic uses separator bars to divide its menu items into three groups
Separator bars
To create a separator bar in the Menu Editor
Assigning Access Keys and Shortcut Keys Access keys allow the user to open a menu by pressing the ALT key and typing a designated letter. Once a menu is open, the user can choose a control by pressing the letter (the access key) assigned to it. For example, ALT+E might open the Edit menu, and P might select the Paste menu item.
Access keys
To assign an access key to a menu control in the Menu Editor
Shortcut keys run a menu item immediately when pressed. Frequently used menu items may be assigned a keyboard shortcut, which provides a single-step method of keyboard access, rather than a three-step method of pressing ALT, a menu title access character, and then a menu item access character. Shortcut key assignments include function key and control key combinations, such as CTRL+F1 or CTRL+A.
Shortcut keys
To assign a shortcut key to a menu item
Each menu you create can include up to five levels of submenus. A submenu branches off another menu to display its own menu items. You may want to use a submenu when:
To create a submenu
A menu control array is a set of menu items on the same menu that share the same name and event procedures. Use a menu control array to:
To create a menu control array in the Menu Editor
Creating and Modifying Menus at Run Time
The menus you create at design time can also respond dynamically to run-time conditions. For example, if a menu item action becomes inappropriate at some point, you can prevent users from selecting that menu item by disabling it. In the MDI NotePad application, for example, if the clipboard doesn't contain any text, the Paste menu item is dimmed on the Edit menu, and users cannot select it.
You can also dynamically add menu items, if you have a menu control array. This is described in "Adding Menu Controls at Run Time," later in this topic.
You can also program your application to use a check mark to indicate which of several commands was last selected. For example, the Options, Toolbar menu item from the MDI NotePad application displays a check mark if the toolbar is displayed. Other menu control features described in this section include code that makes a menu item visible or invisible and that adds or deletes menu items.
Enabling and Disabling Menu Commands All menu controls have an Enabled property, and when this property is set to False, the menu is disabled and does not respond to user actions. Shortcut key access is also disabled when Enabled is set to False.
A disabled menu item
For example, this statement disables the Paste menu item on the Edit menu of the MDI NotePad application:
mnuEditPaste.Enabled = False
Disabling a menu title in effect disables the entire menu, because the user cannot access any menu item without first clicking the menu title. For example, the following code would disable the Edit menu of the MDI Notepad application:
mnuEdit.Enabled = False
Displaying a Check Mark on a Menu Control
Using the Checked property, you can place a check mark on a menu to:
You create check marks in Visual Basic with the Checked property. Set the initial value of the Checked property in the Menu Editor by selecting the check box labeled Checked. To add or remove a check mark from a menu control at run time, set its Checked property from code. For example:
Private Sub mnuOptions_Click ()
' Set the state of the check mark based on
' the Visible property.
mnuOptionsToolbar.Checked = picToolbar.Visible
End Sub
Making Menu Controls Invisible In the Menu Editor, you set the initial value of the Visible property for a menu control by selecting the check box labeled Visible. To make a menu control visible or invisible at run time, set its Visible property from code. For example:
mnuFileArray(0).Visible = True ' Make the control
' visible.
mnuFileArray(0).Visible = False ' Make the control
' invisible.
When a menu control is invisible, the rest of the controls in the menu move up to fill the empty space. If the control is on the menu bar, the rest of the controls on the menu bar move left to fill the space.
Note Making a menu control invisible effectively disables it, because the control is inaccessible from the menu, access or shortcut keys. If the menu title is invisible, all the controls on that menu are unavailable.
Adding Menu Controls at Run Time
Menu control array elements created and displayed at run time
You must use a control array to create a control at run time. Because the mnuRecentFile menu control is assigned a value for the Index property at design time, it automatically becomes an element of a control array — even though no other elements have yet been created.
When you create mnuRecentFile(0), you actually create a separator bar that is invisible at run time. The first time a user saves a file at run time, the separator bar becomes visible, and the first file name is added to the menu. Each time you save a file at run time, additional menu controls are loaded into the array, making the menu grow.
Controls created at run time can be hidden by using the Hide method or by setting the control's Visible property to False. If you want to remove a control in a control array from memory, use the Unload statement.
Writing Code for Menu Controls
When the user chooses a menu control, a Click event occurs. You need to write a Click event procedure in code for each menu control. All menu controls except separator bars (and disabled or invisible menu controls) recognize the Click event.
The code that you write in a menu event procedure is no different than that which you would write in any other control's event procedure. For example, the code in a File, Close menu's Click event might look like this:
Sub mnuFileClose_Click()
Unload Me
End Sub
Visual Basic displays a menu automatically when the menu title is chosen; therefore, it is not necessary to write code for a menu title's Click event procedure unless you want to perform another action, such as disabling certain menu items each time the menu is displayed.
Note At design time, the menus you create are displayed on the form when you close the Menu Editor. Choosing a menu item on the form displays the Click event procedure for that menu control.
Displaying Pop-up Menus A pop-up menu is a floating menu that is displayed over a form, independent of the menu bar. The items displayed on the pop-up menu depend on where the pointer was located when the right mouse button was pressed; therefore, pop-up menus are also called context menus. In Microsoft Windows 95 or later systems, you activate context menus by clicking the right mouse button.
Any menu that has at least one menu item can be displayed at run time as a pop-up menu. To display a pop-up menu, use the PopupMenu method. This method uses the following syntax:
[object.]PopupMenu menuname [, flags [,x [, y [, boldcommand ]]]]
For example, the following code displays a menu named mnuFile when the user clicks a form with the right mouse button. You can use the MouseUp or MouseDown event to detect when the user clicks the right mouse button, although the standard is to use the MouseUp event:
Private Sub Form_MouseUp (Button As Integer, Shift As _
Integer, X As Single, Y As Single)
If Button = 2 Then ' Check if right mouse button
' was clicked.
PopupMenu mnuFile ' Display the File menu as a
' pop-up menu.
End If
End Sub
Any code following a call to the PopupMenu method is not run until the user selects an item in the menu or cancels the menu.
Note Only one pop-up menu can be displayed at a time. While a pop-up menu is displayed, calls to the PopupMenu method are ignored. Calls to the PopupMenu method are also ignored whenever a menu control is active.
Often you want a pop-up menu to access options that are not usually available on the menu bar. To create a menu that will not display on the menu bar, make the top-level menu item invisible at design time (make sure the Visible check box in the Menu Editor is not checked). When Visual Basic displays a pop-up menu, the Visible property of the specified top-level menu is ignored.
The Flags Argument You use the flags argument in the PopupMenu method to further define the location and behavior of a pop-up menu. The following table lists the flags available to describe a pop-up menu's location.
Location constants Description vbPopupMenuLeftAlign Default. The specified x location defines the left edge of the pop-up menu. vbPopupMenuCenterAlign The pop-up menu is centered around the specified x location. vbPopupMenuRightAlign The specified x location defines the right edge of the pop-up menu. The following table lists the flags available to describe a pop-up menu's behavior.
Behavior constants Description vbPopupMenuLeftButton Default. The pop-up menu is displayed when the user clicks a menu item with the left mouse button only. vbPopupMenuRightButton The pop-up menu is displayed when the user clicks a menu item with either the right or left mouse button To specify a flag, you combine one constant from each group using the Or operator. The following code displays a pop-up menu with its top border centered on a form when the user clicks a command button. The pop-up menu triggers Click events for menu items that are clicked with either the right or left mouse button.
Private Sub Command1_Click ()
' Dimension X and Y variables.
Dim xloc, yloc
' Set X and Y variables to center of form.
xloc = ScaleWidth / 2
yloc = ScaleHeight / 2
' Display the pop-up menu.
PopupMenu mnuEdit, vbPopupMenuCenterAlign Or _
vbPopupMenuRightButton, xloc, yloc
End Sub
Menus in MDI Applications
In an MDI application, the menus for each child are displayed on the MDI form, rather than on the child forms themselves. When a child form has the focus, that child's menu (if any) replaces the MDI form's menu on the menu bar. If there are no child forms visible, or if the child with the focus does not have a menu, the MDI form's menu is displayed
It is common for MDI applications to use several sets of menus. When the user opens a document, the application displays the menu associated with that type of document. Usually, a different menu is displayed when no child forms are visible. For example, when there are no files open, Microsoft Excel displays only the File and Help menus. When the user opens a file, other menus are displayed (File, Edit, View, Insert, Format, Tools, Data, Window, and so on).
Creating Menus for MDI Applications
You can create menus for your Visual Basic application by adding menu controls to the MDI form and to the child forms. One way to manage the menus in your MDI application is to place the menu controls you want displayed all of the time, even when no child forms are visible, on the MDI form. When you run the application, the MDI form's menu is automatically displayed when there are no child forms visible
The MDI form menu is displayed when no child forms are loaded
Place the menu controls that apply to a child form on the child form. At run time, as long as there is at least one child form visible, these menu titles are displayed in the menu bar of the MDI form.
Some applications support more than one type of document. For example, in Microsoft Access, you can open tables, queries, forms, and other document types. To create an application such as this in Visual Basic, use two child forms. Design one child with menus that perform spreadsheet tasks and the other with menus that perform charting tasks.
At run time, when an instance of a spreadsheet form has the focus, the spreadsheet menu is displayed, and when the user selects a chart, that form's menu is displayed. If all the spreadsheets and charts are closed, the MDI form's menu is displayed. For more information on creating menus, see "Using Menus in Your Application" earlier in this chapter.
Creating a Window Menu
Most MDI applications (for example, Microsoft Word for Windows and Microsoft Excel) incorporate a Window menu. This is a special menu that displays the captions of all open child forms, as shown in Figure 6.15. In addition, some applications place commands on this menu that manipulate the child windows, such as Cascade, Tile, and Arrange Icons.
The Window menu displays the name of each open child form
Any menu control on an MDI form or MDI child form can be used to display the list of open child forms by setting the WindowList property for that menu control to True. At run time, Visual Basic automatically manages and displays the list of captions and displays a check mark next to the one that had the focus most recently. In addition, a separator bar is automatically placed above the list of windows.
To set the WindowList property
Arranging Child Forms
As was mentioned earlier, some applications list actions such as Tile, Cascade, and Arrange Icons on a menu, along with the list of open child forms. Use the Arrange method to rearrange child forms in the MDI form. You can display child forms as cascading, as horizontally tiled, or as child form icons arranged along the lower portion of the MDI form. The following example shows the Click event procedures for the Cascade, Tile, and Arrange Icons menu controls.
Private Sub mnuWCascade_Click ()
' Cascade child forms.
frmMDI.Arrange vbCascade
End Sub
Private Sub mnuWTile_Click ()
' Tile child forms (horizontal).
frmMDI.Arrange vbTileHorizontal
End Sub
Private Sub mnuWArrange_Click ()
' Arrange all child form icons.
frmMDI.Arrange vbArrangeIcons
End Sub
Note The intrinsic constants vbCascade, vbTileHorizontal, and vbArrangeIcons are listed in the Visual Basic (VB) object library of the Object Browser.
When you tile or cascade child forms that have a fixed border style, each child form is positioned as if it had a sizable border. This can cause child forms to overlap.
Creating a Toolbar
The toolbar (also called a ribbon or control bar) has become a standard feature in many Windows-based applications. A toolbar provides quick access to the most frequently used menu commands in an application. Creating a toolbar is easy and convenient using the toolbar control, which is available with the Professional and Enterprise editions of Visual Basic. If you are using the Learning Edition of Visual Basic, you can create toolbars manually as described in "Negotiating Menu and Toolbar Appearance" later in this chapter. The following example demonstrates creating a toolbar for an MDI application; the procedure for creating a toolbar on a standard form is basically the same.
To manually create a toolbar
Toolbars are used to provide the user with a quick way to access some of the application's commands. For example, the first button on the toolbar in Figure 6.16 is a shortcut for the File New command. There are now three places in the MDI NotePad sample application where the user can request a new file:
' This module is in a public procedure.
Public Sub FileNew ()
Dim frmNewPad As New frmNotePad
frmNewPad.Show
End Sub
' The user chooses New on the child form File menu.
Private Sub mnuchildFileNew_Click ()
FileNew
End Sub
' The user chooses New on the MDI form File menu.
Private Sub mnumdiFileNew_Click ()
frmNotePad.FileNew
End Sub
' The user clicks the File New button on the toolbar.
Private Sub btnFileNew_Click ()
frmNotePad.FileNew
End Sub
Negotiating Menu and Toolbar Appearance When an object supplied by another application is activated on a form, there are a number of ways that object's menus and toolbars may appear on the container form; therefore, you need to specify how they will be displayed. This process is called user-interface negotiation because Visual Basic and the object you have linked or embedded must negotiate for space in the container form.
Controlling Menu Appearance
You can determine whether a linked or embedded object's menu will appear in the container form by setting a form's NegotiateMenus property. If the child form's NegotiateMenus property is set to True (default) and the container has a menu bar defined, the object's menus are placed on the container's menu bar when the object is activated. If the container has no menu bar, or the NegotiateMenus property is set to False, the object's menus will not appear when it is activated.
Note The NegotiateMenus property does not apply to MDI Forms.
Controlling Toolbar Appearance
The MDI form's NegotiateToolbars property determines whether the linked or embedded object's toolbars will be floating palettes or placed on the parent form. This behavior does not require toolbars to be present on the MDI parent form. If the MDI form's NegotiateToolbars property is True, the object's toolbar appears on the MDI parent form. If NegotiateToolbars is False, the object's toolbar will be a floating palette.
Note The NegotiateToolbars property applies only to MDI forms.
If an MDI form includes a toolbar, it is usually contained in a picture box control on the parent form. The picture box's Negotiate property determines whether the container's toolbar is still displayed or is replaced by the object's toolbar when activated. If Negotiate is True, the object's toolbar is displayed in addition to the container's toolbar. If Negotiate is False, the object's toolbar replaces the container's toolbar.
Note Menu and toolbar negotiation will occur only for insertable objects that support in-place activation. For more information on in-place activation, see "Programming with ActiveX Components."
You can see how these three properties interact by using the following procedure.
To perform menu and toolbar negotiation
The easiest way to add a dialog box to your application is to use a predefined dialog, because you don't have to worry about designing, loading, or showing the dialog box. However, your control over its appearance is limited. Predefined dialog boxes are always modal.
The following table lists the functions you can use to add predefined dialog boxes to your Visual Basic application.
Use this function To do this InputBox function Display a command prompt in a dialog box, and return whatever is entered by the user. MsgBox function Display a message in a dialog box, and return a value indicating the command button was clicked by the user. Prompting for Input with InputBox
Use the InputBox function to solicit data from the user. This function displays a modal dialog box that asks the user to enter some data. The text input box
A dialog box using the InputBox function
FileName = InputBox("Enter file to open:", "File Open")
Note Remember that when you use the InputBox function, you have little control over the components of the dialog box. You can change only the text in the title bar, the command prompt displayed to the user, the position of the dialog box on the screen, and whether or not it displays a Help button.
Displaying Information with MsgBox
Use the MsgBox function to get yes or no responses from users, and to display brief messages, such as errors, warnings, or alerts in a dialog box. After reading the message, the user chooses a button to close the dialog box.
An error message dialog box created using the MsgBox function MsgBox "Error encountered while trying to open file, _
please retry.", vbExclamation, "Text Editor"
Using Forms as Custom Dialog Boxes
A custom dialog box is a form you create containing controls — including command buttons, option buttons, and text boxes — that lets the user supply information to the application. You customize the appearance of the form by setting property values. You also write code to display the dialog box at run time.
To create a custom dialog box, you can start with a new form or customize an existing dialog box. Over time, you can build up a collection of dialog boxes that can be used in many applications
To customize an existing dialog box
To create a new dialog box
Adding a Title A dialog box should always have a title that identifies it. To create a title, set the form's Caption property to the text string that will appear in the title bar. Usually, this is done at design time using the Properties window, but you can also do this from code. For example:
frmAbout.Caption = "About"
Tip If you want to remove the title bar completely, set the form's ControlBox, MinButton, and MaxButton properties to False; set the BorderStyle to a nonsizable setting (0, 1, or 3); and set the Caption equal to an empty string (""). Setting Standard Dialog Box Properties
Generally, the user responds to a dialog box by providing information and then closing the dialog box with an OK or Cancel command button. Because a dialog box is temporary, users usually don't need to move, size, maximize, or minimize it. As a result, the sizable border style, Control menu box, Maximize button, and Minimize button that come with a new form are unnecessary on most dialog boxes.
You can remove these items by setting the BorderStyle, ControlBox, MaxButton, and MinButton properties. For example, an About dialog box might use the following property settings
Property Setting Effect BorderStyle 1 Changes the border style to fixed single, thus preventing the dialog box from being sized at run time. ControlBox False Removes the Control menu box. MaxButton False Removes the Maximize button, thus preventing the dialog box from being maximized at run time. MinButton False Removes the Minimize button, thus preventing the dialog box from being minimized at run time.
Remember that if you remove the Control menu box (ControlBox = False), you must provide the user with another way to exit the dialog box. This is commonly done by adding an OK, Cancel, or Exit command button to the dialog box and adding code in the Click event for the button that hides or unloads the dialog. Performing Multiple Actions on an Object
You often need to perform several different actions on the same object. For example, you might need to set several properties for the same object. One way to do this is to use several statements. Private Sub Form_Load()
Command1.Caption = "OK"
Command1.Visible = True
Command1.Top = 200
Command1.Left = 5000
Command1.Enabled = True
End Sub
Notice that all these statements use the same object variable, Command1. You can make this code easier to write, easier to read, and more efficient to run by using the With...End With statement.
Private Sub Form_Load()
With Command1
.Caption = "OK"
.Visible = True
.Top = 200
.Left = 5000
.Enabled = True
End With
End Sub
You can also nest With statements by placing one With...End With statement inside another With...End With statement.
Using Default Properties
Many objects have default properties. You can use default properties to simplify your code, because you don't have to refer explicitly to the property when setting its value. For an object where Value is the default property, these two statements are equivalent: object = 20
and
object.Value = 20
To see how this works, draw a command button and a text box on a form. Add the following statement to the command button's Click event:
Text1 = "hello"
Run the application and click the command button. Because Text is the default property of the text box, the text box will display the text, "hello."
Using Default Properties with Object Variables
When a reference to an object is stored in an object variable, you can still use the default property. The following code fragment demonstrates this.
Private Sub Command1_Click()
Dim obj As Object
' Place a reference to Text1 in the object
' variable.
Set obj = Text1
' Set the value of the default property (Text).
obj = "hello"
End Sub
In the code above, obj = "hello" is exactly the same as typing obj.Text = "hello".
Using Default Properties with Variants
Accessing default properties is different when an object reference is stored in a variable of type Variant, instead of in an object variable. This is because a Variant can contain data of many different types. For example, you can read the default property of Text1 using a reference in a Variant, but trying to assign the string "goodbye" to the default property doesn't work. Instead, it replaces the object reference with the string, and changes the Variant type.
To see how this works, enter the following code in the Click event of the command button from the previous example:
Private Sub Command1_Click()
Dim vnt As Variant
' Set the default property (Text) to "hello".
Text1 = "hello"
' Place a reference to Text1 in the Variant.
Set vnt = Text1
' Display the default property of Text1, and show
' that the Variant contains an object reference.
MsgBox vnt, , "IsObject? " & IsObject(vnt)
' Attempt to set the default property of Text1.
vnt = "goodbye"
MsgBox vnt, , "IsObject? " & IsObject(vnt)
End Sub
When you run the application and click the command button, you first get a message box displaying the current value of the default property of Text1, "hello," which you can verify by looking at Text1. The caption of the message box confirms that the Variant contains an object reference — that is, a reference to Text1.
When you click the OK button on the message box, "goodbye" is assigned to the Variant, destroying the reference to Text1. Another message box is then displayed, showing the contents of the Variant — which as you can see doesn't match the current value of Text1.Text.
The caption of the message box confirms that the Variant no longer contains an object reference — it now contains the string "goodbye."
Creating Arrays of Objects
You can declare and use arrays of an object type just as you declare and use an array of any data type. These arrays can be fixed-size or dynamic. Arrays of Form Variables
You can declare an array of forms with Private, Dim, ReDim, Static, or Public in the same way you declare an array of any other type. If you declare the array with the New keyword, Visual Basic automatically creates a new instance of the form for each element in the array as you use the elements in the array. Private Sub Command1_Click ()
Dim intX As Integer
Dim frmNew(1 To 5) As New Form1
For intX = 1 To 5
frmNew(intX).Show
frmNew(intX).WindowState = vbMinimized
' To create minimized forms without having them
' first appear briefly at normal size, reverse
' the order of the two lines above.
Next
End Sub
Pressing the command button to execute the code above will create five minimized instances of Form1.
Note If you look at the Task Bar, you'll see Form1 six times. The extra instance of Form1 isn't minimized — it's the one you started with.
Arrays of Control Variables
You can declare an array of controls with Private, Dim, ReDim, Static, or Public in the same way you declare an array of any other type. Unlike form arrays, however, control arrays cannot be declared with the New keyword. For example, you can declare an array to be a specific control type:
ReDim ActiveImages(10) As Image
When you declare an array to be a particular control type, you can assign only controls of that type to the array. In the case of the preceding declaration, for example, you can only assign image controls to the array — but those image controls can come from different forms.
Contrast this with the built-in Controls collection, which can contain many different types of controls — all which must be on the same form.
Alternatively, you can declare an array of generic control variables. For example, you might want to keep track of every control that was dropped onto a particular control, and not allow any control to be dropped more than once. You can do this by maintaining a dynamic array of control variables that contains references to each control that has been dropped:
Private Sub List1_DragDrop(Source As VB.Control, _
X As Single, Y As Single)
Dim intX As Integer
Static intSize As Integer
Static ctlDropped() As Control
For intX = 1 To intSize
' If the dropped control is in the array, it's
' already been dropped here once.
If ctlDropped(intX) Is Source Then
Beep
Exit Sub
End If
Next
' Enlarge the array.
intSize = intSize + 1
ReDim Preserve ctlDropped(intSize)
' Save a reference to the control that was dropped.
Set ctlDropped(intSize) = Source
' Add the name of the control to the list box.
List1.AddItem Source.Name
End Sub
This example uses the Is operator to compare the variables in the control array with the control argument. The Is operator can be used to test the identity of Visual Basic object references: If you compare two different references to the same object, the Is operator returns True.
The example also uses the Set statement to assign the object reference in the Source argument to an element in the array.
Creating Collections of Objects Collections provide a useful way to keep track of objects. Unlike arrays, Collection objects don't have to be re-dimensioned as you add and remove members.
For example, you might want to keep track of every control that was dropped onto a particular control, and not allow any control to be dropped more than once. You can do this by maintaining a Collection that contains references to each control that has been dropped:
Private Sub List1_DragDrop(Source As VB.Control, _
X As Single, Y As Single)
Dim vnt As Variant
Static colDroppedControls As New Collection
For Each vnt In colDroppedControls
' If the dropped control is in the collection,
' it's already been dropped here once.
If vnt Is Source Then
Beep
Exit Sub
End If
Next
' Save a reference to the control that was dropped.
colDroppedControls.Add Source
' Add the name of the control to the list box.
List1.AddItem Source.Name
End Sub
This example uses the Is operator to compare the object references in the colDroppedControls collection with the event argument containing the reference to the dropped control. The Is operator can be used to test the identity of Visual Basic object references: If you compare two different references to the same object, the Is operator returns True.
The example also uses the Add method of the Collection object to place a reference to the dropped control in the collection.
Unlike arrays, Collections are objects themselves. The variable colDroppedControls is declared As New, so that an instance of the Collection class will be created the first time the variable is referred to in code. The variable is also declared Static, so that the Collection object will not be destroyed when the event procedure ends.
The Visual Basic Collection Object
A collection is a way of grouping a set of related items. Collections are used in Visual Basic to keep track of many things, such as the loaded forms in your program (the Forms collection), or all the controls on a form (the Controls collection).
Visual Basic provides the generic Collection class to give you the ability to define your own collections. You can create as many Collection objects — that is, instances of the Collection class — as you need. You can use Collection objects as the basis for your own collection classes and object models, as discussed in "Creating Your Own Collection Classes" and "Object Models" later in this chapter.
For example, collections are a good way to keep track of multiple forms. "Multiple Document Interface (MDI) Applications" in "Creating a User Interface" discusses applications in which the user can open any number of document windows. The following code fragment shows how you might use the Add method of a collection object to keep a list of MDI child windows the user has created. This code assumes that you have a form named mdiDocument, whose MDIChild property is set to True.
' Module-level collection in the parent MDIForm.
Public colDocuments As New Collection
' Code for creating a new MDI child document form.
Private Sub mnuFileNew()
Dim f As New mdiDocument
Static intDocumentNumber As Integer
intDocumentNumber = intDocumentNumber + 1
' The following line creates the form.
f.Caption = "Document" & intDocumentNumber
' Add the object reference to the collection.
colDocuments.Add f
f.Show
End Sub
The colDocuments collection acts like a subset of the built-in Forms collection, containing only instances of the form mdiDocument. The size of the collection is adjusted automatically as each new form is added. You can use For Each ... Next to iterate through the collection. If you want to give the form a key by which it can be retrieved, you can supply a text string as the second parameter of the Add method, as described later in this section.
The New keyword in the declaration for the variable colDocuments causes a Collection object to be created the first time the variable is referred to in code. Because Collection is a class, rather than a data type, you must create an instance of it and keep a reference to that instance (object) in a variable.
Like any other object, a Collection object will be destroyed when the last variable that contains a reference to it is set to Nothing or goes out of scope. All the object references it contains will be released. For this reason, the variable colDocuments is declared in the parent MDIForm, so that it exists throughout the life of the program.
Note If you use a collection to keep track of forms, use the collection's Remove method to delete the object reference from the collection after you unload the form. You cannot reclaim the memory the form was using as long as a reference to the form still exists, and the reference the Collection object is holding is just as good as a reference in an object variable.
Collections in Visual Basic
What is a collection? In "The Visual Basic Collection Object," a collection was defined as a way of grouping related objects. That leaves a lot of room for interpretation; it's more of a concept than a definition.
In fact, as you'll see when you begin comparing collections, there are a lot of differences even among the kinds of collections provided in Visual Basic. For example, the following code causes an error:
Dim col As Collection
Set col = Forms ' Error!
What's happening here? The Forms collection is a collection; the variable col is declared As Collection; why can't you assign a reference to Forms to the variable col?
The reason for this is that the Collection class and the Forms collection are not polymorphic; that is, you can't exchange one for the other, because they were developed from separate code bases. They don't have the same methods, store object references in the same way, or use the same kinds of index values.
This makes the Collection class's name seem like an odd choice, because it really represents only one of many possible collection implementations. This topic explores some of the implementation differences you'll encounter.
Zero-Based and One-Based Collections
A collection is either zero-based or one-based, depending on what its starting index is. As you might guess, the former means that the index of the first item in the collection is zero, and the latter means it's one. Examples of zero-based collections are the Forms and Controls collections. The Collection object is an example of a one-based collection.
Older collections in Visual Basic are more likely to be zero-based, while more recent additions are more likely to be one-based. One-based collections are somewhat more intuitive to use, because the index ranges from one to Count, where Count is the property that returns the number of items in a collection.
The index of a zero-based collection, by contrast, ranges from zero to one less than the Count property.
Index and Key Values
Many collections in Visual Basic allow you to access an item using either a numeric index or a string key, as the Visual Basic Collection object does. (Visual Basic's Collection object allows you to add items without specifying a key, however.)
The Forms collection, by contrast, allows only a numeric index. This is because there's no unique string value associated with a form. For example, you can have multiple forms with the same caption, or multiple loaded forms with the same Name property
Adding and Removing Items
Collections also differ in whether or not you can add items to them, and if so, how those items are added. You can't add a printer to the Printers collection using Visual Basic code, for example.
Because the Collection object is a general-purpose programming tool, it's more flexible than other collections. It has an Add method you can use to put items into the collection, and a Remove method for taking items out.
By contrast, the only way to get a form into the Forms collection is to load the form. If you create a form with the New operator, or by referring to a variable declared As New, it will not be added to the Forms collection until you use the Load statement to load it.
The Forms and Controls collections don't have Remove methods. You add and remove forms and controls from these collections indirectly, by using the Load and Unload statements.
What Has It Got In Its Pocketses?
As noted above, a form is not added to the Forms collection until it's loaded. Thus the most accurate specification of the Forms collection is that it contains all of the currently loaded forms in the program.
Even that's not completely accurate. If your project uses Microsoft Forms (included for compatibility with Microsoft Office), you'll find those forms in a separate collection named UserForms. So the Forms collection contains all of the currently loaded Visual Basic forms in the program.
The contents of the Collection class are very precisely specified: anything that can be stored in a Variant. Thus the Collection object can contain an object or an integer, but not a user-defined type.
Unfortunately, this specification covers a lot of territory — a given instance of the Collection class could store any mongrel assortment of data types, arrays, and objects.
Tip One of the most important reasons for creating your own collection classes, as discussed in "Creating Your Own Collection Classes," is so you can control the contents of your collections — a concept called type safety. Enumerating a Collection
You can use For Each … Next to enumerate the items in a collection, without worrying about whether the collection is zero-based or one-based. Of course, this is hardly a defining characteristic of collections, because Visual Basic allows you to use For Each … Next to enumerate the items in an array.
What makes For Each … Next work is a tiny object called an enumerator. An enumerator keeps track of where you are in a collection, and returns the next item when it's needed.
When you enumerate an array, Visual Basic creates an array enumerator object on the fly. Collections have their own enumerator objects, which are also created as needed.
Enumerators Don't Skip Items
The enumerators of collections in Visual Basic don't skip items. For example, suppose you enumerate a collection containing "A," "B," and "C," and that while doing so you remove "B." Visual Basic collections will not skip over "C" when you do this.
Enumerators May Not Catch Added Items
If you add items to a collection while enumerating it, some enumerators will include the added items, while some will not. The Forms collection, for example, will not enumerate any forms you load while enumerating.
The Collection object will enumerate items you add while enumerating, if you allow them to be added at the end of the collection. Thus the following loop never ends (until you hit CTRL+BREAK, that is):
Dim col As New Collection
Dim vnt As Variant
col.Add "Endless"
col.Add "Endless"
For Each vnt In col
MsgBox vnt
col.Add "Endless"
Next
On the other hand, items you add at the beginning of the collection will not be included in the enumeration:
Dim col As New Collection
Dim vnt As Variant
col.Add "Will be enumerated"
For Each vnt In col
MsgBox vnt
' Add the item at the beginning.
col.Add "Won't be enumerated", Before:=1
Next
Why Enumerators? By emitting a new enumerator each time a For Each … Next begins, a collection allows nested enumerations. For example, suppose you have a reference to a Collection object in the variable mcolStrings, and that the collection contains only strings. The following code prints all the combinations of two different strings:
Dim vnt1 As Variant
Dim vnt2 As Variant
For Each vnt1 In mcolStrings
For Each vnt2 In mcolStrings
If vnt1 <> vnt2 Then
Debug.Print vnt1 & " " & vnt2
End If
Next
Next
Classes: Putting User-Defined Types and Procedures Together
User-defined types are a powerful tool for grouping related items of data. Consider, for example, the user-defined type named udtAccount defined here:
Public Type udtAccount
Number As Long
Type As Byte
CustomerName As String
Balance As Double
End Type
You can declare a variable of type udtAccount, set the values of its fields individually, and then pass the whole record to procedures that print it, save it to a database, perform computations on it, validate its fields, and so on.
Powerful as they are, user-defined types present the programmer with some problems. You may create a Withdrawal procedure that raises an error if a withdrawal exceeds the balance in the account, but there's nothing to prevent the Balance field from being reduced by other code in your program.
In other words, the connection between procedures and user-defined types depends on the discipline, memory, and knowledge of the programmer maintaining the code.
Objects: User-Defined Types with an Attitude
Object-oriented programming solves this problem by combining data and procedures in a single entity, as shown in Figure.
When the user-defined type udtAccount becomes the Account class, its data become private, and the procedures that access them move inside the class and become properties and methods. This is what's meant by the term encapsulation — that is, an object is a unit (a capsule, if you will) containing both code and data.
When you create an Account object from the class, the only way you can access its data is through the properties and methods that make up its interface. The following code fragment shows how the procedures inside the Account class support encapsulation:
' The account balance is hidden from outside code.
Private mdblBalance As Double
' The read-only Balance property allows outside code
' to find out the account balance.
Public Property Get Balance() As Double
Balance = mdblBalance
End Property
' The Withdrawal method changes the account balance,
' but only if an overdraft error doesn't occur.
Public Sub Withdrawal(ByVal Amount As Double)
If Amount > Balance Then
Err.Raise Number:=vbObjectError + 2081, _
Description:="Overdraft"
End If
mdblBalance = mdblBalance - Amount
End Sub
For the moment, don't worry about how you get the procedures inside the class, or about understanding the syntax of property procedures and private variables. The important thing to remember is that you can define an object that encapsulates and validates its own data.
With the Account object, you never have be concerned about whether you've called the right procedures to update the account, because the only procedures you can call are built into the object.
For More Information "Customizing Form Classes" puts property and method creation into a framework you're already familiar with. Later, "Adding Properties and Methods to a Class" will explain the syntax.
You can read about user-defined types in "Creating Your Own Data Types" in "More About Programming."
For details about Sub and Function procedures, see "Introduction to Procedures" in "Programming Fundamentals."
Customizing Form Classes
It may surprise you to learn that you've been creating classes for as long as you've been programming in Visual Basic. It's true: Form1, that familiar denizen of every project you've ever started, is really · a class.
To see this, open a new Standard Exe project. Add a button to Form1, and place the following code in its Click event:
Private Sub Command1.Click()
Dim f As New Form1
f.Show
End Sub
Press F5 to run the project, and click the button. Holy smokes, there's another instance of Form1! Click its button. There's another! Every instance you create looks the same, and has the same behavior, because they're all instances of the Form1 class.
Public Form1 As New Form1
When you select Form1 as your startup object, or type Form1.Show in code, you're referring to this hidden global object variable. Because it's declared As New, an instance of the Form1 class is created the first time you use this predeclared variable in code.
The reason this declaration is hidden is that Visual Basic changes it every time you change the Name property of a form. In this way, the hidden variable always has the same name as the form class.
The second instance of Form1, and all the ones that followed, had an object variable for just as long as it took to call their Show methods. Then that variable went out of scope, and was set to Nothing. But Visual Basic keeps a special collection named Forms, which you can read about in "More About Forms" in "Creating a User Interface." The Forms collection contains a reference to each of the loaded forms in your project, so that you can always find and control them.
Note As you'll learn, this is not true of all classes. For example, the classes you design won't have hidden global variables or global collections to keep track of them — those are special features of form classes. However, you can declare your own global variables, and you can create your own collections — as described in "Creating Your Own Collection Classes."
Properties, Methods, and Events of Form Classes
The first time you added a property to a form class, you probably did it visually, by dropping a command button (or some other control) on Form1. In doing so, you added a read-only Command1 property to the form class. Thereafter, you invoked this property of Form1 whenever you needed to call a method or property of the command button:
Command1.Caption = "Click Me"
When you changed the Name property of any control on a form, Visual Basic quietly changed the name of the read-only property, so they always matched.
If you still have the project open from the earlier exercise, you can see this Command1 property by pressing F2 to open the Object Browser. In the Project/Library box, select Project1. You'll see Form1 in the Classes pane. In the Members pane, scroll down until you find Command1, and select it.
Command1 has a property symbol beside it, and if you look in the description pane, you'll see that it's a WithEvents property. As you'll learn in "Adding Events to a Class," this means that the property (or object variable) has event procedures associated with it. One of those event procedures, Command1_Click(), may have been the first place you ever wrote Visual Basic code.
But Wait, There's More Dropping controls on a form is not the only way to add new members to the form class. You can add your own custom properties, methods, and events, as easily as you create new variables and procedures.
To see this, add the following code to the Declarations section of Form1:
' The Comment property of the Form1 class.
Public Comment As String
Add the following code to the Click event of Form1:
Private Sub Form_Click()
MsgBox Comment, , "My comment is:"
End Sub
Finally, change the code in the Command1_Click() event procedure by adding a line, as follows:
Private Sub Command1.Click()
Dim f As New Form1
f.Comment = InputBox("What's my comment?")
f.Show
End Sub
Press F5 to run the project. Click Command1, and when the input box appears, type in some racy comment and click OK. When the new instance of Form1 appears, click on it to play back its Comment property.
Click on the first instance of Form1, and notice that its Comment property is blank. Because Visual Basic created this instance as the Startup Object, you never got a chance to set its Comment property.
Forms Can Call Each Other's Methods If you were watching closely, you may have noticed that the code you added to the Form1 class didn't set the object's own Comment property — it set the Comment property of the new instance of Form1 it was creating.
This ability of forms to set each other's properties and call each other's methods is a very useful technique. For example, when an MDIForm is opening a new child window, it can initialize the new window by setting its properties and calling its methods.
You can also use this technique to pass information between forms.
Tip You can create custom events for forms. "Adding an Event to a Form" later in this chapter, provides a step by step procedure
Other Kinds of Modules
You add properties, methods, and events to form classes by putting code in their code modules. In the same way, you can add properties, methods, and events to class modules and — if you have the Professional or Enterprise Edition of Visual Basic — to UserControl and UserDocument code modules.
As you read "Adding Properties and Methods to a Class" and "Adding Events to a Class," remember that everything you read applies to form classes as well as to class modules.
Class Module Step by Step This example shows how you can use class modules to define classes, from which you can then create objects. It will also show you how to create properties and methods for the new class, and demonstrate how objects are created and destroyed.
Open a new Standard Exe project, and insert a class module by selecting Add Class Module from the Project menu. Draw four command buttons on the form. The following table lists the property values you need to set for the objects in this example.
Object Property Setting Class module Name Thing Command1 Caption Show the Thing Command2 Caption Reverse the Thing's Name Command3 Caption Create New Thing Command4 Caption Temporary Thing
Note Class modules are saved in files with the extension .cls. In the class module Declarations section, add the following:
Option Explicit
Public Name As String
Private mdtmCreated As Date
The variable Name will be a property of the Thing object, because it's declared Public.
Note Don't confuse this Name property with the Name property of the class module, which the table above instructed you to set. (The Name property of the class module gives the Thing class its name.) Why would you give the Thing class a Name property? A better question might be, why not? You may want to give the Thing class a Name property because Things should have names! Remember that there's nothing special about the property and method names Visual Basic uses. You can use those same property and method names for your classes. The variable mdtmCreated is a private data member that is used to store the value of the read-only Created property. The Created property returns the date and time a Thing object was created. To implement the Created property, add the following Property Get to the Declarations section of the class module:
Property Get Created() As Date
Created = mdtmCreated
End Property
Note If you added the property procedure using the Add Procedure dialog box, on the Tools menu, be sure to delete the Property Let declaration that is automatically added by this dialog. Property Let is only required for read-write properties, as explained in "Putting Property Procedures to Work for You." The Thing object has one method, ReverseName, which simply reverses the order of the letters in the Name property. It doesn't return a value, so it's implemented as a Sub procedure. Add the following Sub procedure to the class module.
Public Sub ReverseName()
Dim intCt As Integer
Dim strNew As String
For intCt = 1 To Len(Name)
strNew = Mid$(Name, intCt, 1) & strNew
Next
Name = strNew
End Sub
Class modules have two events, Initialize and Terminate. In the Object drop down of the class module, select Class. The Procedure drop down will show the events. Place the following code in the event procedures:
Private Sub Class_Initialize()
' Set date/time of object creation, to be returned
' by the read-only Created property.
mdtmCreated = Now
' Display object properties.
MsgBox "Name: " & Name & vbCrLf & "Created: " _
& Created, , "Thing Initialize"
End Sub
Private Sub Class_Terminate()
' Display object properties.
MsgBox "Name: " & Name & vbCrLf & "Created: " _
& Created, , "Thing Terminate"
End Sub
Usually, the Initialize event procedure contains any code that needs to be executed at the moment the object is created, such as providing the time stamp for the Created property. The Terminate event contains any code you need to execute in order to clean up after the object when it is being destroyed.
In this example, the two events are being used primarily to give you a visual indication that a Thing object is being created or destroyed.
Using the Thing Object
Add this declaration to the Declarations section of the form module: Option Explicit
Private mth As Thing
The variable mth will hold a reference to a Thing object, which will be created in the form's Load event. Put the following code in the Form_Load event procedure, and in the Click event procedures for the four buttons.
Private Sub Form_Load()
Set mth = New Thing
mth.Name = InputBox("Enter a name for the Thing")
End Sub
' Button "Show the Thing"
Private Sub Command1_Click()
MsgBox "Name: " & mth.Name & vbCrLf _
& "Created: " & mth.Created, , "Form Thing"
End Sub
' Button "Reverse the Thing's Name"
Private Sub Command2_Click()
mth.ReverseName
' Click "Show the Thing"
Command1.Value = True
End Sub
' Button "Create New Thing"
Private Sub Command3_Click()
Set mth = New Thing
mth.Name = InputBox( _
"Enter a name for the new Thing")
End Sub
' Button "Temporary Thing".
Private Sub Command4_Click()
Dim thTemp As New Thing
thTemp.Name = InputBox( _
"Enter a name for the Temporary Thing")
End Sub
Running the Project
Press F5 to run the project. Looking at the code in the Form_Load event procedure, you can see that the New operator is used to create a Thing object. A reference to this Thing is assigned to the variable mth.
You will see the InputBox asking you for a name for the Thing. When you type a name and press ENTER, the return value is assigned to the Name property of the Thing object.
Show the Form Thing
You can verify that the Name property has been assigned by pressing the first button, "Show the Thing," which displays a message box with all the properties of the Thing object.
Reverse the Thing's Name Press the second button, "Reverse the Thing's Name." This button calls the ReverseName method to turn the Thing object's name around, and then clicks the first button to display the updated property values.
Create New Thing Click the "Create New Thing" button to destroy the existing Thing object and create a new one. (Or, as it turns out, to create a new Thing and then destroy the old one.)
The New operator causes a new Thing to be created, so you'll see the MsgBox displayed by the new Thing's Initialize event. When you click OK, a reference to the new Thing is placed in the form-level variable mth.
This wipes out the reference to the old Thing. Because there are no more references to it, it's destroyed, and you'll see its Terminate event message box. When you click OK, the InputBox statement requests a name for the new Thing.
Note If you want to destroy the old Thing before creating the new one, you can add the line of code Set mth = Nothing at the beginning of the event procedure. Temporary Thing
The fourth button demonstrates another aspect of object lifetime. When you press it, you'll be prompted for a name for the temporary Thing.
But wait — there isn't a temporary Thing object yet. You haven't seen its Initialize message box. How can you assign it a name?
Because the variable thTemp was declared As New, a Thing object will be created the moment one of its properties or methods is invoked. This will happen when the return value of the InputBox is assigned to the Name property. Type a name and click OK on the InputBox.
You'll now see the Thing Initialize message box, which shows you that the Name property is still blank. When you click OK to dismiss the message box, the value from the InputBox statement is finally assigned to the Name property. That's a lot of activity for one line of code.
Of course, as soon as you've done that, the Click event procedure ends, and the variable thTemp goes out of scope. The object reference for the temporary Thing is released, so you'll see the Thing Terminate message box. Notice that it contains the name you supplied.
Each time you click this button, another temporary Thing will be created, named, and destroyed.
Closing the Program Close the program by clicking the form's close button. Do not use the End button on the toolbar. When the program closes, Form1 is destroyed. The variable mth goes out of scope, and Visual Basic cleans up the reference to the Thing. There are no remaining references to the Thing, so it's destroyed, and its Terminate event message box is displayed.
Run the program again, and this time end it using the End button on the toolbar. Notice that the Terminate message box for the Thing object is not displayed.
It's important to remember that ending your program with the End button, or with an End statement in your code, halts the program immediately, without executing the Terminate events of any objects. It's always better to shut down your program by unloading all the forms.
You may find it useful to run the example by pressing F8 to step through the code one line at a time. This is a good way to understand the order of events for object creation and destruction.
Important In an actual application, the Initialize and Terminate events should not contain message boxes, or any other code that allows Windows messages to be processed. In general, it's better to use Debug.Print statements when debugging object lifetimes.
Debugging Class Modules
programs. This is because an error in a property or method of a class module always acts like a handled error. (That is, there's always a procedure on the call stack that can handle the error — namely the procedure that called the class module's property or method.)
Visual Basic compensates for this difference by providing the error-trapping option Break in Class Module, in addition to the older options Break on Unhandled Errors and Break on All Errors.
Note You can set the Default Error Trapping State on the General tab of the Options dialog box, available from the Tools menu. The option you select affects the current session, and becomes the default for all subsequent instances of Visual Basic. To change the setting only for the current session, without affecting the default, select Toggle from the Code window context menu (which is available by right-clicking on the Code window).
For example, suppose the class module Class1 contains the following code:
Public Sub Oops()
Dim intOops As Integer
intOops = intOops / 0
End Sub
Now suppose a procedure in another class module, form, or standard module calls the member Oops:
Private Sub Command1_Click()
Dim c1 As New Class1
c1.Oops
End Sub
If the error trapping option is set to Break on Unhandled Errors, execution will not stop on the zero divide. Instead, the error will be raised in the calling procedure, Command1_Click. Execution will stop on the call to the Oops method.
You could use Break on All Errors to stop in the zero divide, but Break on All Errors is a very inconvenient option for most purposes. It stops on every error, even errors for which you've written error handling code.
Break in Class Module is a compromise setting:
Execution will not stop on class module code for which you've written an error handler.
Execution only stops on an error that's unhandled in the class module, and therefore would be returned to the caller of the method.
When the Visual Basic development environment is started, it defaults to Break in Class Module.
If there are no class modules involved, Break in Class Module is exactly the same as Break on Unhandled Errors.
Tip When you hit a break point using Break in Class Module or Break on All Errors, you can step or run past the error — into your error handling code or into the code that called procedure in which the error occurred — by pressing ALT+F8 or ALT+F5.
Life Cycle of Visual Basic Forms
Because they're visible to the user, forms and controls have a different life cycle than other objects. For example, a form will not close just because you've released all your references to it. Visual Basic maintains a global collection of all forms in your project, and only removes a form from that collection when you unload the form.
In similar fashion, Visual Basic maintains a collection of controls on each form. You can load and unload controls from control arrays, but simply releasing all references to a control is not sufficient to destroy it.
States a Visual Basic Form Passes Through
A Visual Basic form normally passes through four states in its lifetime:
Created, but not loaded
Loaded, but not shown
Shown.
Memory and resources completely reclaimed.
There's a fifth state a form can get into under certain circumstances: Unloaded and unreferenced while a control is still referenced.
This topic describes these states, and the transitions between them.
Created, But Not Loaded
The beginning of this state is marked by the Initialize event. Code you place in the Form_Initialize event procedure is therefore the first code that gets executed when a form is created.
In this state, the form exists as an object, but it has no window. None of its controls exist yet. A form always passes through this state, although its stay there may be brief.
For example, if you execute Form1.Show, the form will be created, and Form_Initialize will execute; as soon as Form_Initialize is complete, the form will be loaded, which is the next state.
The same thing happens if you specify a form as your Startup Object, on the General tab of the Project Properties dialog box (which is available from the Project menu). A form specified as the Startup Object is created as soon as the project starts, and is then immediately loaded and shown.
Note You can cause your form to load from within Form_Initialize, by calling its Show method or by invoking its built-in properties and methods, as described below.
Remaining Created, But Not Loaded
By contrast, the following code creates an instance of Form1 without advancing the form to the loaded state:
Dim frm As Form1
Set frm = New Form1
Once Form_Initialize has ended, the only procedures you can execute without forcing the form to load are Sub, Function, and Property procedures you've added to the form's code window. For example, you might add the following method to Form1:
Public Sub ANewMethod()
Debug.Print "Executing ANewMethod"
End Sub
You could call this method using the variable frm (that is, frm.ANewMethod) without forcing the form on to the next state. In similar fashion, you could call ANewMethod in order to create the form:
Dim frm As New Form1
frm.ANewMethod
Because frm is declared As New, the form is not created until the first time the variable is used in code — in this case, when ANewMethod is invoked. After the code above is executed, the form remains created, but not loaded.
Note Executing Form1.ANewMethod, without declaring a form variable, has the same effect as the example above. As explained in "Customizing Form Classes," Visual Basic creates a hidden global variable for each form class. This variable has the same name as the class; it's as though Visual Basic had declared Public Form1 As New Form1.
You can execute as many custom properties and methods as you like without forcing the form to load. However, the moment you access one of the form's built-in properties, or any control on the form, the form enters the next state.
Note You may find it helpful to think of a form as having two parts, a code part and a visual part. Before the form is loaded, only the code part is in memory. You can call as many procedures as you like in the code part without loading the visual part of the form.
The Only State All Forms Pass Through
Created, But Not Loaded is the only state all forms pass through. If the variable frm in the examples above is set to Nothing, as shown here, the form will be destroyed before entering the next state:
Dim frm As New Form1
frm.ANewMethod
Set frm = Nothing ' Form is destroyed.
A form used in this fashion is no better than a class module, so the vast majority of forms pass on to the next state.
Loaded, But Not Shown
The event that marks the beginning of this state is the familiar Load event. Code you place in the Form_Load event procedure is executed as soon as the form enters the loaded state.
When the Form_Load event procedure begins, the controls on the form have all been created and loaded, and the form has a window — complete with window handle (hWnd) and device context (hDC) — although that window has not yet been shown.
Any form that becomes visible must first be loaded.
Many forms pass automatically from the Created, But Not Loaded state into the Loaded, but Not Shown state. A form will be loaded automatically if:
The form has been specified as the Startup Object, on the General tab of the Project Properties dialog box.
The Show method is the first property or method of the form to be invoked, as for example Form1.Show.
The first property or method of the form to be invoked is one of the form's built-in members, as for example the Move method.
Note This case includes any controls on the form, because each control defines a property of the form; that is, in order to access the Caption property of Command1, you must go through the form's Command1 property: Command1.Caption.
The Load statement is used to load the form, without first using New or As New to create the form, as described earlier.
In the first two cases listed above, the form will continue directly on to the visible state, as soon as Form_Load completes. In the last two cases, the form will remain loaded, but not shown.
It has long been common coding practice in Visual Basic to load a form but never show it. This might be done for several reasons:
To use the Timer control to generate timed events.
To use controls for their functionality, rather than their user interface — for example, for serial communications or access to the file system.
To execute DDE transactions.
Note With the Professional or Enterprise edition, you can create ActiveX components (formerly called OLE servers), which are often better at providing code-only functionality than controls are. See Creating ActiveX Components in the Component Tools Guide.
Always Coming Home
Forms return from the visible state to the loaded state whenever they're hidden. Returning to the loaded state does not re-execute the Load event, however. Form_Load is executed only once in a form's life.
Shown
Once a form becomes visible, the user can interact with it. Thereafter, the form may be hidden and shown as many times as you like before finally being unloaded.
Unloaded and Unreferenced, But a Control Is Still Referenced
To get into this odd state, you have to unload and free the form while keeping a reference to one of its controls. If this sounds like a silly thing to do, rest assured that it is.
Dim frm As New Form1
Dim obj As Object
frm.Show vbModal
' When the modal form is dismissed, save a
' reference to one of its controls.
Set obj = frm.Command1
Unload frm
Set frm = Nothing
The form has been unloaded, and all references to it released. However, you still have a reference to one of its controls, and this will keep the code part of the form from releasing the memory it's using. If you invoke any of the properties or methods of this control, the form will be reloaded:
obj.Caption = "Back to life"
The values in module-level variables will still be preserved, but the property values of all the controls will be set back to their defaults, as if the form were being loaded for the first time. Form_Load will execute.
Note In some previous versions of Visual Basic, the form did not completely re-initialize, and Form_Load did not execute again.
Note Not all forms behave as Visual Basic forms do. For example, the Microsoft Forms provided in Microsoft Office don't have Load and Unload events; when these forms receive their Initialize events, all their controls exist and are ready to use.
Class Modules vs. Standard Modules
Classes differ from standard modules in the way their data is stored. There's never more than one copy of a standard module's data. This means that when one part of your program changes a public variable in a standard module, and another part of your program subsequently reads that variable, it will get the same value.
Class module data, on the other hand, exists separately for each instance of the class (that is, for each object created from the class).
By the same token, data in a standard module has program scope — that is, it exists for the life of your program — while class module data for each instance of a class exists only for the lifetime of the object; it's created when the object is created, and destroyed when the object is destroyed.
Finally, variables declared Public in a standard module are visible from anywhere in your project, whereas Public variables in a class module can only be accessed if you have an object variable containing a reference to a particular instance of a class.
All of the above are also true for public procedures in standard modules and class modules. This is illustrated by the following example. You can run this code by opening a new Standard Exe project and using the Project menu to add a module and a class module.
Place the following code in Class1:
' The following is a property of Class1 objects.
Public Comment As String
' The following is a method of Class1 objects.
Public Sub ShowComment()
MsgBox Comment, , gstrVisibleEverywhere
End Sub
Place the following code in Module1:
' Code in the standard module is global.
Public gstrVisibleEverywhere As String
Public Sub CallableAnywhere(ByVal c1 As Class1)
' The following line changes a global variable
' (property) of an instance of Class1. Only the
' particular object passed to this procedure is
' affected.
c1.Comment = "Touched by a global function."
End Sub
Put two command buttons on Form1, and add the following code to Form1:
Private mc1First As Class1
Private mc1Second As Class1
Private Sub Form_Load()
' Create two instances of Class1.
Set mc1First = New Class1
Set mc1Second = New Class1
gstrVisibleEverywhere = "Global string data"
End Sub
Private Sub Command1_Click()
Call CallableAnywhere(mc1First)
mc1First.ShowComment
End Sub
Private Sub Command2_Click()
mc1Second.ShowComment
End Sub
Press F5 to run the project. When Form1 is loaded, it creates two instances of Class1, each having its own data. Form1 also sets the value of the global variable gstrVisibleEverywhere.
Press Command1, which calls the global procedure and passes a reference to the first Class1 object. The global procedure sets the Comment property, and Command1 then calls the ShowComment method to display the object's data.
As Figure shows, the resulting message box demonstrates that the global procedure CallableAnywhere set the Comment property of the object that was passed to it, and that the global string is visible from within Class1.
Figure Message box from the first Class1 object
Important Avoid making the code in your classes dependent on global data — that is, public variables in standard modules. Many instances of a class can exist simultaneously, and all of these objects share the global data in your program.
Using global variables in class module code also violates the object-oriented programming concept of encapsulation, because objects created from such a class do not contain all their data.
Static Class Data
There may be occasions when you want a single data item to be shared among all objects created from a class module. This is sometimes referred to as static class data.
You cannot implement true static class data in a Visual Basic class module. However, you can simulate it by using Property procedures to set and return the value of a Public data member in a standard module, as in the following code fragment:
' Read-only property returning the application name.
Property Get CommonString() As String
' The variable gstrVisibleEverywhere is stored in a
' standard module, and declared Public.
CommonString = gstrVisibleEverywhere
End Property
Note You cannot use the Static keyword for module-level variables in a class module. The Static keyword can only be used within procedures.
It's possible to simulate static class data that's not read-only by providing a corresponding Property Let procedure — or Property Set for a property that contains an object reference — to assign a new value to the standard module data member. Using global variables in this fashion violates the concept of encapsulation, however, and is not recommended.
For example, the variable gstrVisibleEverywhere can be set from anywhere in your project, even from code that doesn't belong to the class that has the CommonString property. This can lead to subtle errors in your program.
Adding Properties to a Class
The easiest way to define properties for a class is by adding public variables to the class module. For example, you could very easily create an Account class by declaring two public variables in a class module named Account:
Public Balance As Double
Public Name As String
This is pretty easy. It's just as easy to create private data for a class; simply declare a variable Private, and it will be accessible only from code within the class module:
Private mstrMothersMaidenName As String
Private mintWithdrawalsMonthToDate As Integer
Data Hiding
The ability to protect part of an object's data, while exposing the rest as properties, is called data hiding. This is one aspect of the object-oriented principle of encapsulation, as explained in "Classes: Putting User-Defined Types and Procedures Together."
Data hiding means that you can make changes in the implementation of a class — for example, increasing the Account class's private variable mintWithdrawalsMonthToDate from an Integer to a Long — without affecting existing code that uses the Account object.
Data hiding also allows you to define properties that are read-only. For example, you could use a Property Get procedure to return the value of the private variable containing the number of withdrawals in a month, while only incrementing the variable from within the Account object's code. Which brings us to property procedures.
Property Procedures
Data hiding wouldn't be much use if the only way you could create properties was by declaring public variables. How much good would it do you to give the Account class a Type property, if any code that had a reference to an Account object could blithely set the account type to any value at all? Property procedures allow you to execute code when a property value is set or retrieved. For example, you might want to implement the Type property of the Account object as a pair of Property procedures:
Public Enum AccountTypes
atSavings = 1
atChecking
atLineOfCredit
End Enum
' Private data storage for the Type property.
Private mbytType As AccountTypes
Public Property Get Type() As AccountTypes
Type = mbytType
End Property
Public Property Let Type(ByVal NewType As AccountTypes)
Select Case NewType
Case atChecking, atSavings, atLineOfCredit
' No need to do anything if NewType is valid.
Case Else
Err.Raise Number:=vbObjectError + 32112, _
Description:="Invalid account type"
End Select
If mbytType > NewType Then
Err.Raise Number:=vbObjectError + 32113, _
Description:="Cannot downgrade account type"
Else
mbytType = NewType
End If
End Property
Now suppose you have a variable named acct that contains a reference to an Account object. When the code x = acct.Type is executed, the Property Get procedure is invoked to return the value stored in the class module's private data member mbytType.
When the code acct.Type = atChecking is executed, the Property Let is invoked. If the Account object is brand new, mbytType will be zero, and any valid account type can be assigned. If the current account type is atSavings, the account will be upgraded.
However, if the current account type is atLineOfCredit, the Property Let will raise an error, preventing the downgrade. Likewise, if the code acct.Type = 0 is executed, the Select statement in the Property Let will detect the invalid account type and raise an error.
In short, property procedures allow an object to protect and validate its own data.
Property Procedures vs. Public Variables
Property procedures are clearly such a powerful means for enabling encapsulation that you may be wondering if you should even bother with public variables. The answer, as always in programming, is "Of course — sometimes." Here are some ground rules: Use property procedures when:
Putting Property Procedures to Work for You
Visual Basic provides three kinds of property procedures, as described in the following table.
Procedure
Purpose Property Get Returns the value of a property Property Let Sets the value of a property Property Set Sets the value of an object property (that is, a property that contains a reference to an object).
As you can see from the table, each of these property procedures has a particular role to play in defining a property. The typical property will be made up of a pair of property procedures: A Property Get to retrieve the property value, and a Property Let or Property Set to assign a new value. These roles can overlap in some cases. The reason there are two kinds of property procedures for assigning a value is that Visual Basic has a special syntax for assigning object references to object variables:
Dim wdg As Widget
Set wdg = New Widget
The rule is simple: Visual Basic calls Property Set if the Set statement is used, and Property Let if it is not.
Tip To keep Property Let and Property Set straight, hearken back to the Basics of yore, when instead of x = 4 you had to type Let x = 4 (syntax supported by Visual Basic to this very day). Visual Basic always calls the property procedure that corresponds to the type of assignment — Property Let for Let x = 4, and Property Set for Set c1 = New Class1 (that is, object properties).
Read-Write Properties
The following code fragment shows a typical read-write property:
' Private storage for property value.
Private mintNumberOfTeeth As Integer
Public Property Get NumberOfTeeth() As Integer
NumberOfTeeth = mintNumberOfTeeth
End Property
Public Property Let NumberOfTeeth(ByVal NewValue _
As Integer)
' (Code to validate property value omitted.)
mintNumberOfTeeth = NewValue
End Property
The name of the private variable that stores the property value is made up of a scope prefix (m) that identifies it as a module-level variable; a type prefix (int); and a name (NumberOfTeeth). Using the same name as the property serves as a reminder that the variable and the property are related.
As you've no doubt noticed, here and in earlier examples, the names of the property procedures that make up a read-write property must be the same.
Note Property procedures are public by default, so if you omit the Public keyword, they will still be public. If for some reason you want a property to be private (that is, accessible only from within the object), you must declare it with the Private keyword. It's good practice to use the Public keyword, even though it isn't required, because it makes your intentions clear.
Property Procedures at Work and Play
It's instructive to step through some property procedure code. Open a new Standard Exe project and add a class module, using the Project menu. Copy the code for the NumberOfTeeth property, shown above, into Class1.
Switch to Form1, and add the following code to the Load event:
Private Sub Form_Load()
Dim c1 As Class1
Set c1 = New Class1
' Assign a new property value.
c1.NumberOfTeeth = 42
' Display the property value.
MsgBox c1.NumberOfTeeth
End Sub
Press F8 to step through the code one line at a time. Notice that when the property value is assigned, you step into the Property Let, and when it's retrieved, you step into the Property Get. You may find it useful to duplicate this exercise with other combinations of property procedures.
Arguments of Paired Property Procedures Must Match
The property procedure examples you've seen so far have been simple, as they will be for most properties. However, property procedures can have multiple arguments — and even optional arguments. Multiple arguments are useful for properties that act like arrays, as discussed below.
When you use multiple arguments, the arguments of a pair of property procedures must match. The following table demonstrates the requirements for arguments in property procedure
Procedure Declaration syntax Property Get Property Get propertyname(1,..., n) As type Property Let Property Let propertyname(1,..., n, n+1) Property Set Property Set propertyname(1,..., n, n+1) The first argument through the second-to-last argument (1,..., n) must share the same names and data types in all Property procedures with the same name. As with other procedure types, all of the required parameters in this list must precede the first optional parameter.
You've probably noticed that a Property Get procedure declaration takes one less argument than the related Property Let or Property Set. The data type of the Property Get procedure must be the same as the data type of the last argument (n+1) in the related Property Let or Property Set.
For example, consider this Property Let declaration, for a property that acts like a two-dimensional array:
Public Property Let Things(ByVal X As Integer, _
ByVal Y As Integer, ByVal Thing As Variant)
' (Code to assign array element omitted.)
End Property
The Property Get declaration must use arguments with the same name and data type as the arguments in the Property Let procedure:
Public Property Get Things(ByVal X As Integer, _
ByVal Y As Integer) As Variant
' (Code for retrieval from array omitted.)
End Property
The data type of the final argument in a Property Set declaration must be either an object type or a Variant.
Matching Up the Arguments
The reason for these argument matching rules is illustrated in Figure , which shows how Visual Basic matches up the parts of the assignment statement with the arguments of a Property Let.
Figure Calling a Property Let procedure
The most common use for property procedures with multiple arguments is to create property arrays
Read-Only Properties
To create a read-only property, simply omit the Property Let or (for object properties) the Property Set.
Object Properties
If you're creating a read-write object property, you use a Property Get and a Property Set, as here:
Private mwdgWidget As Widget
Public Property Get Widget() As Widget
' The Set statement must be used to return an
' object reference.
Set Widget = mwdgWidget
End Property
Public Property Set Widget(ByVal NewWidget As Widget)
Set mwdgWidget = NewWidget
End Property
Variant Properties
Read-write properties of the Variant data type are the most complicated. They use all three property procedure types, as shown here:
Private mvntAnything As Variant
Public Property Get Anything() As Variant
' The Set statement is used only when the Anything
' property contains an object reference.
If IsObject(mvntAnything) Then
Set Anything = mvntAnything
Else
Anything = mvntAnything
End If
End Property
Public Property Let Anything(ByVal NewValue As Variant)
' (Validation code omitted.)
mvntAnything = NewWidget
End Property
Public Property Set Anything(ByVal NewValue As Variant)
' (Validation code omitted.)
Set mvntAnything = NewWidget
End Property
The Property Set and Property Let are straightforward, as they're always called in the correct circumstances. However, the Property Get must handle both of the following cases:
strSomeString = objvar1.Anything
Set objvar2 = objvar1.Anything
In the first case, the Anything property contains a string, which is being assigned to a String variable. In the second, Anything contains an object reference, which is being assigned to an object variable.
The Property Get can be coded to handle these cases, by using the IsObject function to test the private Variant before returning the value.
Of course, if the first line of code is called when Anything contains an object reference, an error will occur, but that's not Property Get's problem — that's a problem with using Variant properties.
Write-Once Properties
There are many possible combinations of property procedures. All of them are valid, but some are relatively uncommon, like write-only properties (only a Property Let, no Property Get). And some depend on factors other than the kinds of property procedures you combine.
For example, when you organize the objects in your program by creating an object model, as described in "Object Models" later in this chapter, you may want an object to be able to refer back to the object that contains it. You can do this by implementing a Parent property.
You need to set this Parent property when the object is created, but thereafter you may want to prevent it from being changed — accidentally or on purpose. The following example shows how the Account object might implement a Parent property that points to the Department object that contains the account.
' Private data storage for Parent property.
Private mdeptParent As Department
Property Get Parent() As Department
' Use the Set statement for object references.
Set Parent = mdeptParent
End Property
' The property value can only be set once.
Public Property Set Parent(ByVal NewParent _
As Department)
If deptParent Is Nothing Then
' Assign the initial value.
Set mdeptParent = NewParent
Else
Err.Raise Number:=vbObjectError + 32144, _
Description:="Parent property is read-only"
End If
End Property
When you access the parent of an Account object, for example by coding strX = acctNew.Parent.Name to get the department name, the Property Get is invoked to return the reference to the parent object.
The Property Set in this example is coded so that the Parent property can be set only once. For example, when the Department object creates a new account, it might execute the code Set acctNew.Parent = Me to set the property. Thereafter the property is read-only.
Adding Methods to a Class The methods of a class are just the public Sub or Function procedures you've declared. Since Sub and Function procedures are public by default, you don't even have to explicitly specify the Public keyword to create a method.
For example, to create a Withdrawal method for the Account class, you could add this Public Function procedure to the class module:
Public Function WithDrawal(ByVal Amount As Currency, _
ByVal TransactionCode As Byte) As Double
' (Code to perform the withdrawal and return the
' new balance, or to raise an Overdraft error.)
End Function
Tip Although you don't have to type the Public keyword, doing so is good programming practice, because it makes your intent clear to people maintaining your code later.
Declaring Methods as Public Subs
Returning the new balance is optional, since you could easily call the Balance property of the Account object after calling the Withdrawal method. You could thus code Withdrawal as a Public Sub procedure.
Tip If you find yourself calling Balance almost every time you call Withdrawal, returning the new balance will be slightly more efficient. This is because, as noted in "Adding Properties to Class Modules," any property access, even reading a public variable, means a function call — an explicit or implicit Property Get.
Important The following names cannot be used as property or method names, because they belong to the underlying IUnknown and IDispatch interfaces: QueryInterface, AddRef, Release, GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, and Invoke. These names will cause a compilation error.
Protecting Implementation Details
Even more important, you can also change the code inside the public Sub or Function procedure that implements a method, without affecting code that uses the method. As long as you don't change the data types of the procedure's arguments, or the type of data returned by a Function procedure, the interface is unchanged.
Hiding the details of an object's implementation behind the interface is another facet of encapsulation. Encapsulation allows you to enhance the performance of methods, or completely change the way a method is implemented, without having to change code that uses the method.
Note The guidelines for naming interface elements — discussed in "Naming Properties, Methods, and Events" — apply not only to property and method names, but to the names of parameters in the Sub and Function procedures that define your methods. These parameter names are visible when you view the methods in the Object Browser, and can be used as named parameters (that is, parametername:=value) when the methods are invoked. Is It a Property or a Method?
In general, a property is data about an object, while a method is an action the object can be asked to perform. Some things are obviously properties, like Color and Name, and some are obviously methods, like Move and Show.
As with any facet of human endeavor, however, there's a gray area in which an argument can be made either way.
For example, why is the Item method of the Visual Basic Collection class a method and not an indexed property? Aren't the items in the collection just data? The Item method of a hypothetical Widgets collection class could be implemented either way, as shown here:
' Private storage for the objects in the Widgets
' collection (same for both implementations).
Private mcol As New Collection
Public Property Get Item(Index As Variant) As Widget
Set Item = mcol.Item(Index)
End Function
- or -
Public Function Item(Index As Variant) As Widget
Set Item = mcol.Item(Index)
End Function
There's not a whole lot of difference between these two implementations. Both are read-only, so both depend on the Add method of the Widgets class to get Widget objects into the collection. Both delegate everything to a Collection object — even their errors are generated by the Collection!
The Syntax Argument
Set Widgets.Item(4) = wdgMyNewWidget
If so, implement the member as a read-write property, using Property Get and Property Set, because methods don't support this syntax.
Note In most collection implementations you encounter, this syntax is not allowed. Implementing a Property Set for a collection is not as easy as it looks.
The Property Window Argument
You can also suppose for a moment that your object is like a control. Can you imagine the member showing up in the Property window, or on a property page? If that doesn't make sense, don't implement the member as a property.
The Sensible Error Argument
If you forget that you made Item a read-only property and try to assign a value to it, you'll most likely find it easier to understand the error message Visual Basic raises for a Property Get — "Can't assign to read-only property" — than the error message it raises for a Function procedure — "Function call on left-hand side of assignment must return Variant or Object."
The Argument of Last Resort As a last resort, flip a coin. If none of the other arguments in this topic seem compelling, it probably doesn't make much difference.
Making a Property or Method the Default
You can give objects created from your classes default properties, like the default properties of objects provided by Visual Basic. The best candidate for default member is the one you use most often.
To set a property or method as the default
You can also open the Procedure Attributes dialog box from the Object Browser. This is convenient when you're changing the default member of a class, because it allows you to locate the existing default member quickly.
To change a default property using the Object Browser
Fixing Defaults You Have Accidentally Made Private or Friend
The Procedure Attributes dialog box only allows you to select public properties and methods as the default for a class. If you make a public property or method the default for a class, and later change the declaration to Private or Friend, the property or method may continue to behave as if it were still declared Public.
To correct this problem, you must make the property or method Public again, because the Procedure Attributes dialog box will not show procedures declared Private and Friend. Once you have changed the declaration to Public, you can use the Procedure Attributes dialog to remove the Default attribute. You can then change the declaration back to Friend or Private
Friend Properties and Methods.
In addition to declaring properties and methods Public and Private, you can declare them Friend. Friend members look just like Public members to other objects in your project. That is, they appear to be part of a class's interface. They are not.
In the ActiveX components you can create with the Professional and Enterprise editions of Visual Basic, Friend members play an important role. Because they're not part of an object's interface, they can't be accessed by programs that use the component's objects. They're visible to all the other objects within the component, however, so they allow safe internal communication within the component.
Important Because Friend members aren't part of an object's public interface, they can't be accessed late bound — that is, through variables declared As Object. To use Friend members, you must declare variables with early binding — that is, As classname.
Standard Exe projects can't be ActiveX components, because their class modules can't be Public, and thus can't be used by other applications. All communication between objects in a Standard Exe project is therefore private, and there's no need for Friend members.
However, Friend members have one particularly useful feature. Because they're not part of an ActiveX interface, they can be used to pass user-defined types between objects without exposing them publicly. For example, suppose you have the following user-defined type in a standard module:
Public Type udtDemo intA As Integer
lngB As Long
strC As String
End Type
You can define the following private variable and Friend members in Class1:
Private mDemo As udtDemo
Friend Property Get Demo() As udtDemo
Demo = mDemo
End Property
' Note that udtDemo must be passed by reference.
Friend Property Let Demo(NewDemo As udtDemo)
mDemo = NewDemo
End Property
Friend Sub SetDemoParts(ByVal A As Integer, _
ByVal B As Long, ByVal C As String)
mDemo.intA = A
mDemo.lngB = B
mDemo.strC = C
End Sub
Public Sub ShowDemo()
MsgBox mDemo.intA & vbCrLf _
& mDemo.lngB & vbCrLf & mDemo.strC
End Sub
Note When you pass user-defined types as Sub, Function, or property procedure arguments, you must pass them by reference. (ByRef is the default for procedure arguments.) You can then write the following code to use Class1:
Private Sub Command1_Click()
Dim c1A As New Class1
Dim c1B As New Class1
c1A.SetDemoParts 42, 1138, "Howdy"
c1B.Demo = c1A.Demo
c1B.ShowDemo
End Sub
The message box will display 42, 1138, and "Howdy."
Note Because Friend procedures are not part of a class's interface, they are not included when you use the Implements statement to implement multiple interfaces, as described in "Polymorphism."
Adding Events to a Class
Okay, let's say you've created a dinosaur simulation, complete with Stegosaur, Triceratops, and Tyrannosaur classes. As the final touch, you want the Tyrannosaur to roar, and when it does you want every other dinosaur in your simulation to sit up and take notice.
If the Tyrannosaur class had a Roar event, you could handle that event in all your other dinosaur classes. This topic discusses the declaration and handling of events in your class modules.
Note Kids, don't try this at home, at least with more than a few dinosaurs. Connecting every dinosaur with every other dinosaur using events could make your dinosaurs so slow that mammal objects would take over the simulation. Declaring and Raising Events
Assume for the moment that you have a Widget class. Your Widget class has a method that can take a long time to execute, and you'd like your application to be able to put up some kind of completion indicator.
Of course, you could make the Widget object show a percent-complete dialog box, but then you'd be stuck with that dialog box in every project in which you used the Widget class. A good principle of object design is to let the application that uses an object handle the user interface — unless the whole purpose of the object is to manage a form or dialog box.
The Widget's purpose is to perform other tasks, so it's reasonable to give it a PercentDone event, and to let the procedure that calls the Widget's methods handle that event. The PercentDone event can also provide a mechanism for canceling the task.
You can start building the code example for this topic by opening a Standard Exe project, and adding two buttons and a label to Form1. On the Project menu, select Add Class Module to add a class module to the project. Name the objects as shown in the following table.
Object Property Setting Class module Name Widget First Button Caption Start Task Second Button Caption Cancel Label Name
Caption lblPercentDone
"0"
The Widget Class
You declare an event in the Declarations section of a class module, using the Event keyword. An event can have ByVal and ByRef arguments, as the Widget's PercentDone event demonstrates:
Option Explicit
Public Event PercentDone(ByVal Percent As Single, _
ByRef Cancel As Boolean)
When the calling object receives a PercentDone event, the Percent argument contains the percentage of the task that's complete. The ByRef Cancel argument can be set to True to cancel the method that raised the event.
Note You can declare event arguments just as you do arguments of procedures, with the following exceptions: Events cannot have named arguments, optional arguments, or ParamArray arguments. Events do not have return values. Raising the PercentDone Event
The PercentDone event is raised by the LongTask method of the Widget class. The LongTask method takes two arguments: the length of time the method will pretend to be doing work, and the minimum time interval before LongTask pauses to raise the PercentDone event.
Public Sub LongTask(ByVal Duration As Single, _
ByVal MinimumInterval As Single)
Dim sngThreshold As Single
Dim sngStart As Single
Dim blnCancel As Boolean
' The Timer function returns the fractional number
' of seconds since Midnight, as a Single.
sngStart = Timer
sngThreshold = MinimumInterval
Do While Timer < (sngStart + Duration)
' In a real application, some unit of work would
' be done here each time through the loop.
If Timer > (sngStart + sngThreshold) Then
RaiseEvent PercentDone( _
sngThreshold / Duration, blnCancel)
' Check to see if the operation was canceled.
If blnCancel Then Exit Sub
sngThreshold = sngThreshold + MinimumInterval
End If
Loop
End Sub
Every MinimumInterval seconds, the PercentDone event is raised. When the event returns, LongTask checks to see if the Cancel argument was set to True.
Note For simplicity, LongTask assumes you know in advance how long the task will take. This is almost never the case. Dividing tasks into chunks of even size can be difficult, and often what matters most to users is simply the amount of time that passes before they get an indication that something is happening.
Handling an Object's Events
An object that raises events is called an event source. To handle the events raised by an event source, you can declare a variable of the object's class using the WithEvents keyword.An object that raises events is called an event source. To handle the events raised by an event source, you can declare a variable of the object's class using the WithEvents keyword.
This topic continues the Widget object example begun in "Declaring and Raising Events." To handle the PercentDone event of a Widget, place the following code in the Declarations section of Form1:
Option Explicit
Private WithEvents mWidget As Widget
Private mblnCancel As Boolean
The WithEvents keyword specifies that the variable mWidget will be used to handle an object's events. You specify the kind of object by supplying the name of the class from which the object will be created.
The variable mWidget is declared in the Declarations section of Form1 because WithEvents variables must be module-level variables. This is true regardless of the type of module you place them in.
The variable mblnCancel will be used to cancel the LongTask method.
Limitations on WithEvents Variables
You should be aware of the following limitations on the use of WithEvents variables:
As soon as you declare a variable WithEvents, the variable name appears in the left-hand drop down of the module's code window. When you select mWidget, the Widget class's events will appear in the right-hand drop down, as shown in Figure
Figure An event associated with a WithEvents variable
Selecting an event will display the corresponding event procedure, with the prefix mWidget_. All the event procedures associated with a WithEvents variable will have the variable name as a prefix. Add the following code to the mWidget_PercentDone event procedure.
Private Sub mWidget_PercentDone(ByVal Percent As _
Single, Cancel As Boolean)
lblPercentDone.Caption = CInt(100 * Percent) & "%"
DoEvents
If mblnCancel Then Cancel = True
End Sub
Whenever the PercentDone event is raised, the event procedure displays the percent complete in a Label control. The DoEvents statement allows the label to repaint, and also gives the user the opportunity to click the Cancel button. Add the following code for the Click event of the button whose caption is Cancel. Private Sub Command2_Click()
mblnCancel = True
End Sub
If the user clicks the Cancel button while LongTask is running, the Command2_Click event will be executed as soon as the DoEvents statement allows event processing to occur. The module-level variable mblnCancel is set to True, and the mWidget_PercentDone event then tests it and sets the ByRef Cancel argument to True. Connecting a WithEvents Variable to an Object
Form1 is all set up to handle a Widget object's events. All that remains is to find a Widget somewhere.
When you declare a variable WithEvents at design time, there is no object associated with it. A WithEvents variable is just like any other object variable. You have to create an object and assign a reference to the object to the WithEvents variable. Add the following code to the Form_Load event procedure to create the Widget.
Private Sub Form_Load()
Set mWidget = New Widget
End Sub
When the code above is executed, Visual Basic creates a Widget and connects its events to the event procedures associated with mWidget. From that point on, whenever the Widget raises its PercentDone event, the mWidget_PercentDone event procedure will be executed.
To call the LongTask method, add the following code to the Click event of the button whose caption is Start Task.
' Start Task button.
Private Sub Command1_Click()
mblnCancel = False
lblPercentDone.Caption = "0%"
lblPercentDone.Refresh
Call mWidget.LongTask(14.4, 0.66)
If Not mblnCancel Then lblPercentDone.Caption = 100
End Sub
Before the LongTask method is called, the label that displays the percent complete must be initialized, and the module-level Boolean flag for canceling the method must be set to False.
LongTask is called with a task duration of 14.4 seconds. The PercentDone event is to be raised once every two-thirds of a second. Each time the event is raised, the mWidget_PercentDone event procedure will be executed.
When LongTask is done, mblnCancel is tested to see if LongTask ended normally, or if it stopped because mblnCancel was set to True. The percent complete is updated only for the former case.
Running the Program
Press F5 to put the project in Run mode. Click the Start Task button. Each time the PercentDone event is raised, the label is updated with the percentage of the task that's complete. Click the Cancel button to stop the task. Notice that the appearance of the Cancel button doesn't change immediately when you click it. The Click event can't happen until the DoEvents statement allows event processing.
You may find it instructive to run the program with F8, and step through the code a line at a time. You can clearly see how execution enters LongTask, and then re-enters Form1 briefly each time the PercentDone event is raised.
What would happen if, while execution was back in Form1's code, the LongTask method was called again? Confusion, chaos, and eventually (if it happened every time the event was raised) a stack overflow.
Handling Events for a Different Widget You can cause the variable mWidget to handle events for a different Widget object by assigning a reference to the new Widget to mWidget. In fact, you can make the code in Command1 do this every time you click the button, by adding two lines of code:
Set mWidget = New Widget '<- New line.
Call mWidget.LongTask(14.4, 0.66)
Set mWidget = Nothing '<- New line.
The code above creates a new Widget each time the button is pressed. As soon as the LongTask method completes, the reference to the Widget is released by setting mWidget to Nothing, and the Widget is destroyed.
A WithEvents variable can only contain one object reference at a time, so if you assign a different Widget object to mWidget, the previous Widget object's events will no longer be handled. If mWidget is the only object variable containing a reference to the old Widget, the object will be destroyed.
Note You can declare as many WithEvents variables as you need, but arrays of WithEvents variables are not supported.
Terminating Event Handling for a WithEvents Variable
As long as there is a Widget object assigned to the variable mWidget, the event procedures associated with mWidget will be called whenever the Widget raises an event. To terminate event handling, you can set mWidget to Nothing, as shown in the following code fragment.
' Terminate event handling for mWidget.
Set mWidget = Nothing
When a WithEvents variable is set to Nothing, Visual Basic disconnects the object's events from the event procedures associated with the variable.
Important A WithEvents variable contains an object reference, just like any other object variable. This object reference counts toward keeping the object alive. When you are setting all references to an object to Nothing in order to destroy it, don't forget the variables you declared WithEvents.
Comparing WithEvents to Control Events on Forms
You've probably noticed some similarities between the way you use WithEvents variables and the way you handle the events raised by controls on a form. In both cases, when you select the event in the right-hand drop down of a code window, you get an event procedure containing the correct arguments for the event.
In fact, the mechanism is exactly the same. A control is treated as a property of the form class, and the name of that property is the value you assigned to the control's Name property in the Properties window.
It's as if there's a Public module-level variable with the same name as the control, and all of the control's event procedure names begin with that variable name, just as they would with a WithEvents variable.
You can easily see this by declaring the variable mWidget Public instead of Private. The moment you do this, mWidget will show up in the Object Browser as a property of Form1, just like the controls on the form.
The difference between the two cases is that Visual Basic automatically creates instances of all the controls on a form when the form is created, whereas you have to create your own instances of classes whose events you want to handle, and assign references to those objects to WithEvents variables.
Adding an Event to a Form
The following step by step procedure shows how you can create custom events for forms. To try this exercise, open a new Standard Exe project and do the following:
To add an event to Form1
To add an event to a class and then use the event, you must:
If you’ve read the preceding material on creating classes, you know by now that a class is an object that encapsulates data and code, and that the properties of a class are the data that describe an object. You also know that you can use property procedures or public properties to expose the data represented by those properties.
So far, so good — all of the examples thus far have dealt with transient data, that is, data that is created and consumed at run time. For many programs, this may be all that you need, but what if you need to store data between sessions, or utilize data that already exists outside of your program? In order to work with external sources of data, you need to make your class data-aware.
Data-aware classes can be divided into two categories — data consumers and data sources. Class modules have two design-time properties, DataBindingBehavior and DataSourceBehavior, that determine how a class will interact with external data. The BindingCollection object is used to bind data-aware classes to controls or to each other.
Data Sources
A data source is a class that provides data from an external source to be consumed by other objects. A Data control is in reality an instance of a class that is a data source, but classes that have been set up to act as data sources can be much more powerful than a Data control. Unlike the Data control, a data-aware class doesn’t have to have a visual representation, and it isn’t limited to a particular data interface such as Data Access Objects (DAO) or Remote Data Objects (RDO). In fact, a data-aware class can act as a data source for any type of data, including traditional ODBC sources, ActiveX Data Objects (ADO), or any OLE DB provider. The DataSourceBehavior property determines whether or not a class can act as a data source. By setting the DataSourceBehavior to 1 (vbDataSource), your class can act as a source of data for other objects.
Data Consumers
Simply put, a data consumer is a class that can be bound to an external source of data, much as a TextBox control can be bound to a Data control. In earlier versions of Visual Basic, controls were the only objects that could be bound to a data source. Data-aware classes set up as data consumers allow you to bind any object to any object created from a class that has been set up as a data source.
The DataBindingBehavior property allows a class to bind to external data. By setting this property to 1 (vbSimpleBound), an object created from your class will be bound to a single data field in an external data source. By setting the DataBindingBehavior to 2 (vbComplexBound), your class will be bound to a row of data in an external data source. Think of it this way — if your objects were controls, a TextBox control would be simple bound, whereas a grid control would be complex bound.
The BindingCollection Object Just as you would bind a control to a database through a Data control, data-aware classes need a central object to bind them together. That object is the BindingCollection object. Just as it sounds, the BindingCollection is a collection of bindings between a data source and one or more data consumers.
In order to use the BindingCollection object you must first add a reference to the Microsoft Data Binding Collection by selecting it in the References dialog, available from the Project menu. As with any object, you’ll need to create an instance of the BindingCollection object at run time.
The DataSource property of the BindingCollection object is used to specify the object that will provide the data. This object must be a class or UserControl with its DataSourceBehavior property set to vbDataSource.
Once the BindingCollection has been instantiated and its DataSource set, you can use the Add method to define the binding relationships. The Add method takes three required arguments: the name of the consumer object, the property of that object to be bound to the source, and the field from the source that will be bound to the property. You can add multiple bindings to the BindingCollection by repeating the Add method; you can use the Remove method to delete a binding.
Creating a Data Source In this section, we’ll walk step-by-step through the process of creating a data-aware class that acts as a data source. This example will bind a TextBox control to our data source class in order to display the data. The next section, "Creating a Data Consumer," demonstrates how to bind our data source class to a data consumer class.
Creating the Source Class The first step in creating a source class is to define a new class and give it the properties and methods necessary to provide data:
Using the Source Class
Now that the source class is defined, we can do something useful with it. In this example we’ll bind it to a TextBox control so that we can see its output; we’ll also use a CommandButton to execute our Cycle method.
Polymorphism
Polymorphism and Performance
Polymorphism is important for performance reasons. To see this, consider the following function:
Public Sub GetFood(ByVal Critter As Object, _
ByVal Food As Object)
Dim dblDistance As Double
' Code to calculate distance to food (omitted).
Critter.Move dblDistance ' Late bound
Critter.Bite Food ' Late bound
End Sub
The Move and Bite methods are late bound to Critter. Late binding happens when Visual Basic can't determine at compile time what kind of object a variable will contain. In this example, the Critter argument is declared As Object, so at run time it could contain a reference to any kind of object — like a Car or a Rock.
Because it can't tell what the object will be, Visual Basic compiles some extra code to ask the object if it supports the method you've called. If the object supports the method, this extra code invokes it; if not, the extra code raises an error. Every method or property call incurs this additional overhead.
By contrast, interfaces allow early binding. When Visual Basic knows at compile time what interface is being called, it can check the type library to see if that interface supports the method. Visual Basic can then compile in a direct jump to the method, using a virtual function table (vtable). This is many times faster than late binding.
Now suppose the Move and Bite methods belong to an Animal interface, and that all animal classes implement this interface. The Critter argument can now be declared As Animal, and the Move and Bite methods will be early bound:
Public Sub GetFood(ByVal Critter As Animal, _
ByVal Food As Object)
Dim dblDistance As Double
' Code to calculate distance to food (omitted).
Critter.Move dblDistance ' Early bound (vtable).
Critter.Bite Food ' Early bound (vtable).
End Sub
Creating and Implementing an Interface
As explained in "How Visual Basic Provides Polymorphism," an interface is a set of properties and methods. In the following code example, you'll create an Animal interface and implement it in two classes, Flea and Tyrannosaur.
You can create the Animal interface by adding a class module to your project, naming it Animal, and inserting the following code:
Public Sub Move(ByVal Distance As Double)
End Sub
Public Sub Bite(ByVal What As Object)
End Sub
Notice that there's no code in these methods. Animal is an abstract class, containing no implementation code. An abstract class isn't meant for creating objects — its purpose is to provide the template for an interface you add to other classes. (Although, as it turns out, sometimes it's useful to implement the interface of a class that isn't abstract; this is discussed later in this topic.)
Note Properly speaking, an abstract class is one from which you can't create objects. You can always create objects from Visual Basic classes, even if they contain no code; thus they are not truly abstract. Now you can add two more class modules, naming one of them Flea and the other Tyrannosaur. To implement the Animal interface in the Flea class, you use the Implements statement:
Option Explicit
Implements Animal
As soon as you've added this line of code, you can click the left-hand (Object) drop down in the code window. One of the entries will be Animal. When you select it, the right-hand (Procedure) drop down will show the methods of the Animal interface.
Select each method in turn, to create empty procedure templates for all the methods. The templates will have the correct arguments and data types, as defined in the Animal class. Each procedure name will have the prefix Animal_ to identify the interface
Important An interface is like a contract. By implementing the interface, a class agrees to respond when any property or method of the interface is invoked. Therefore, you must implement all the properties and methods of an interface.
You can now add the following code to the Flea class:
Private Sub Animal_Move(ByVal Distance As Double)
' (Code to jump some number of inches omitted.)
Debug.Print "Flea moved"
End Sub
Private Sub Animal_Bite(ByVal What As Object)
' (Code to suck blood omitted.)
Debug.Print "Flea bit a " & TypeName(What)
End Sub
You may be wondering why the procedures are declared Private. If they were Public, the procedures Animal_Jump and Animal_Bite would be part of the Flea interface, and we'd be stuck in the same bind we were in originally, declaring the Critter argument As Object so it could contain either a Flea or a Tyrannosaur.
Multiple Interfaces The Flea class now has two interfaces: The Animal interface you've just implemented, which has two members, and the default Flea interface, which has no members. Later in this example you'll add a member to one of the default interfaces.
You can implement the Animal interface similarly for the Tyrannosaur class:
Option Explicit
Implements Animal
Private Sub Animal_Move(ByVal Distance As Double)
' (Code to pounce some number of yards omitted.)
Debug.Print "Tyrannosaur moved"
End Sub
Private Sub Animal_Bite(ByVal What As Object)
' (Code to take a pound of flesh omitted.)
Debug.Print "Tyrannosaur bit a " & TypeName(What)
End Sub
Exercising the Tyrannosaur and the Flea
Add the following code to the Load event of Form1:
Private Sub Form_Load()
Dim fl As Flea
Dim ty As Tyrannosaur
Dim anim As Animal
Set fl = New Flea
Set ty = New Tyrannosaur
' First give the Flea a shot.
Set anim = fl
Call anim.Bite(ty) 'Flea bites dinosaur.
' Now the Tyrannosaur gets a turn.
Set anim = ty
Call anim.Bite(fl) 'Dinosaur bites flea.
End Sub
Press F8 to step through the code. Notice the messages in the Immediate window. When the variable anim contains a reference to the Flea, the Flea's implementation of Bite is invoked, and likewise for the Tyrannosaur.
The variable anim can contain a reference to any object that implements the Animal interface. In fact, it can only contain references to such objects. If you attempt to assign a Form or PictureBox object to anim, an error will occur.
The Bite method is early bound when you call it through anim, because Visual Basic knows at compile time that whatever object is assigned to anim will have a Bite method.
Passing Tyrannosaurs and Fleas to Procedures Remember the GetFood procedure from "How Visual Basic Provides Polymorphism?" You can add the second version of the GetFood procedure — the one that illustrates polymorphism — to Form1, and replace the code in the Load event with the following:
Private Sub Form_Load()
Dim fl As Flea
Dim ty As Tyrannosaur
Set fl = New Flea
Set ty = New Tyrannosaur
'Flea dines on dinosaur.
Call GetFood(fl, ty)
' And vice versa.
Call GetFood(ty, fl)
End Sub
Stepping through this code shows how an object reference that you pass to an argument of another interface type is converted into a reference to the second interface (in this case, Animal). What happens is that Visual Basic queries the object to find out whether it supports the second interface. If the object does, it returns a reference to the interface, and Visual Basic places that reference in the argument variable. If the object does not support the second interface, an error occurs. Implementing Methods That Return Values
Suppose the Move method returned a value. After all, you know how far you want an Animal to move, but an individual specimen might not be able to move that far. It might be old and decrepit, or there might be a wall in the way. The return value of the Move method could be used to tell you how far the Animal actually moved.
Public Function Move(ByVal Distance As Double) _
As Double
End Function
When you implement this method in the Tyrannosaur class, you assign the return value to the procedure name, just as you would for any other Function procedure:
Private Function Animal_Move(ByVal Distance _
As Double) As Double
Dim dblDistanceMoved As Double
' Code to calculate how far to pounce (based on
' age, state of health, and obstacles) is omitted.
' This example assumes that the result has been
' placed in the variable dblDistanceMoved.
Debug.Print "Tyrannosaur moved"; dblDistanceMoved
Animal_Move = dblDistanceMoved
End Function
To assign the return value, use the full procedure name, including the interface prefix.
Implementing Properties
This topic continues the code example begun in "Creating and Implementing an Interface," adding properties to the Animal interface that was implemented in the Flea and Tyrannosaur classes. You may find it helpful to read that topic before beginning this one.
Suppose we give the Animal class an Age property, by adding a Public variable to the Declarations section:
Option Explicit
Public Age As Double
The Procedure drop downs in the code modules for the Tyrannosaur and Flea classes now contain property procedures for implementing the Age property, as shown in Figure
Figure:Implementing property procedures
This illustrates a point made in "Adding Properties to a Class" earlier in this chapter. Using a public variable to implement a property is strictly a convenience for the programmer. Behind the scenes, Visual Basic implements the property as a pair of property procedures.
You must implement both procedures. The property procedures are easily implemented by storing the value in a private data member, as shown here:
Private mdblAge As Double
Private Property Get Animal_Age() As Double
Animal_Age = mdblAge
End Property
Private Property Let Animal_Age(ByVal RHS As Double)
mdblAge = RHS
End Property
The private data member is an implementation detail, so you have to add it yourself.
Note When Implements provides the template for a Property Set or Property Let, it has no way of determining the name of the last argument, so it substitutes the name RHS, as shown in the code example above. There's no data validation on a property implemented as a public data member, but that doesn't mean you can't add validation code to the Property Let for Animal_Age. For example, you might want to restrict the values to ages appropriate for a Tyrannosaur or a Flea, respectively.
In fact, this shows the independence of interface and implementation. As long as the interface matches the description in the type library, the implementation can be anything.
Before you go on to the next step, remove the implementation of the read-write Age property from both class modules.
Implementing a Read-Only Property
Of course, allowing the age of an animal to be set arbitrarily is bad object design. The object should know its own age, and provide it to the user as a read-only property. Remove the public variable Age from the Animal class, and add the template for a read-only age property, like this:
Public Property Get Age() As Double
End Property
Now the Procedure drop downs in the code windows for the Tyrannosaur and Flea classes contain only a single entry, Age [PropertyGet]. You might implement this for the Tyrannosaur as follows:
Private mdblBirth As Double
Private Property Get Animal_Age() As Double
Animal_Age = Now - mdblBirth
End Property
The code above returns the age of the Tyrannosaur in days. You could set mdblBirth in the Initialize event of the Tyrannosaur class, as here:
Private Sub Class_Initialize()
mdblBirth = Now
End Sub
And of course you could return the property value in more commonly used units, such as dog years.
Time Out for a Brief Discussion of Objects and Interfaces
This topic completes the code example begun in "Creating and Implementing an Interface," and continued in "Implementing Properties." You may find it helpful to read those topics before beginning this one.
The Tyrannosaur and Flea code example seems to play fast and loose with interfaces and objects. References to objects are assigned to one object variable, and references to interfaces to another.
In fact, all of the references are object references. A reference to an interface is also a reference to the object that implements the interface. Furthermore, an object may have multiple interfaces, but it's still the same object underneath.
In Visual Basic, each class has a default interface that has the same name as the class. Well, almost the same. By convention, an underscore is prefixed to the class name. The underscore indicates that this interface is hidden in the type library.
Thus the Tyrannosaur class has a default interface called _Tyrannosaur. Because Tyrannosaur also implements Animal, the class has a second interface named Animal.
However, underneath it all, the object is still a Tyrannosaur. Place a command button on Form1, and add the following code:
Private Sub Command1_Click()
Dim ty As Tyrannosaur
Dim anim As Animal
Set ty = New Tyrannosaur
Set anim = ty
MsgBox TypeName(anim)
End Sub
You might expect the message box to display "Animal," but in fact it displays "Tyrannosaur."
Querying for Interfaces
When you assign a Tyrannosaur object to variable of type Animal, Visual Basic asks the Tyrannosaur object if it supports the Animal interface. (The method used for this is called QueryInterface, or QI for short; you may sometimes hear QI used as a verb.) If the answer is no, an error occurs.
If the answer is yes, the object is assigned to the variable. Only the methods and properties of the Animal interface can be accessed through this variable.
Generic Object Variables and Interfaces What happens if you assign the object reference to a generic object variable, as in the following code?
Private Sub Command1_Click()
Dim ty As Tyrannosaur
Dim anim As Animal
Dim obj As Object
Set ty = New Tyrannosaur
Set anim = ty
Set obj = anim
MsgBox TypeName(obj)
End Sub
The result is again Tyrannosaur. Now, what interface do you get when you call properties and methods through the variable obj? Add the following method to the Tyrannosaur class:
Public Sub Growl()
Debug.Print "Rrrrrr"
End Sub
The Growl method belongs to the Tyrannosaur object's default interface. In the code for the command button's Click event, replace the MsgBox statement with the following two lines of code:
obj.Move 42
obj.Growl
When you run the project and click the button, execution stops on the Growl method, with the error "Object does not support this property or method." Clearly, the interface is still Animal.
This is something to bear in mind when using variables of type Object with objects that have multiple interfaces. The interface the variable will access is the last interface assigned. For example:
Private Sub Command1_Click()
Dim ty As Tyrannosaur
Dim anim As Animal
Dim obj As Object
Set ty = New Tyrannosaur
Set anim = ty
Set obj = anim
obj.Move 42 ' Succeeds
obj.Growl ' Fails
Set obj = ty
obj.Move 42 ' Fails
obj.Growl ' Succeeds
End Sub
Fortunately, there's very little reason to use the slower, late-bound Object data type with objects that have multiple interfaces. One of the main reasons for using multiple interfaces is to gain the advantage of early binding through polymorphism.
Other Sources of Interfaces Visual Basic class modules are not your only source of interfaces to implement. You can implement any interface contained in a type library, as long as that interface supports Automation.
If you have the Professional or Enterprise Edition of Visual Basic, you can create your own type libraries of abstract classes. These type libraries can be used in many projects, as described in "General Principles of Component Design" in Creating ActiveX Components in the Component Tools Guide.
The Professional and Enterprise editions also include the MkTypLib (Make Type Library) utility in the Tools directory. If you've used this utility with Microsoft Visual C++, you may find it a more congenial way to create interfaces.
Using Interfaces in Your Project To use an interface in your project, click References on the Project menu to open the References dialog box. If the type library is registered, it will appear in the list of references, and you can check it. If the type library is not in the list, you can use the Browse button to locate it.
Once you have a reference to a type library, you can use Implements to implement any Automation interfaces the type library contains.
The Many (Inter)Faces of Code Reuse There are two main forms of code reuse — binary and source. Binary code reuse is accomplished by creating and using an object, while source code reuse is achieved by inheritance, which isn't supported by Visual Basic. (Source code reuse can also be achieved by copying and modifying the source code, but this technique is nothing new, and has many well-known problems.)
Visual Basic has been a pioneer of binary code reuse — controls being the classic example. You reuse the code in a control by placing an instance of the control on your form. This is known as a containment relationship or a has-a relationship; that is, the form contains or has a CommandButton.
Delegating to an Implemented Object Implements provides a powerful new means of code reuse. You can implement an abstract class (as discussed in "Creating and Implementing an Interface"), or you can implement the interface of a fully functional class. You can create the inner object (that is, the implemented object) in the Initialize event of the outer object (that is, the one that implements the inner object's interface).
As noted in "Creating and Implementing an Interface," an interface is like a contract — you must implement all the members of the inner object's interface in the outer object's class module. However, you can be very selective in the way you delegate to the properties and methods of the inner object. In one method you might delegate directly to the inner object, passing the arguments unchanged, while in another method you might execute some code of your own before calling the inner object — and in a third method you might execute only your own code, ignoring the inner object altogether!
For example, suppose you have a OneManBand class and a Cacophony class, both of which generate sounds. You'd like to add the functionality of the Cacophony class to the OneManBand class, and reuse some of the implementation of the Cacophony class's methods.
' OneManBand implements the Cacophony interface.
Implements Cacophony
' Object variable to keep the reference in.
Private mcac As Cacophony
Private Sub Class_Initialize()
' Create the object.
Set mcac = New Cacophony
End Sub
You can now go to the Object drop down and select Cacophony, and then get procedure templates for the methods of the Cacophony interface. To implement these methods, you can delegate to the Cacophony object. For example, the Beep method might look like this:
Private Sub Cacophony_Beep(ByVal Frequency As Double, _
ByVal Duration As Double)
' Delegate to the inner Cacophony object.
Call mcac.Beep(Frequency, Duration)
End Sub
The implementation above is very simple. The outer object (OneManBand) delegates directly to the inner (Cacophony), reusing the Cacophony object's Beep method without any changes. This is a good thing, but it's only the beginning.
The Implements statement is a very powerful tool for code reuse, because it gives you enormous flexibility. You might decide to alter the effects of the OneManBand class's Beep method, by inserting your own code before (or after) the call to the inner Cacophony object:
Private Sub Cacophony_Beep(ByVal Frequency As Double, _
ByVal Duration As Double)
' Bump everything up an octave.
Frequency = Frequency * 2
' Based on another property of the OneManBand
' class, Staccato, cut the duration of each beep.
If Staccato Then Duration = Duration * 7 / 8
Call mcac.Beep(Frequency, Duration)
' You can even call other methods of OneManBand.
If Staccato Then Pause(Duration * 1 / 8)
End Sub
For some of the methods, your implementation may delegate directly to the inner Cacophony object, while for others you may interpose your own code before and after delegating — or even omit delegation altogether, using entirely your own code to implement a method.
Because the OneManBand class implements the Cacophony interface, you can use it with any musical application that calls that interface. Your implementation details are hidden from the calling application, but the resulting sounds are all your own.
Note COM provides another mechanism for binary code reuse, called aggregation. In aggregation, an entire interface is reused, without any changes, and the implementation is provided by an instance of the class being aggregated. Visual Basic does not support this form of code reuse.
Doesn't This Get Tedious? Writing delegation code can indeed become tedious, especially if most of the outer object's properties and methods simply delegate directly to the corresponding properties and methods of the inner object.
If you have the Professional or Enterprise Edition of Visual Basic, you can use the Visual Basic Extensibility model to create your own delegation wizard to automate the task, similar to the Class Wizard that's included in the Professional and Enterprise editions.
Object Models
The simplest way to keep track of objects is to declare an object variable for each object you plan to create. Of course, this places a limit on the number of objects you can create.
You can keep multiple object references in an array or a collection, as discussed in "Creating Arrays of Objects" and "Creating Collections of Objects" earlier in this chapter.
In the beginning, you'll probably locate object variables, arrays, and collections in forms or standard modules, as you do with ordinary variables. As you add more classes, though, you'll probably discover that the objects you're using have clear relationships to each other.
Object Models Express Containment Relationships Object models give structure to an object-based program. By defining the relationships between the objects you use in your program, an object model organizes your objects in a way that makes programming easier.
Typically, an object model expresses the fact that some objects are "bigger," or more important than others — these objects can be thought of as containing other objects, or as being made up of other objects.
For example, you might create a SmallBusiness object as the core of your program. You might want the SmallBusiness object to have other types of objects associated with it, such as Employee objects and Customer objects. You would probably also want it to contain a Product object. An object model for this program is shown in Figure
Figure :An object model
You can define four class modules, named SmallBusiness, Employee, Customer, and Product, and give them each appropriate properties and methods, but how do you make the connections between objects? You have two tools for this purpose: Object properties and the Collection object. The following code fragment shows one way to implement the hierarchy in Figure 9.11.
' Code for the Declarations section of the
' SmallBusiness class module.
Public Name As String
Public Product As New Product
Public Employees As New Collection
Public Customers As New Collection
The first time you refer to the Product property, the object will be created, because it was declared As New. For example, the following code might create and set the name and price of the SmallBusiness object's Product object.
' Code for a standard module.
Public sbMain As New SmallBusiness
Sub Main
sbMain.Name = "Velociraptor Enterprises, Inc."
' The first time the Product variable is used in
' code, the Product object is created.
sbMain.Product.Name = "Inflatable Velociraptor"
sbMain.Product.Price = 1.98
.
. ' Code to initialize and show main form.
.
End Sub
Note Implementing an object property with public variables is sloppy. You could inadvertently destroy the Product object by setting the property to Nothing somewhere in your code. It's better to create object properties as read-only properties, as shown in the following code fragment.
' Code for a more robust object property. Storage for
' the property is private, so it can't be set to
' Nothing from outside the object.
Private mProduct As New Product
Property Get Product() As Product
' The first time this property is called, mProduct
' contains Nothing, so Visual Basic will create a
' Product object.
Set Product = mProduct
End If
One-to-Many Object Relationships Object properties work well when the relationship between objects is one-to-one. It frequently happens, however, that an object of one type contains a number of objects of another type. In the SmallBusiness object model, the Employees property is implemented as a Collection object, so that the SmallBusiness object can contain multiple Employee objects. The following code fragment shows how new Employee objects might be added to this collection.
Public Function NewEmployee(Name, Salary, HireDate, _
ID) As Employee
Dim empNew As New Employee
empNew.Name = Name ' Implicit object creation.
empNew.Salary = Salary
empNew.HireDate = HireDate
' Add to the collection, using the ID as a key.
sbMain.Employees.Add empNew, CStr(ID)
' Return a reference to the new Employee.
Set NewEmployee = empNew
End Function
The NewEmployee function can be called as many times as necessary to create employees for the business represented by the SmallBusiness object. The existing employees can be listed at any time by iterating over the Employees collection.
Note Once again, this is not a very robust implementation. Better practice is to create your own collection classes, and expose them as read-only properties. This is discussed in "Creating Your Own Collection Classes." Tip The Class Builder utility, included in the Professional and Enterprise editions of Visual Basic, can generate much of the code you need to implement an object model. Class Builder creates robust object properties and collection classes, and allows you to rearrange your model easily.
Parent Properties
When you have a reference to an object, you can get to the objects it contains by using its object properties and collections. It's also very useful to be able to navigate up the hierarchy, to get to the object that contains the object you have a reference to.
Navigating upward is usually done with Parent properties. The Parent property returns a reference to the object's container. For a discussion of object model navigation, see "Navigating Object Models" in "Programming with Components."
You can find an example of a Parent property in "Adding Properties to Classes" earlier in this chapter.
Tip When you assign a Parent property to an object in a collection, don't use a reference to the Collection object. The real parent of the object is the object that contains the collection. If the Parent property points to the collection, you'll have to use two levels of indirection to get to the real parent — that is, obj.Parent.Parent instead of obj.Parent. Parent Properties, Circular References, and Object Teardown
One of the biggest problems with Parent properties is that they create circular references. The "larger" object has a reference to the object it contains, and the contained object has a reference through its Parent property, creating a loop as shown in Figure
What's wrong with this picture? The way you get rid of objects when you're done with them is to release all references to them. Assuming the reference to the SmallBusiness object is in a variable named sbMain, as earlier in this topic, you might write the following code:
Set sbMain = Nothing
Unfortunately, there's still a reference to the SmallBusiness object — in fact, there may be many references, because each Employee object's Parent property will hold a reference to the SmallBusiness object.
Since the SmallBusiness object's Employees collection holds a reference to each Employee object, none of the objects ever get destroyed.
TearDown Methods One solution is to give the SmallBusiness object a TearDown method. This could set all of the SmallBusiness object's object properties to Nothing, and also set all the Collection objects (Employees, Customers) to Nothing.
When a Collection object is destroyed, Visual Basic sets all the object references it was holding to Nothing. If there are no other references to the Employee and Customer objects that were contained in the Employees and Customers collections, they'll be destroyed.
Of course, if the Employee object is made up of finer objects, it will have the same circular reference problem its parent does. In that case, you'll have to give the Employee class a TearDown method. Instead of just setting the Employees Collection object to Nothing, the SmallBusiness object will first have to iterate through the collection, calling the TearDown method of each Employee object.
It's Not Over Yet Even then, not all the objects may be destroyed. If there are variables anywhere in your program that still contain references to the SmallBusiness object, or to any of the objects it contains, those objects won't be destroyed. Part of the cleanup for your program must be to ensure that all object variables everywhere are set to Nothing.
To test whether this is happening, you may want to add some debugging code to your objects. For example, you can add the following code to a standard module:
' Global debug collection
Public gcolDebug As New Collection
' Global function to give each object a unique ID.
Public Function DebugSerial() As Long
Static lngSerial As Long
lngSerial = lngSerial + 1
DebugSerial = lngSerial
End Function
In each class module, you can put code similar to the following. Each class provides its own name where "Product" appears.
' Storage for the debug ID.
Private mlngDebugID As Long
Property Get DebugID() As Long
DebugID = mlngDebugID
End Property
Private Sub Class_Initialize()
mlngDebugID = DebugSerial
' Add a string entry to the global collection.
gcolDebug.Add "Product Initialize; DebugID=" _
& DebugID, CStr(DebugID)
End Sub
Private Sub Class_Terminate()
' Remove the string entry, so you know the object
' isn't around any more.
gcolDebug.Remove CStr(DebugID)
End Sub
As each object is created, it places a string in the global collection; as it's destroyed it removes the string. At any time, you iterate over the global collection to see what objects haven't been destroyed.
Public Collection Example: The House of Straw
To create the example, open a new project and insert two class modules. Draw five command buttons, a list box, two text boxes, and two labels on the form, as shown in Figure
The following table lists the property values you need to set for this example.
Object Property Setting Class module Name Employee Class module Name SmallBusiness Form Caption Employees Collection First command button Caption
Name Add
cmdAddEmployee Second command button Caption
Name Delete
cmdDeleteEmployee Third command button Caption
Name Refresh List
cmdListEmployees Fourth command button Caption
Name Trouble
cmdTrouble Fifth command button Caption
Name Close
cmdClose First label control Caption Name Second label control Caption Salary First text box Name
Text txtName
(blank) Second text box Name
Text txtSalary
(blank) List Box Name lstEmployees In the Employee class module, add the following declarations and property procedures:
Option Explicit
' Properties of the Employee class.
Public Name As String
Public Salary As Long
' Private data for the write-once ID property.
Private mstrID As String
Property Get ID() As String
ID = mstrID
End Property
' The first time the ID property is set, the static
' Boolean is also set. Subsequent calls do nothing.
' (It would be better to raise an error, instead.)
Property Let ID(strNew As String)
Static blnAlreadySet As Boolean
If Not blnAlreadySet Then
blnAlreadySet = True
mstrID = strNew
End If
End Property
The ID property is the key for retrieving or deleting an Employee object from the collection, so it must be set once and never changed. This is accomplished with a Static Boolean variable that is set to True the first time the property is set. The property can always be read, because there is a Property Get.
In the SmallBusiness class module, add the following declaration. The collection object will be created the first time the Employees variable is referred to in code.
Option Explicit
Public Employees As New Collection
The Form Does All the Work All of the remaining code goes into the form module. Add the following declaration in the Declarations section.
Option Explicit
Public sbMain As New SmallBusiness
The code in the cmdEmployeeAdd_Click event adds a member to the collection.
Private Sub cmdEmployeeAdd_Click()
Dim empNew As New Employee
Static intEmpNum As Integer
' Using With makes your code faster and more
' concise (.ID vs. empNew.ID).
With empNew
' Generate a unique ID for the new employee.
intEmpNum = intEmpNum + 1
.ID = "E" & Format$(intEmpNum, "00000")
.Name = txtName.Text
.Salary = CDbl(txtSalary.Text)
' Add the Employee object reference to the
' collection, using the ID property as the key.
sbMain.Employees.Add empNew, .ID
End With
txtName.Text = ""
txtSalary.Text = ""
' Click the Refresh List button.
cmdListEmployees.Value = True
End Sub
The code in the cmdListEmployees_Click event procedure uses a For Each ... Next statement to add all the employee information to the ListBox control.
Private Sub cmdListEmployees_Click()
Dim emp As Employee
lstEmployees.Clear
For Each emp In sbMain.Employees
lstEmployees.AddItem emp.ID & ", " & emp.Name _
& ", " & emp.Salary
Next
End Sub
The cmdEmployeeDelete_Click event uses the Collection object's Remove method to delete the collection member currently selected in the ListBox control.
Private Sub cmdEmployeeDelete_Click()
' Check to make sure there's an employee selected.
If lstEmployees.ListIndex > -1 Then
' The first six characters are the ID.
sbMain.Employees.Remove _
Left(lstEmployees.Text, 6)
End If
' Click the Refresh List button.
cmdListEmployees.Value = True
End Sub
Add the following code to the Trouble button.
Private Sub cmdTrouble_Click()
' Say what!?
sbMain.Employees.Add Me
End Sub
The cmdClose_Click event closes the application. When you close projects that use objects, do so by unloading all the forms, to ensure that any Terminate event procedures in your class modules will get executed. By contrast, using the End statement stops a program abruptly, without executing Terminate events.
Private Sub cmdClose_Click()
Unload Me
End Sub
To add employees in the example, run the application, enter values in the two text boxes, and then choose the Add button. Add a few employees, and then experiment with the delete and list buttons.
Robust as a Straw House This simple implementation is not very robust. Because the Employees property is just a public Collection object, you could inadvertently access it from anywhere in your program. Furthermore, the Add method of the Collection object doesn't do any type checking. For example, the code in the Trouble button's Click event blithely inserts an object reference to the form into the collection of employees.
Click the Trouble button, and notice that no error occurs. Now click the Refresh List button. When the For Each ... Next loop encounters the unexpected object type, it causes error 13, Type mismatch.
This is an example of the kind of error you're exposed to when you build an object model with public Collection objects. Objects can be added from anywhere in your project, and there's no guarantee that they'll be properly initialized. If a programmer clones the code to add an employee, and the original code is later changed, the resulting errors can be very difficult to track down.
The Project Explorer
As you create, add, or remove editable files from a project, Visual Basic reflects your changes in the Project Explorer window, which contains a current list of the files in the project.
The Project Explorer window
The Project File
Each time you save a project, Visual Basic updates the project file (.vbp). A project file contains the same list of files that appears in the Project Explorer window, as well as references to the ActiveX controls and insertable objects that are used in the project.
You can open an existing project file by double-clicking its icon, by choosing the Open Project command from the File menu, or by dragging the file and dropping it on the Project Explorer window.
The Structure of a Visual Basic Project
Form Modules
Form modules (.frm file name extension) can contain textual descriptions of the form and its controls, including their property settings. They can also contain form-level declarations of constants, variables, and external procedures; event procedures; and general procedures. Class Modules
Class modules (.cls file name extension) are similar to form modules, except that they have no visible user interface. You can use class modules to create your own objects, including code for methods and properties. Standard Modules
Standard modules (.bas file name extension) can contain public or module-level declarations of types, constants, variables, external procedures, and public procedures. Resource Files
Resource files (.res file name extension) contain bitmaps, text strings, and other data that you can change without having to re-edit your code. For example, if you plan to localize your application in a foreign language, you can keep all of the user-interface text strings and bitmaps in a resource file, which you can then localize instead of the entire application. A project can contain no more than one resource file. ActiveX Documents
ActiveX documents (.dob) are similar to forms, but are displayable in an Internet browser such as Internet Explorer. The Professional and Enterprise editions of Visual Basic are capable of creating ActiveX documents. User Control and Property Page Modules
User Control (.ctl) and Property Page (.pag) modules are also similar to forms, but are used to create ActiveX controls and their associated property pages for displaying design-time properties. The Professional and Enterprise editions of Visual Basic are capable of creating ActiveX controls. Components
In addition to files and modules, several other types of components can be added to the project. ActiveX Controls
ActiveX controls (.ocx file name extension) are optional controls which can be added to the toolbox and used on forms. When you install Visual Basic, the files containing the controls included with Visual Basic are copied to a common directory (the \Windows\System subdirectory under Windows 95 or later). Additional ActiveX controls are available from a wide variety of sources. You can also create your own controls using the Professional or Enterprise editions of Visual Basic. Insertable Objects
Insertable objects, such as a Microsoft Excel Worksheet object, are components you can use as building blocks to build integrated solutions. An integrated solution can contain data in different formats, such as spreadsheets, bitmaps, and text, which were all created by different applications.
Creating, Opening, and Saving Projects
New Project
Closes the current project, prompting you to save any files that have changed. You can select a type of project from the New Project dialog. Visual Basic then creates a new project with a single new file.
Open Project
Closes the current project, prompting you to save any changes. Visual Basic then opens an existing project, including the forms, modules, and ActiveX controls listed in its project (.vbp) file.
Save Project
Updates the project file of the current project and all of its form, standard, and class modules.
Save Project As
Updates the project file of the current project, saving the project file under a file name that you specify. Visual Basic also prompts you to save any forms or modules that have changed.
Adding, Removing, and Saving Files
To add a file to a project
When you add a file to a project, you are simply including a reference to the existing file in the project; you are not adding a copy of the file. Therefore, if you make changes to a file and save it, your changes will affect any project that includes the file. To change a file without affecting other projects, select the file in the Project Explorer, choose Save filename As from the File menu, and then save the file under a new file name.
To remove a file from a project
The set of controls available in the toolbox can be customized for each project. Any given control must be in the toolbox before you can add it to a form in the project. The basic set of standard controls that always appear in the toolbox is described in "Forms, Controls, and Menus."
To add a control to a project's toolbox
The Components dialog box
To add ActiveX controls to the Components dialog box, choose the Browse button, and search other directories for files with a .ocx file name extension. When you add an ActiveX control to the list of available controls, Visual Basic automatically selects the check box.
Note Each ActiveX control is accompanied by a file with an .oca extension. This file stores cached type library information and other data specific to the control. The .oca files are typically stored in the same directory as the ActiveX controls and are recreated as needed (file sizes and dates may change).
Removing Controls from a Project
Making and Running an Executable File
To make an executable file in Visual Basic
Programming Fundamentals
This chapter introduces the essential components of the Visual Basic language. After creating the interface for your application using forms and controls, you will need to write the code that defines the application's behavior. As with any modern programming language, Visual Basic supports a number of common programming constructs and language elements.
Visual Basic is an object-based programming language. The mere mention of objects may cause undue anxiety in many programmers. Don't worry: whether you realize it or not, you've been dealing with objects most of your life. Once you understand a few basic concepts, objects actually help to make programming easier than ever before.
If you've programmed in other languages, much of the material covered in this chapter will seem familiar. While most of the constructs are similar to other languages, the event-driven nature of Visual Basic introduces some subtle differences. Try and approach this material with an open mind; once you understand the differences you can use them to your advantage.
If you're new to programming, the material in this chapter will serve as an introduction to the basic building blocks for writing code. Once you understand the basics, you will be able to create powerful applications using Visual Basic.
The Structure of a Visual Basic Application An application is really nothing more than a set of instructions directing the computer to perform a task or tasks. The structure of an application is the way in which the instructions are organized; that is, where the instructions are stored and the order in which instructions are executed.
Simple applications such as the classic "hello world" example have a simple structure; organization isn't very important with a single line of code. As applications become more complex, the need for organization or structure becomes obvious. Imagine the chaos that would result if your application's code was allowed to execute in random order. In addition to controlling the execution of an application, the structure is important to the programmer: how easily can you find specific instructions within your application?
Because a Visual Basic application is based on objects, the structure of its code closely models its physical representation on screen. By definition, objects contain data and code. The form that you see on screen is a representation of the properties that define its appearance and intrinsic behavior. For each form in an application, there is a related form module (with file name extension .frm) that contains its code
A form and its related form module
Each form module contains event procedures — sections of code where you place the instructions that will execute in response to specific events. Forms can contain controls. For each control on a form, there is a corresponding set of event procedures in the form module. In addition to event procedures, form modules can contain general procedures that are executed in response to a call from any event procedure.
Code that isn't related to a specific form or control can be placed in a different type of module, a standard module (.BAS). A procedure that might be used in response to events in several different objects should be placed in a standard module, rather than duplicating the code in the event procedures for each object.
A class module (.CLS) is used to create objects that can be called from procedures within your application. Whereas a standard module contains only code, a class module contains both code and data — you can think of it as a control without a physical representation.
While "Managing Projects" describes which components you can add to an application, this chapter explains how to write code in the various components that make up an application. By default, your project contains a single form module. You can add additional form, class, and standard modules, as needed. Class modules are discussed in "Programming with Objects."
Before You Start Coding Perhaps the most important (and often overlooked) part of creating an application in Visual Basic is the design phase. While it's obvious that you need to design a user interface for your application, it may not be as obvious that you need to design the structure of the code. The way you structure your application can make a difference in its performance as well as in the maintainability and usability of your code.
The code in a Visual Basic application is organized in a hierarchical fashion. A typical application consists of one or more modules: a form module for each form in the application, optional standard modules for shared code, and optional class modules. Each module contains one or more procedures that contain the code: event procedures, Sub or Function procedures, and Property procedures.
Determining which procedures belong in which module depends somewhat on the type of application that you are creating. Because Visual Basic is based on objects, it helps to think of your application in terms of the objects that it represents. The design of the sample application for this chapter, Vcr.vbp, is based on the objects that comprise a video cassette recorder and a television. The VCR application consists of two form modules, a standard module, and two class modules. You can use the Object Browser to examine the structure of the project
The structure of the VCR project is shown in the Object Browser
The main form for the VCR application (frmVCR) is a visual representation of a combination VCR and television screen . It is composed of several objects that model those found in the real world version. A group of Command buttons (cmdPlay, cmdRecord, and so on) mimic the buttons used to operate a VCR. The software VCR also contains a clock (lblTime), a channel indicator (lblChannel), function indicators (shpPlay, shpRecord, and so on), and a "picture tube" (picTV). The event procedures for all of these objects are contained in the Vcr.frm form module.
Using the Code Editor
The Visual Basic Code Editor is a window where you write most of your code. It is like a highly specialized word processor with a number of features that make writing Visual Basic code a lot easier.
The Code Editor window
Each section of code can contain several different procedures, accessed using the Procedure Listbox. The procedure list for a form module contains a separate section for each event procedure for the form or control. For example, the procedure list for a Label control includes sections for the Change, Click, and DblClick events, among others. Class modules list only the event procedures for the class itself — Initialize and Terminate. Standard modules don't list any event procedures, because a standard module doesn't support events.
The procedure list for a general section of a module contains a single selection — the Declarations section, where you place module-level variable, constant, and DLL declarations. As you add Sub or Function procedures to a module, those procedures are added in the Procedure Listbox below the Declarations section.
Two different views of your code are available in the Code Editor window. You can choose to view a single procedure at a time, or to view all of the procedures in the module with each procedure separated from the next by a line (as shown in Figure 5.4). To switch between the two views, use the View Selection buttons in the lower left-hand corner of the editor window.
Auto Quick Info
Bookmarks
Bookmarks can be used to mark lines of code in the Code Editor so that you can easily return to them later. Commands to toggle bookmarks on or off as well as to navigate existing bookmarks are available from the Edit, Bookmarks menu item, or from the Edit toolbar
Code Basics Combining Statements on One Line
There is usually one Visual Basic statement to a line, and there is no statement terminator. However, you can place two or more statements on a line if you use a colon (:) to separate them:
Text1.Text = "Hello" : Red = 255 : Text1.BackColor = _
Red
In order to make your code more readable, however, it's better to place each statement on a separate line.
Adding Comments to Your Code
As you read through the examples in this guide, you'll often come across the comment symbol ('). This symbol tells Visual Basic to ignore the words that follow it. Such words are remarks placed in the code for the benefit of the developer, and other programmers who might examine the code later. For example:
' This is a comment beginning at the left edge of the
' screen.
Text1.Text = "Hi!" ' Place friendly greeting in text
' box.
Comments can follow a statement on the same line or can occupy an entire line. Both are illustrated in the preceding code. Remember that comments can't follow a line-continuation character on the same line.
Variables
In Visual Basic, you use variables to temporarily store values during the execution of an application. Variables have a name (the word you use to refer to the value the variable contains) and a data type (which determines the kind of data the variable can store).
You can think of a variable as a placeholder in memory for an unknown value. For example, imagine you are creating a program for a fruit stand to track the sales of apples. You don't know the price of an apple or the quantity sold until the sale actually occurs. You can use two variables to hold the unknown values — let's name them ApplePrice and ApplesSold. Each time the program is run, the user supplies the values for the two variables. To calculate the total sales and display it in a Textbox named txtSales, your code would look like this:
txtSales.txt = ApplePrice * ApplesSold
The expression returns a different total each time, depending on what values the user provides. The variables allow you to make a calculation without having to know in advance what the actual inputs are.
In this example, the data type of ApplePrice is Currency; the data type of ApplesSold is an integer. Variables can represent many other values as well: text values, dates, various numeric types, even objects.
Declaring Variables To declare a variable is to tell the program about it in advance. You declare a variable with the Dim statement, supplying a name for the variable:
Dim variablename [As type]
Variables declared with the Dim statement within a procedure exist only as long as the procedure is executing. When the procedure finishes, the value of the variable disappears. In addition, the value of a variable in a procedure is local to that procedure — that is, you can't access a variable in one procedure from another procedure. These characteristics allow you to use the same variable names in different procedures without worrying about conflicts or accidental changes.
A variable name:
You don't have to declare a variable before using it. For example, you could write a function where you don't need to declare TempVal before using it:
Function SafeSqr(num)
TempVal = Abs(num)
SafeSqr = Sqr(TempVal)
End Function
Visual Basic automatically creates a variable with that name, which you can use as if you had explicitly declared it. While this is convenient, it can lead to subtle errors in your code if you misspell a variable name. For example, suppose that this was the function you wrote:
Function SafeSqr(num)
TempVal = Abs(num)
SafeSqr = Sqr(TemVal)
End Function
At first glance, this looks the same. But because the TempVal variable was misspelled on the next-to-last line, this function will always return zero. When Visual Basic encounters a new name, it can't determine whether you actually meant to implicitly declare a new variable or you just misspelled an existing variable name, so it creates a new variable with that name.
Explicit Declaration
To avoid the problem of misnaming variables, you can stipulate that Visual Basic always warn you whenever it encounters a name not declared explicitly as a variable.
Scoping Variables
Depending on how it is declared, a variable is scoped as either a procedure-level (local) or module-level variable.
Scope Private Public Procedure-level Variables are private to the procedure in which they appear. Not applicable. You cannot declare public variables within a procedure. Module-level Variables are private to the module in which they appear Variables are available to all modules Variables Used Within a Procedure
Procedure-level variables are recognized only in the procedure in which they're declared. These are also known as local variables. You declare them with the Dim or Static keywords. For example:
Dim intTemp As Integer
–or–
Static intPermanent As Integer
Values in local variables declared with Static exist the entire time your application is running while variables declared with Dim exist only as long as the procedure is executing.
Local variables are a good choice for any kind of temporary calculation. For example, you can create a dozen different procedures containing a variable called intTemp. As long as each intTemp is declared as a local variable, each procedure recognizes only its own version of intTemp. Any one procedure can alter the value in its local intTemp without affecting intTemp variables in other procedures.
Variables Used Within a Module
By default, a module-level variable is available to all the procedures in that module, but not to code in other modules. You create module-level variables by declaring them with the Private keyword in the Declarations section at the top of the module. For example:
Private intTemp As Integer
At the module level, there is no difference between Private and Dim, but Private is preferred because it readily contrasts with Public and makes your code easier to understand.
Variables Used by All Modules
To make a module-level variable available to other modules, use the Public keyword to declare the variable. The values in public variables are available to all procedures in your application. Like all module-level variables, public variables are declared in the Declarations section at the top of the module. For example: Public intTemp As Integer
Note You can't declare public variables within a procedure, only within the Declarations section of a module.
Static Variables
In addition to scope, variables have a lifetime, the period of time during which they retain their value. The values in module-level and public variables are preserved for the lifetime of your application. However, local variables declared with Dim exist only while the procedure in which they are declared is executing. Usually, when a procedure is finished executing, the values of its local variables are not preserved and the memory used by the local variables is reclaimed. The next time the procedure is executed, all its local variables are reinitialized.
However, you can preserve the value of a local variable by making the variable static. Use the Static keyword to declare one or more variables inside a procedure, exactly as you would with the Dim statement:
Static Depth
For example, the following function calculates a running total by adding a new value to the total of previous values stored in the static variable Accumulate:
Function RunningTotal(num)
Static ApplesSold
ApplesSold = ApplesSold + num
RunningTotal = ApplesSold
End Function
If ApplesSold was declared with Dim instead of Static, the previous accumulated values would not be preserved across calls to the function, and the function would simply return the same value with which it was called.
You can produce the same result by declaring ApplesSold in the Declarations section of the module, making it a module-level variable. Once you change the scope of a variable this way, however, the procedure no longer has exclusive access to it. Because other procedures can access and change the value of the variable, the running totals might be unreliable and the code would be more difficult to maintain.
Declaring All Local Variables as Static
To make all local variables in a procedure static, place the Static keyword at the beginning of a procedure heading. For example: Static Function RunningTotal(num)
This makes all the local variables in the procedure static regardless of whether they are declared with Static, Dim, Private, or declared implicitly. You can place Static in front of any Sub or Function procedure heading, including event procedures and those declared as Private.
Constants
Often you'll find that your code contains constant values that reappear over and over. Or you may find that the code depends on certain numbers that are difficult to remember — numbers that, in and of themselves, have no obvious meaning.
In these cases, you can greatly improve the readability of your code — and make it easier to maintain — by using constants. A constant is a meaningful name that takes the place of a number or string that does not change. Although a constant somewhat resembles a variable, you can't modify a constant or assign a new value to it as you can to a variable. There are two sources for constants:
The prefixes are intended to prevent accidental collisions in cases where constants have identical names and represent different values. Even with prefixes, it's still possible that two object libraries may contain identical constants representing different values. Which constant is referenced in this case depends on which object library has the higher priority. For information on changing the priority of object libraries, see the "References Dialog Box."
To be absolutely sure you avoid constant name collisions, you can qualify references to constants with the following syntax:
[libname.][modulename.]constname
Libname is usually the class name of the control or library. Modulename is the name of the module that defines the constant. Constname is the name of the constant. Each of these elements is defined in the object library, and can be viewed in the Object Browser.
Creating Your Own Constants The syntax for declaring a constant is:
[Public|Private] Const constantname[As type] = expression
The argument constantname is a valid symbolic name (the rules are the same as those for creating variable names), and expression is composed of numeric or string constants and operators; however, you can't use function calls in expression.
A Const statement can represent a mathematical or date/time quantity:
Const conPi = 3.14159265358979
Public Const conMaxPlanets As Integer = 9
Const conReleaseDate = #1/1/95#
The Const statement can also be used to define string constants:
Public Const conVersion = "07.10.A"
Const conCodeName = "Enigma"
You can place more than one constant declaration on a single line if you separate them with commas:
Public Const conPi = 3.14, conMaxPlanets = 9, _
conWorldPop = 6E+09
The expression on the right side of the equal sign ( = ) is often a number or literal string, but it can also be an expression that results in a number or string (although that expression can't contain calls to functions). You can even define constants in terms of previously defined constants:
Const conPi2 = conPi * 2
Once you define constants, you can place them in your code to make it more readable. For example:
Static SolarSystem(1 To conMaxPlanets)
If numPeople > conWorldPop Then Exit Sub
Scoping User-Defined Constants A Const statement has scope like a variable declaration, and the same rules apply:
Variables are placeholders used to store values; they have names and data types. The data type of a variable determines how the bits representing those values are stored in the computer's memory. When you declare a variable, you can also supply a data type for it. All variables have a data type that determines what kind of data they can store.
By default, if you don't supply a data type, the variable is given the Variant data type. The Variant data type is like a chameleon — it can represent many different data types in different situations. You don't have to convert between these types of data when assigning them to a Variant variable: Visual Basic automatically performs any necessary conversion.
If you know that a variable will always store data of a particular type, however, Visual Basic can handle that data more efficiently if you declare a variable of that type. For example, a variable to store a person's name is best represented as a string data type, because a name is always composed of characters.
Data types apply to other things besides variables. When you assign a value to a property, that value has a data type; arguments to functions also have data types. In fact, just about anything in Visual Basic that involves data also involves data types.
You can also declare arrays of any of the fundamental types.
For More Information For more information, see the section, "Arrays," later in this chapter. Selecting data types to improve your application's performance is discussed in "Designing for Performance and Compatibility."
Declaring Variables with Data Types
Before using a non-Variant variable, you must use the Private, Public, Dim or Static statement to declare it As type. For example, the following statements declare an Integer, Double, String, and Currency type, respectively:
Private I As Integer
Dim Amt As Double
Static YourName As String
Public BillsPaid As Currency
A Declaration statement can combine multiple declarations, as in these statements:
Private I As Integer, Amt As Double
Private YourName As String, BillsPaid As Currency
Private Test, Amount, J As Integer
Note If you do not supply a data type, the variable is given the default type. In the preceding example, the variables Test and Amount are of the Variant data type. This may surprise you if your experience with other programming languages leads you to expect all variables in the same declaration statement to have the same specified type (in this case, Integer).
Numeric Data Types
Visual Basic supplies several numeric data types — Integer, Long (long integer), Single (single-precision floating point), Double (double-precision floating point), and Currency. Using a numeric data type generally uses less storage space than a variant.
If you know that a variable will always store whole numbers (such as 12) rather than numbers with a fractional amount (such as 3.57), declare it as an Integer or Long type. Operations are faster with integers, and these types consume less memory than other data types. They are especially useful as the counter variables in For...Next loops.
The Byte Data Type If the variable contains binary data, declare it as an array of the Byte data type. (Arrays are discussed in "Arrays" later in this chapter). Using Byte variables to store binary data preserves it during format conversions. When String variables are converted between ANSI and Unicode formats, any binary data in the variable is corrupted. Visual Basic may automatically convert between ANSI and Unicode when:
All numeric variables can be assigned to each other and to variables of the Variant type. Visual Basic rounds off rather than truncates the fractional part of a floating-point number before assigning it to an integer.
The String Data Type If you have a variable that will always contain a string and never a numeric value, you can declare it to be of type String:
Private S As String
You can then assign strings to this variable and manipulate it using string functions:
S = "Database"
S = Left(S, 4)
By default, a string variable or argument is a variable-length string; the string grows or shrinks as you assign new data to it. You can also declare strings that have a fixed length. You specify a fixed-length string with this syntax:
String * size
For example, to declare a string that is always 50 characters long, use code like this:
Dim EmpName As String * 50
If you assign a string of fewer than 50 characters, EmpName is padded with enough trailing spaces to total 50 characters. If you assign a string that is too long for the fixed-length string, Visual Basic simply truncates the characters.
Because fixed-length strings are padded with trailing spaces, you may find the Trim and RTrim functions, which remove the spaces, useful when working with them.
Fixed-length strings in standard modules can be declared as Public or Private. In forms and class modules, fixed-length strings must be declared Private.
Exchanging Strings and Numbers
You can assign a string to a numeric variable if the string represents a numeric value. It's also possible to assign a numeric value to a string variable. For example, place a command button, text box, and list box on a form. Enter the following code in the command button's Click event. Run the application, and click the command button.
Private Sub Command1_Click()
Dim intX As Integer
Dim strY As String
strY = "100.23"
intX = strY ' Passes the string to a numeric
' variable.
List1.AddItem Cos(strY) ' Adds cosine of number in
' the string to the listbox.
strY = Cos(strY) ' Passes cosine to the
' string variable.
Text1.Text = strY ' String variable prints in
' the text box.
End Sub
Visual Basic will automatically coerce the variables to the appropriate data type. You should use caution when exchanging strings and numbers; passing a non-numeric value in the string will cause a run-time error to occur.
The Boolean Data Type If you have a variable that will contain simple true/false, yes/no, or on/off information, you can declare it to be of type Boolean. The default value of Boolean is False. In the following example, blnRunning is a Boolean variable which stores a simple yes/no setting.
Dim blnRunning As Boolean
' Check to see if the tape is running.
If Recorder.Direction = 1 Then
blnRunning = True
End if
The Date Data Type Date and time values can be contained both in the specific Date data type and in Variant variables. The same general characteristics apply to dates in both types.
The Object Data Type
Object variables are stored as 32-bit (4-byte) addresses that refer to objects within an application or within some other application. A variable declared as Object is one that can subsequently be assigned (using the Set statement) to refer to any actual object recognized by the application.
Dim objDb As Object
Set objDb = OpenDatabase("c:\Vb5\Biblio.mdb")
When declaring object variables, try to use specific classes (such as TextBox instead of Control or, in the preceding case, Database instead of Object) rather than the generic Object. Visual Basic can resolve references to the properties and methods of objects with specific types before you run an application. This allows the application to perform faster at run time. Specific classes are listed in the Object Browser.
When working with other applications' objects, instead of using a Variant or the generic Object, declare objects as they are listed in the Classes list in the Object Browser. This ensures that Visual Basic recognizes the specific type of object you're referencing, allowing the reference to be resolved at run time.
Converting Data Types Visual Basic provides several conversion functions you can use to convert values into a specific data type. To convert a value to Currency, for example, you use the CCur function:
PayPerWeek = CCur(hours * hourlyPay)
Conversion function Converts an expression to
Cbool Boolean Cbyte Byte Ccur Currency Cdate Date CDbl Double Cint Integer CLng Long CSng Single CStr String Cvar Variant CVErr Error
The Variant Data Type
Variant variable is capable of storing all system-defined types of data. You don't have to convert between these types of data if you assign them to a Variant variable; Visual Basic automatically performs any necessary conversion. For example:
Dim SomeValue ' Variant by default.
SomeValue = "17" ' SomeValue contains "17" (a two-
' character string).
SomeValue = SomeValue - 15 ' SomeValue now contains
' the numeric value 2.
SomeValue = "U" & SomeValue ' SomeValue now contains
' "U2" (a two- character string).
While you can perform operations on Variant variables without much concern for the kind of data they contain, there are some traps you must avoid.
The Empty Value
Sometimes you need to know if a value has ever been assigned to a created variable. A Variant variable has the Empty value before it is assigned a value. The Empty value is a special value different from 0, a zero-length string (""), or the Null value. You can test for the Empty value with the IsEmpty function: If IsEmpty(Z) Then Z = 0
When a Variant contains the Empty value, you can use it in expressions; it is treated as either 0 or a zero-length string, depending on the expression.
The Empty value disappears as soon as any value (including 0, a zero-length string, or Null) is assigned to a Variant variable. You can set a Variant variable back to Empty by assigning the keyword Empty to the Variant.
The Null Value The Variant data type can contain another special value: Null. Null is commonly used in database applications to indicate unknown or missing data. Because of the way it is used in databases, Null has some unique characteristics:
Z = Null
You can use the IsNull function to test if a Variant variable contains Null:
If IsNull(X) And IsNull(Y) Then
Z = Null
Else
Z = 0
End If
If you assign Null to a variable of any type other than Variant, a trappable error occurs. Assigning Null to a Variant variable doesn't cause an error, and Null will propagate through expressions involving Variant variables (though Null does not propagate through certain functions). You can return Null from any Function procedure with a Variant return value.
Variables are not set to Null unless you explicitly assign Null to them, so if you don't use Null in your application, you don't have to write code that tests for and handles it.
The Error Value In a Variant, Error is a special value used to indicate that an error condition has occurred in a procedure. However, unlike for other kinds of errors, normal application-level error handling does not occur. This allows you, or the application itself, to take some alternative based on the error value. Error values are created by converting real numbers to error values using the CVErr function.
Arrays
If you have programmed in other languages, you're probably familiar with the concept of arrays. Arrays allow you to refer to a series of variables by the same name and to use a number (an index) to tell them apart. This helps you create smaller and simpler code in many situations, because you can set up loops that deal efficiently with any number of cases by using the index number. Arrays have both upper and lower bounds, and the elements of the array are contiguous within those bounds. Because Visual Basic allocates space for each index number, avoid declaring an array larger than necessary.
Note The arrays discussed in this section are arrays of variables, declared in code. They are different from the control arrays you specify by setting the Index property of controls at design time. Arrays of variables are always contiguous; unlike control arrays, you cannot load and unload elements from the middle of the array.
All the elements in an array have the same data type. Of course, when the data type is Variant, the individual elements may contain different kinds of data (objects, strings, numbers, and so on). You can declare an array of any of the fundamental data types, including user-defined types (described in the section, "Creating Your Own Data Types," in "More About Programming") and object variables (described in "Programming with Objects").
Declaring Fixed-Size Arrays There are three ways to declare a fixed-size array, depending on the scope you want the array to have:
When declaring an array, follow the array name by the upper bound in parentheses. The upper bound cannot exceed the range of a Long data type (-2,147,483,648 to 2,147,483,647). For example, these array declarations can appear in the Declarations section of a module:
Dim Counters(14) As Integer ' 15 elements.
Dim Sums(20) As Double ' 21 elements.
To create a public array, you simply use Public in place of Dim:
Public Counters(14) As Integer
Public Sums(20) As Double
The same declarations within a procedure use Dim:
Dim Counters(14) As Integer
Dim Sums(20) As Double
The first declaration creates an array with 15 elements, with index numbers running from 0 to 14. The second creates an array with 21 elements, with index numbers running from 0 to 20. The default lower bound is 0.
To specify a lower bound, provide it explicitly (as a Long data type) using the To keyword:
Dim Counters(1 To 15) As Integer
Dim Sums(100 To 120) As String
In the preceding declarations, the index numbers of Counters range from 1 to 15, and the index numbers of Sums range from 100 to 120.
Arrays that Contain Other Arrays
It's possible to create a Variant array, and populate it with other arrays of different data types. The following code creates two arrays, one containing integers and the other strings. It then declares a third Variant array and populates it with the integer and string arrays.
Private Sub Command1_Click()
Dim intX As Integer ' Declare counter variable.
' Declare and populate an integer array.
Dim countersA(5) As Integer
For intX = 0 To 4
countersA(intX) = 5
Next intX
' Declare and populate a string array.
Dim countersB(5) As String
For intX = 0 To 4
countersB(intX) = "hello"
Next intX
Dim arrX(2) As Variant ' Declare a new two-member
' array.
arrX(1) = countersA() ' Populate the array with
' other arrays.
arrX(2) = countersB()
MsgBox arrX(1)(2) ' Display a member of each
' array.
MsgBox arrX(2)(3)
End Sub
Multidimensional Arrays
Sometimes you need to keep track of related information in an array. For example, to keep track of each pixel on your computer screen, you need to refer to its X and Y coordinates. This can be done using a multidimensional array to store the values.
With Visual Basic, you can declare arrays of multiple dimensions. For example, the following statement declares a two-dimensional 10-by-10 array within a procedure:
Static MatrixA(9, 9) As Double
Either or both dimensions can be declared with explicit lower bounds:
Static MatrixA(1 To 10, 1 To 10) As Double
You can extend this to more than two dimensions. For example:
Dim MultiD(3, 1 To 10, 1 To 15)
This declaration creates an array that has three dimensions with sizes 4 by 10 by 15. The total number of elements is the product of these three dimensions, or 600.
Note When you start adding dimensions to an array, the total storage needed by the array increases dramatically, so use multidimensional arrays with care. Be especially careful with Variant arrays, because they are larger than other data types.
Using Loops to Manipulate Arrays You can efficiently process a multidimensional array by using nested For loops. For example, these statements initialize every element in MatrixA to a value based on its location in the array:
Dim I As Integer, J As Integer
Static MatrixA(1 To 10, 1 To 10) As Double
For I = 1 To 10
For J = 1 To 10
MatrixA(I, J) = I * 10 + J
Next J
Next I
Dynamic Arrays
Sometimes you may not know exactly how large to make an array. You may want to have the capability of changing the size of the array at run time.
A dynamic array can be resized at any time. Dynamic arrays are among the most flexible and convenient features in Visual Basic, and they help you to manage memory efficiently. For example, you can use a large array for a short time and then free up memory to the system when you're no longer using the array.
The alternative is to declare an array with the largest possible size and then ignore array elements you don't need. However, this approach, if overused, might cause the operating environment to run low on memory.
To create a dynamic array
The ReDim statement supports the same syntax used for fixed arrays. Each ReDim can change the number of elements, as well as the lower and upper bounds, for each dimension. However, the number of dimensions in the array cannot change.
ReDim DynArray(4 to 12)
For example, the dynamic array Matrix1 is created by first declaring it at the module level:
Dim Matrix1() As Integer
A procedure then allocates space for the array:
Sub CalcValuesNow ()
.
.
.
ReDim Matrix1(19, 29)
End Sub
The ReDim statement shown here allocates a matrix of 20 by 30 integers (at a total size of 600 elements). Alternatively, the bounds of a dynamic array can be set using variables:
ReDim Matrix1(X, Y)
Note You can assign strings to resizable arrays of bytes. An array of bytes can also be assigned to a variable-length string. Be aware that the number of bytes in a string varies among platforms. On Unicode platforms the same string contains twice as many bytes as it does on a non-Unicode platform.
Function Procedures Visual Basic includes built-in, or intrinsic functions, like Sqr, Cos or Chr. In addition, you can use the Function statement to write your own Function procedures.
The syntax for a Function procedure is:
[Private|Public][Static]Function procedurename (arguments) [As type]
statements
End Function
Like a Sub procedure, a Function procedure is a separate procedure that can take arguments, perform a series of statements, and change the value of its arguments. Unlike a Sub procedure, a Function procedure can return a value to the calling procedure. There are three differences between Sub and Function procedures:
Function Hypotenuse (A As Integer, B As Integer) _
As String
Hypotenuse = Sqr(A ^ 2 + B ^ 2)
End Function
You call a Function procedure the same way you call any of the built-in functions in Visual Basic:
Label1.Caption = Hypotenuse(CInt(Text1.Text), _
CInt(Text2.Text))
strX = Hypotenuse(Width, Height)
Working with Procedures Creating New Procedures
To create a new general procedure
Selecting Existing Procedures
To view a procedure in the current module
To view an existing general procedure, select "(General)" from the Object box in the Code window, and then select the procedure in the Procedure box.
–or–
To view an event procedure, select the appropriate object from the Object box in the Code window, and then select the event in the Procedure box.
To view a procedure in another module
Calling Sub Procedures
Sub procedure differs from a Function procedure in that a Sub procedure cannot be called by using its name within an expression. A call to a Sub is a stand-alone statement. Also, a Sub does not return a value in its name as does a function. However, like a Function, a Sub can modify the values of any variables passed to it.
There are two ways to call a Sub procedure:
' Both of these statements call a Sub named MyProc.
Call MyProc (FirstArgument, SecondArgument)
MyProc FirstArgument, SecondArgument
Note that when you use the Call syntax, arguments must be enclosed in parentheses. If you omit the Call keyword, you must also omit the parentheses around the arguments.
Calling Function Procedures
Usually, you call a function procedure you've written yourself the same way you call an intrinsic Visual Basic function like Abs; that is, by using its name in an expression:
' All of the following statements would call a function
' named ToDec.
Print 10 * ToDec
X = ToDec
If ToDec = 10 Then Debug.Print "Out of Range"
X = AnotherFunction(10 * ToDec)
It's also possible to call a function just like you would call a Sub procedure. The following statements both call the same function:
Call Year(Now)
Year Now
When you call a function this way, Visual Basic throws away the return value.
Calling Procedures in Other Modules Public procedures in other modules can be called from anywhere in the project. You might need to specify the module that contains the procedure you're calling. The techniques for doing this vary, depending on whether the procedure is located in a form, class, or standard module.
Procedures in Forms All calls from outside the form module must point to the form module containing the procedure. If a procedure named SomeSub is in a form module called Form1, then you can call the procedure in Form1 by using this statement:
Call Form1.SomeSub(arguments)
Procedures in Class Modules Like calling a procedure in a form, calling a procedure in a class module requires that the call to the procedure be qualified with a variable that points to an instance of the class. For example, DemoClass is an instance of a class named Class1:
Dim DemoClass as New Class1
DemoClass.SomeSub
However, unlike a form, the class name cannot be used as the qualifier when referencing an instance of the class. The instance of the class must be first be declared as an object variable (in this case, DemoClass) and referenced by the variable name.
Procedures in Standard Modules If a procedure name is unique, you don't need to include the module name in the call. A call from inside or outside the module will refer to that unique procedure. A procedure is unique if it appears only in one place.
If two or more modules contain a procedure with the same name, you may need to qualify it with the module name. A call to a common procedure from the same module runs the procedure in that module. For example, with a procedure named CommonName in Module1 and Module2, a call to CommonName from Module2 will run the CommonName procedure in Module2, not the CommonName procedure in Module1.
A call to a common procedure name from another module must specify the intended module. For example, if you want to call the CommonName procedure in Module2 from Module1, use:
Module2.CommonName(arguments)
Passing Arguments to Procedures Usually the code in a procedure needs some information about the state of the program to do its job. This information consists of variables passed to the procedure when it is called. When a variable is passed to a procedure, it is called an argument.
Argument Data Types The arguments for procedures you write have the Variant data type by default. However, you can declare other data types for arguments. For example, the following function accepts a string and an integer:
Function WhatsForLunch(WeekDay As String, Hour _
As Integer) As String
' Returns a lunch menu based on the day and time.
If WeekDay = "Friday" then
WhatsForLunch = "Fish"
Else
WhatsForLunch = "Chicken"
End If
If Hour > 4 Then WhatsForLunch = "Too late"
End Function
Passing Arguments By Value Only a copy of a variable is passed when an argument is passed by value. If the procedure changes the value, the change affects only the copy and not the variable itself. Use the ByVal keyword to indicate an argument passed by value.
For example:
Sub PostAccounts(ByVal intAcctNum as Integer)
.
. ' Place statements here.
.
End Sub
Passing Arguments By Reference Passing arguments by reference gives the procedure access to the actual variable contents in its memory address location. As a result, the variable's value can be permanently changed by the procedure to which it is passed. Passing by reference is the default in Visual Basic.
If you specify a data type for an argument passed by reference, you must pass a value of that type for the argument. You can work around this by passing an expression, rather than a data type, for an argument. Visual Basic evaluates an expression and passes it as the required type if it can.
The simplest way to turn a variable into an expression is to enclose it in parentheses. For example, to pass a variable declared as an integer to a procedure expecting a string as an argument, you would do the following:
Sub CallingProcedure()
Dim intX As Integer
intX = 12 * 3
Foo(intX)
End Sub
Sub Foo(Bar As String)
MsgBox Bar 'The value of Bar is the string "36".
End Sub
Using Optional Arguments
You can specify arguments to a procedure as optional by placing the Optional keyword in the argument list. If you specify an optional argument, all subsequent arguments in the argument list must also be optional and declared with the Optional keyword. The two pieces of sample code below assume there is a form with a command button and list box.
For example, this code provides all optional arguments:
Dim strName As String
Dim strAddress As String
Sub ListText(Optional x As String, Optional y _
As String)
List1.AddItem x
List1.AddItem y
End Sub
Private Sub Command1_Click()
strName = "yourname"
strAddress = 12345 ' Both arguments are provided.
Call ListText(strName, strAddress)
End Sub
This code, however, does not provide all optional arguments:
Dim strName As String
Dim varAddress As Variant
Sub ListText(x As String, Optional y As Variant)
List1.AddItem x
If Not IsMissing(y) Then
List1.AddItem y
End If
End Sub
Private Sub Command1_Click()
strName = "yourname" ' Second argument is not
' provided.
Call ListText(strName)
End Sub
In the case where an optional argument is not provided, the argument is actually assigned as a variant with the value of Empty. The example above shows how to test for missing optional arguments using the IsMissing function.
Providing a Default for an Optional Argument It's also possible to specify a default value for an optional argument. The following example returns a default value if the optional argument isn't passed to the function procedure:
Sub ListText(x As String, Optional y As _
Integer = 12345)
List1.AddItem x
List1.AddItem y
End Sub
Private Sub Command1_Click()
strName = "yourname" ' Second argument is not
' provided.
Call ListText(strName) ' Adds "yourname" and
' "12345".
End Sub
Using an Indefinite Number of Arguments Generally, the number of arguments in the procedure call must be the same as in the procedure specification. Using the ParamArray keyword allows you to specify that a procedure will accept an arbitrary number of arguments. This allows you to write functions like Sum:
Dim x As Integer
Dim y As Integer
Dim intSum As Integer
Sub Sum(ParamArray intNums())
For Each x In intNums
y = y + x
Next x
intSum = y
End Sub
Private Sub Command1_Click()
Sum 1, 3, 5, 7, 8
List1.AddItem intSum
End Sub
Creating Simpler Statements with Named Arguments For many built-in functions, statements, and methods, Visual Basic provides the option of using named arguments as a shortcut for typing argument values. With named arguments, you can provide any or all of the arguments, in any order, by assigning a value to the named argument. You do this by typing the argument name plus a colon followed by an equal sign and the value ( MyArgument:= "SomeValue") and placing that assignment in any sequence delimited by commas. Notice that the arguments in the following example are in the reverse order of the expected arguments:
Function ListText(strName As String, Optional strAddress As String)
List1.AddItem strName
List2.AddItem strAddress
End Sub
Private Sub Command1_Click()
ListText strAddress:="12345", strName:="Your Name"
End Sub
This is especially useful if your procedures have several optional arguments that you do not always need to specify.
Determining Support for Named Arguments
To determine which functions, statements, and methods support named arguments, use the AutoQuickInfo feature in the Code window, check the Object Browser, or see the Language Reference. Consider the following when working with named arguments:
Visual Basic procedures can test conditions and then, depending on the results of that test, perform different operations. The decision structures that Visual Basic supports include:
Use an If...Then structure to execute one or more statements conditionally. You can use either a single-line syntax or a multiple-line block syntax:
If condition Then statement
If condition Then
statements
End If
The condition is usually a comparison, but it can be any expression that evaluates to a numeric value. Visual Basic interprets this value as True or False; a zero numeric value is False, and any nonzero numeric value is considered True. If condition is True, Visual Basic executes all the statements following the Then keyword. You can use either single-line or multiple-line syntax to execute just one statement conditionally (these two examples are equivalent):
If anyDate < Now Then anyDate = Now
If anyDate < Now Then
anyDate = Now
End If
Notice that the single-line form of If...Then does not use an End If statement. If you want to execute more than one line of code when condition is True, you must use the multiple-line block If...Then...End If syntax.
If anyDate < Now Then
anyDate = Now
Timer1.Enabled = False ' Disable timer control.
End If
If...Then...Else
Use an If...Then...Else block to define several blocks of statements, one of which will execute:
If condition1 Then
[statementblock-1]
[ElseIf condition2 Then
[statementblock-2]] ...
[Else
[statementblock-n]]
End If
Visual Basic first tests condition1. If it's False, Visual Basic proceeds to test condition2, and so on, until it finds a True condition. When it finds a True condition, Visual Basic executes the corresponding statement block and then executes the code following the End If. As an option, you can include an Else statement block, which Visual Basic executes if none of the conditions are True.
If...Then…ElseIf is really just a special case of If...Then...Else. Notice that you can have any number of ElseIf clauses, or none at all. You can include an Else clause regardless of whether you have ElseIf clauses.
For example, your application could perform different actions depending on which control in a menu control array was clicked:
Private Sub mnuCut_Click (Index As Integer)
If Index = 0 Then ' Cut command.
CopyActiveControl ' Call general procedures.
ClearActiveControl
ElseIf Index = 1 Then ' Copy command.
CopyActiveControl
ElseIf Index = 2 Then ' Clear command.
ClearActiveControl
Else ' Paste command.
PasteActiveControl
End If
End Sub
Notice that you can always add more ElseIf parts to your If...Then structure. However, this syntax can get tedious to write when each ElseIf compares the same expression to a different value. For this situation, you can use a Select Case decision structure.
Select Case
Visual Basic provides the Select Case structure as an alternative to If...Then...Else for selectively executing one block of statements from among multiple blocks of statements. A Select Case statement provides capability similar to the If...Then...Else statement, but it makes code more readable when there are several choices.
A Select Case structure works with a single test expression that is evaluated once, at the top of the structure. Visual Basic then compares the result of this expression with the values for each Case in the structure. If there is a match, it executes the block of statements associated with that Case:
Select Case testexpression
[Case expressionlist1
[statementblock-1]]
[Case expressionlist2
[statementblock-2]]
.
.
.
[Case Else
[statementblock-n]]
End Select
Each expressionlist is a list of one or more values. If there is more than one value in a single list, the values are separated by commas. Each statementblock contains zero or more statements. If more than one Case matches the test expression, only the statement block associated with the first matching Case will execute. Visual Basic executes statements in the Case Else clause (which is optional) if none of the values in the expression lists matches the test expression.
For example, suppose you added another command to the Edit menu in the If...Then...Else example. You could add another ElseIf clause, or you could write the function with Select Case:
Private Sub mnuCut_Click (Index As Integer)
Select Case Index
Case 0 ' Cut command.
CopyActiveControl ' Call general procedures.
ClearActiveControl
Case 1 ' Copy command.
CopyActiveControl
Case 2 ' Clear command.
ClearActiveControl
Case 3 ' Paste command.
PasteActiveControl
Case Else
frmFind.Show ' Show Find dialog box.
End Select
End Sub
Notice that the Select Case structure evaluates an expression once at the top of the structure. In contrast, the If...Then...Else structure can evaluate a different expression for each ElseIf statement. You can replace an If...Then...Else structure with a Select Case structure only if the If statement and each ElseIf statement evaluates the same expression.
Loop Structures
Loop structures allow you to execute one or more lines of code repetitively. The loop structures that Visual Basic supports include:
Use a Do loop to execute a block of statements an indefinite number of times. There are several variations of the Do...Loop statement, but each evaluates a numeric condition to determine whether to continue execution. As with If...Then, the condition must be a value or expression that evaluates to False (zero) or to True (nonzero).
In the following Do...Loop, the statements execute as long as the condition is True:
Do While condition
statements
Loop
When Visual Basic executes this Do loop, it first tests condition. If condition is False (zero), it skips past all the statements. If it's True (nonzero), Visual Basic executes the statements and then goes back to the Do While statement and tests the condition again.
Consequently, the loop can execute any number of times, as long as condition is nonzero or True. The statements never execute if condition is initially False. For example, this procedure counts the occurrences of a target string within another string by looping as long as the target string is found:
Function CountStrings (longstring, target)
Dim position, count
position = 1
Do While InStr(position, longstring, target)
position = InStr(position, longstring, target)_
+ 1
count = count + 1
Loop
CountStrings = count
End Function
If the target string doesn't occur in the other string, then InStr returns 0, and the loop doesn't execute.
Another variation of the Do...Loop statement executes the statements first and then tests condition after each execution. This variation guarantees at least one execution of statements:
Do
statements
Loop While condition
Two other variations are analogous to the previous two, except that they loop as long as condition is False rather than True.
For...Next
Do loops work well when you don't know how many times you need to execute the statements in the loop. When you know you must execute the statements a specific number of times, however, a For·Next loop is a better choice. Unlike a Do loop, a For loop uses a variable called a counter that increases or decreases in value during each repetition of the loop. The syntax is:
For counter = start To end [Step increment]
statements
Next [counter]
The arguments counter, start, end, and increment are all numeric.
Note The increment argument can be either positive or negative. If increment is positive, start must be less than or equal to end or the statements in the loop will not execute. If increment is negative, start must be greater than or equal to end for the body of the loop to execute. If Step isn't set, then increment defaults to 1.
In executing the For loop, Visual Basic
Private Sub Form_Click ()
Dim I As Integer
For i = 0 To Screen.FontCount
Print Screen.Fonts(i)
Next
End Sub
In the VCR sample application, the HighlightButton procedure uses a For...Next loop to step through the controls collection of the VCR form and show the appropriate Shape control:
Sub HighlightButton(MyControl As Variant)
Dim i As Integer
For i = 0 To frmVCR.Controls.Count - 1
If TypeOf frmVCR.Controls(i) Is Shape Then
If frmVCR.Controls(i).Name = MyControl Then
frmVCR.Controls(i).Visible = True
Else
frmVCR.Controls(i).Visible = False
End If
End If
Next
End Sub
For Each...Next
For Each...Next loop is similar to a For...Next loop, but it repeats a group of statements for each element in a collection of objects or in an array instead of repeating the statements a specified number of times. This is especially helpful if you don't know how many elements are in a collection.
Here is the syntax for the For Each...Next loop:
For Each element In group
statements
Next element
For example, the following Sub procedure opens Biblio.mdb and adds the name of each table to a list box.
Sub ListTableDefs()
Dim objDb As Database
Dim MyTableDef as TableDef
Set objDb = OpenDatabase("c:\vb\biblio.mdb", _
True, False)
For Each MyTableDef In objDb.TableDefs()
List1.AddItem MyTableDef.Name
Next MyTableDef
End Sub
Keep the following restrictions in mind when using For Each...Next:
Nested Control Structures
You can place control structures inside other control structures (such as an If...Then block within a For...Next loop). A control structure placed inside another control structure is said to be nested.
Control structures in Visual Basic can be nested to as many levels as you want. It's common practice to make nested decision structures and loop structures more readable by indenting the body of the decision structure or loop.
For example, this procedure prints all the font names that are common to both the Printer and Screen:
Private Sub Form_Click()
Dim SFont, PFont
For Each SFont In Screen.Fonts()
For Each PFont In Printer.Fonts()
If SFont = PFont Then
Print SFont
End If
Next PFont
Next SFont
End Sub
Notice that the first Next closes the inner For loop and the last For closes the outer For loop. Likewise, in nested If statements, the End If statements automatically apply to the nearest prior If statement. Nested Do...Loop structures work in a similar fashion, with the innermost Loop statement matching the innermost Do statement.
Exiting a Control Structure
The Exit statement allows you to exit directly from a For loop, Do loop, Sub procedure, or Function procedure. The syntax for the Exit statement is simple: Exit For can appear as many times as needed inside a For loop, and Exit Do can appear as many times as needed inside a Do loop:
For counter = start To end [Step increment]
[statementblock]
[Exit For]
[statementblock]
Next [counter[, counter] [,...]]
Do [{While | Until} condition]
[statementblock]
[Exit Do]
[statementblock]
Loop
The Exit Do statement works with all versions of the Do loop syntax.
Exit For and Exit Do are useful because sometimes it's appropriate to quit a loop immediately, without performing any further iterations or statements within the loop. For example, in the previous example that printed the fonts common to both the Screen and Printer, the code continues to compare Printer fonts against a given Screen font even when a match has already been found with an earlier Printer font. A more efficient version of the function would exit the loop as soon as a match is found:
Private Sub Form_Click()
Dim SFont, PFont
For Each SFont In Screen.Fonts()
For Each PFont In Printer.Fonts()
If SFont = PFont Then
Print Sfont
Exit For ' Exit inner loop.
End If
Next PFont
Next SFont
End Sub As this example illustrates, an Exit statement almost always appears inside an If statement or Select Case statement nested inside the loop.
Exiting a Sub or Function Procedure
You can also exit a procedure from within a control structure. The syntax of Exit Sub and Exit Function is similar to that of Exit For and Exit Do in the previous section, "Exiting a Control Structure." Exit Sub can appear as many times as needed, anywhere within the body of a Sub procedure. Exit Function can appear as many times as needed, anywhere within the body of a Function procedure.
Exit Sub and Exit Function are useful when the procedure has done everything it needs to do and can return immediately. For example, if you want to change the previous example so it prints only the first common Printer and Screen font it finds, you would use Exit Sub:
Private Sub Form_Click()
Dim SFont, PFont
For Each SFont In Screen.Fonts()
For Each PFont In Printer.Fonts()
If SFont = PFont Then
Print Sfont
Exit Sub ' Exit the procedure.
End If
Next PFont
Next SFont
End Sub
What is an Object?
An object is a combination of code and data that can be treated as a unit. An object can be a piece of an application, like a control or a form. An entire application can also be an object. The following table describes examples of the types of objects you can use in Visual Basic.
Example
Description Command button Controls on a form, such as command buttons and frames, are objects. Form Each form in a Visual Basic project is a separate object. Database Databases are objects, and contain other objects, like fields and indexes Chart A chart in Microsoft Excel is an object
All objects are created as identical copies of their class. Once they exist as individual objects, their properties can be changed. For example, if you draw three command buttons on a form, each command button object is an instance of the CommandButton class. Each object shares a common set of characteristics and capabilities (properties, methods, and events), defined by the class. However, each has its own name, can be separately enabled and disabled, can be placed in a different location on the form, and so on. For simplicity, most of the material outside of this chapter won't make many references to an object's class. Just remember that the term "list box control," for example, means "an instance of the ListBox class."
Controls collection
For example, the following code scrolls through the Controls collection and lists each member's name in a list box.
Dim MyControl as Control
For Each MyControl In Form1.Controls
' For each control, add its name to a list box.
List1.AddItem MyControl.Name
Next MyControl
When you run the Visual Basic Setup program, it allows you to place the program items in an existing program group or create a new program group and new program items for Visual Basic in Windows. You are then ready to start Visual Basic from Windows. To start Visual Basic from Windows
- Click Start on the Task bar.
- Select Programs, Visual Studio and then Microsoft Visual Basic 6.0.
–or–
- Click Start on the Task bar.
Select Programs.
Use the Windows Explorer to find the Visual Basic executable file. - Double-click the Visual Basic icon.
You can also create a shortcut to Visual Basic, and double-click the shortcut.
Figure 1.1The Visual Basic integrated development environment
Integrated Development Environment Elements
Menu Bar
Displays the commands you use to work with Visual Basic. Besides the standard File, Edit, View, Window, and Help menus, menus are provided to access functions specific to programming such as Project, Format, or Debug. Context Menus
Contain shortcuts to frequently performed actions. To open a context menu, click the right mouse button on the object you're using. The specific list of shortcuts available from context menus depends on the part of the environment where you click the right mouse button. For example, the context menu displayed when you right click on the Toolbox lets you display the Components dialog box, hide the Toolbox, dock or undock the Toolbox, or add a custom tab to the Toolbox. Toolbars
Provide quick access to commonly used commands in the programming environment. You click a button on the toolbar once to carry out the action represented by that button. By default, the Standard toolbar is displayed when you start Visual Basic. Additional toolbars for editing, form design, and debugging can be toggled on or off from the Toolbars command on the View menu. Toolbars can be docked beneath the menu bar or can "float" if you select the vertical bar on the left edge and drag it away from the menu bar.
Toolbox
Provides a set of tools that you use at design time to place controls on a form. In addition to the default toolbox layout, you can create your own custom layouts by selecting Add Tab from the context menu and adding controls to the resulting tab. For More Information To learn more about specific controls, see "Forms, Controls, and Menus" and "Using Visual Basic's Standard Controls." For information on how to add controls to the Toolbox, see "Adding Controls to a Project" in "Managing Projects."
Project Explorer Window
Lists the forms and modules in your current project. A project is the collection of files you use to build an application.
For More Information For information on projects, see "Managing Projects."
Properties Window
Lists the property settings for the selected form or control. A property is a characteristic of an object, such as size, caption, or color.
For More Information For information on properties, see "Understanding Properties, Methods, and Events" in "Forms, Controls, and Menus."
Object Browser
Lists objects available for use in your project and gives you a quick way to navigate through your code. You can use the Object Browser to explore objects in Visual Basic and other applications, see what methods and properties are available for those objects, and paste code procedures into your application.
For More Information For more information on using the Object Browser to view procedures, see "Finding Out About Objects" in "Programming with Objects." For details on using add-ins to extend the Visual Basic programming environment, see "Using Wizards and Add-ins" in "Managing Projects."
Form Designer
Serves as a window that you customize to design the interface of your application. You add controls, graphics, and pictures to a form to create the look you want. Each form in your application has its own form designer window.
For More Information To learn how to add controls to an application, see "Your First Visual Basic Application" later in this chapter. To learn more about designing an interface, see "Creating a User Interface."
Code Editor Window
Serves as an editor for entering application code. A separate code editor window is created for each form or code module in your application.
For More Information To learn more about entering code and using the code editor, see "Programming Fundamentals."
Form Layout Window
The Form Layout window (Figure 2.2) allows you to position the forms in your application using a small graphical representation of the screen
Figure 1.2 Form Layot window
Immediate, Locals, and Watch Windows
These additional windows are provided for use in debugging your application. They are only available when you are running your application within the IDE.
SDI or MDI Interface
Two different styles are available for the Visual Basic IDE: single document interface (SDI) or multiple document interface (MDI). With the SDI option, all of the IDE windows are free to be moved anywhere on screen; as long as Visual Basic is the current application, they will remain on top of any other applications. With the MDI option, all of the IDE windows are contained within a single resizable parent window.
To switch between SDI and MDI modes
- Select Options from the Tools menu.
The Options dialog box is displayed. - Select the Advanced tab.
- Check or uncheck the SDI Development Environment check box.
The IDE will start in the selected mode the next time you start Visual Basic.
Run Visual Basic from the command line with a /sdi or /mdi parameter.
Docking Windows
Many of the windows in the IDE can be docked, or connected, to each other or to the edge of the screen. These include the Toolbox, Form Layout Window, Project Explorer, Properties window, Color Palette, and Immediate, Locals, and Watch windows.
With the MDI option, windows can be docked to any side of the parent window; with SDI they can only be docked beneath the menu bar. Docking capabilities can be toggled on or off for a given window by selecting the appropriate check box on the Docking tab of the Options dialog box, available from the Options command on the Tools menu.
To dock or undock a window
- Select the window you wish to dock or undock
- Drag the window to the desired location by holding down the left mouse button.
- The outline of the window will be displayed as you drag.
- Release the mouse button.
There are three main steps to creating an application in Visual Basic:
- Create the interface.
- Set properties.
- Write code.
Forms are the foundation for creating the interface of an application. You can use forms to add windows and dialog boxes to your application. You can also use them as containers for items that are not a visible part of the application's interface. For example, you might have a form in your application that serves as a container for graphics that you plan to display in other forms.
The first step in building a Visual Basic application is to create the forms that will be the basis for your application's interface. Then you draw the objects that make up the interface on the forms you create. For this first application, you'll use two controls from the Toolbox.
Button
Control Text Box Button To draw a control using the Toolbox
- Click the tool for the control you choose to draw — in this case, the text box.
- Move the pointer onto your form. The pointer becomes a cross hair, as shown in Figure 1.3.
Figure 1.3 Drawing a text box with the Toolbox
- Place the cross hair where you want the upper-left corner of the control.
- Drag the cross hair until the control is the size you want. (Dragging means holding the left mouse button down while you move an object with the mouse.)
- Release the mouse button
Setting Properties The next step is to set properties for the objects you've created. The Properties window (Figure 1.4) provides an easy way to set properties for all objects on a form. To open the Properties window, choose the Properties Window command from the View menu, click the Properties Window button on the toolbar, or use the context menu for the control.
Figure 1.4 The Properties window
The Properties window consists of the following elements:
Object box — Displays the name of the object for which you can set properties. Click the arrow to the right of the object box to display the list of objects for the current form.
Sort tabs — Choose between an alphabetic listing of properties or a hierarchical view divided by logical categories, such as those dealing with appearance, fonts, or position.
Properties list — The left column displays all of the properties for the selected object. You can edit and view settings in the right column.
To set properties from the Properties window
- From the View menu, choose Properties, or click the Properties button on the toolbar.
The Properties window displays the settings for the selected form or control. - From the Properties list, select the name of a property
- In the right column, type or select the new property setting.
Enumerated properties have a predefined list of settings. You can display the list by clicking the down arrow at the right of the Settings box, or you can cycle through the list by double-clicking a list item.
Object Property Setting Form Caption Hello, world! Text box Text (Empty) Command button Caption OK
Setting the Icon Property
All forms in Visual Basic have a generic, default icon that appears when you minimize that form. However, you will probably change this icon to one that illustrates the use of the form or your application. To assign an icon to a form, set the Icon property for that form. You can use 32 x 32 pixel icons that were standard in 16-bit versions of Microsoft Windows and are also used in Windows 95/98 and Windows NT, as well as the 16 x 16 pixel icons used in Windows 95/98.
Writing Code
The Code Editor window is where you write Visual Basic code for your application. Code consists of language statements, constants, and declarations. Using the Code Editor window, you can quickly view and edit any of the code in your application.
To open the Code window
- Double-click the form or control for which you choose to write code
Or
From the Project Explorer window, select the name of a form or module, and choose the View Code button. Figure 1.5 shows the Code Editor window that appears when you double-click the Command button control, and the events for that command.
Figure 1.5 The Code Editor window
You can choose to display all procedures in the same Code window, or display a single procedure at a time.
To display all procedures in the same Code window
- From the Tools menu, select the Options dialog box.
- On the Editor tab in the Options dialog box, select the check box to the left of Default to Full Module View. The check box to the left of Procedure Separator adds or removes a separator line between procedures
Or
Click the Full Module View button in the lower left corner of the Code Editor window.
- From the Tools menu, select the Options dialog box.
- On the Editor tab in the Options dialog box, clear the check box to the left of Default to Full Module View.
Or
Click the Procedure View button in the lower left corner of the Code Editor window.
Type the following code between the Sub and End Sub statements:
Private Sub Command1_Click ()
Text1.Text = "Hello, world!"
End Sub
Running the Application
To run the application, choose Start from the Run menu, or click the Start button on the toolbar, or press F5. Click the command button you've created on the form, and you'll see "Hello, world!" displayed in the text box.
Creating a Project
You begin creating this application by choosing New Project from the File menu, then selecting Standard EXE in the New Project dialog box (when you first start Visual Basic, the New Project dialog box is presented). Visual Basic creates a new project and displays a new form. To draw the interface, you use a data control, a MSFlexGrid control, a list box control, and two command buttons. The MSFlexGrid control isn't in the default toolbox, so you'll need to add it:
To add a control to the toolbox
- Select Components from the context menu for the toolbox. (You can right-click within the toolbox window to display the context menu.)
The Components dialog box will be displayed. - Find the MSFlexGrid (Microsoft Flex Grid 6.0) in the Controls list box and select the check box to its left.
- Click the OK button.
Setting Properties
In the Properties window, set properties for the objects according to the following table. Use the default settings for all other properties.
Object Property Setting Form Caption Products Data1 DatabaseName
RecordSource path \Nwind.mdb
Products MSFlexGrid1 DataSource Data1 Command1 Caption Clear Command2 Caption Exit The DatabaseName property of the data control must include the actual path to the database. By default, the Nwind.mdb database is installed in the same directory as Visual Basic. When you select the DatabaseName property in the Properties window, you can click the button to the right of the property to display a standard File Open dialog box to browse for the file. Once the DatabaseName property has been set, the RecordSource property in the Properties window will contain a list of tables or recordsets for the selected database. Setting the DataSource property of the MSFlexGrid control to Data1 automatically links the grid to the data control.
Writing Event Code
All the code for the application is contained in the Command1_Click, Command2_Click, Data1_Reposition, and MSFlexGrid1_DblClick event procedures. Double-click the form or control to display the Code window, and then type the code for each event procedure.
Add this code to the Command1_Click event procedure to clear the list box when the user clicks the button:
Private Sub Command1_Click ()
List1.Clear ' Clears the list box.
End Sub
In the above statement, you are invoking the Clear method of the list box, List1. The Clear method deletes the contents of the list box.
Add this code to the Command2_Click event procedure to unload the form from memory and end the application:
Private Sub Command2_Click ()
Unload Form1
End ' Ends application.
End Sub
In the above procedure, the first statement invokes the Unload event for the form. If you needed to perform a function at shutdown, such as saving a file, you could place that code in the form's Unload event procedure. The second statement calls the End function, which ends the application.
Add this code to the Data1_Reposition event procedure to update the caption each time a record is selected:
Private Sub Data1_Reposition ()
Data1.Caption = Data1.Recordset("ProductName")
End Sub
In the above statement, you are assigning the value on the right (the contents of the Title field in the Recordset of the data control) to the property on the left (the Caption property of the data control object).
Add this code to the MSFlexGrid_DblClick event procedure to add an item to the list box when the user double-clicks a selected row:
Private Sub MSFlexGrid1_DblClick ()
List1.AddItem MSFlexGrid1.Text
End Sub
In the above statement, you are invoking the AddItem method of the list box (List1). The text to be added to the list box is contained in the argument of the method, in this case, the value of the title field in the recordset of the data control. Passing a value to an argument is similar to assigning a value to a property; unlike the assignment statement, the equal sign isn't required.
Saving a Project You finish your work on the application by choosing Save Project from the File menu. Visual Basic will prompt you separately to save the form and then the project. One possible name for the project is "Northwind Shopping List." Windows 95, Windows 98, and Windows NT allow you to use file names up to 255 characters in length, and file names can include spaces. Older versions of Microsoft Windows limited you to file names of eight characters, with a three-character extension.
Enhancing Your Application You have just completed your first Visual Basic application: one that performs a simple but useful function. You can use this application as a basis for adding similar functionality in your own applications, substituting your own data instead of Nwind.mdb. Of course, to make this application truly useful, you might want to add functionality to save or print the contents of the list box, to add additional information such as price and availability, and even to gather credit card information and transmit an order across the Internet. As you continue on through the rest of the Programmer's Guide, you will find examples of doing all that and a lot more.
Understanding Properties, Methods and Events
Visual Basic forms and controls are objects which expose their own properties, methods and events. Properties can be thought of as an object's attributes, methods as its actions, and events as its responses.
An everyday object like a child's helium balloon also has properties, methods and events. A balloon's properties include visible attributes such as its height, diameter and color. Other properties describe its state (inflated or not inflated), or attributes that aren't visible such as its age. By definition, all balloons have these properties; the settings of these properties may differ from one balloon to another.
A balloon also has inherent methods or actions that it might perform. It has an inflate method (the action of filling it with helium), a deflate method (expelling its contents) and a rise method (if you were to let go of it). Again, all balloons are capable of these methods.
Balloons also have predefined responses to certain external events. For instance, a balloon would respond to the event of being punctured by deflating itself, or to the event of being released by rising into the air.
If you were able to program a balloon, the Visual Basic code might look like the following. To set the balloon's properties:
Balloon.Color = Red
Balloon.Diameter = 10
Balloon.Inflated = True
Note the syntax of the code — the object (Balloon) followed by the property (.Color) followed by the assignment of the value (Red). You could change the color of the balloon from code by repeating this statement and substituting a different value. Properties can also be set in the Properties window while you are designing your application.
A balloon's methods are invoked like this:
Balloon.Inflate
Balloon.Deflate
Balloon.Rise 5
The syntax is similar to the property — the object (a noun) followed by the method (a verb). In the third example, there is an additional item, called an argument, which denotes the distance to rise. Some methods will have one or more arguments to further describe the action to be performed.
The balloon might respond to an event as follows:
Sub Balloon_Puncture()
Balloon.Deflate
Balloon.MakeNoise "Bang"
Balloon.Inflated = False
Balloon.Diameter = 1
End Sub
In this case, the code describes the balloon's behavior when a puncture event occurs: invoke the Deflate method, then invoke the MakeNoise method with an argument of "Bang" (the type of noise to make). Since the balloon is no longer inflated, the Inflated property is set to False and the Diameter property is set to a new value.
While you can't actually program a balloon, you can program a Visual Basic form or control. As the programmer, you are in control. You decide which properties should be changed, methods invoked or events responded to in order to achieve the desired appearance and behavior.
Designing a Form
Form objects are the basic building blocks of a Visual Basic application, the actual windows with which a user interacts when they run the application. Forms have their own properties, events, and methods with which you can control their appearance and behavior.
Figure 1.6 Forms and controls have their own properties, events, and methods
The first step in designing a form is to set its properties. You can set a form's properties at design time in the Properties window, or at run time by writing code.
The Properties window
Form Events and Methods
As objects, forms can perform methods and respond to events. The Resize event of a form is triggered whenever a form is resized, either by user interaction or through code. This allows you to perform actions such as moving or resizing controls on a form when its dimensions have changed.
The Activate event occurs whenever a form becomes the active form; the Deactivate event occurs when another form or application becomes active. These events are convenient for initializing or finalizing the form's behavior. For example, in the Activate event you might write code to highlight the text in a particular text box; in the Deactivate event you might save changes to a file or database.
To make a form visible, you would invoke the Show method:
Form2.Show
Invoking the Show method has the same effect as setting a form's Visible property to True.
Many of a form's methods involve text or graphics. The Print, Line, Circle, and Refresh methods are useful for printing or drawing directly onto a form's surface. These methods and more are discussed in "Working with Text and Graphics."
Using Command Buttons
Most Visual Basic applications have command buttons that allow the user to simply click them to perform actions. When the user chooses the button, it not only carries out the appropriate action, it also looks as if it's being pushed in and released. Whenever the user clicks a button, the Click event procedure is invoked. You place code in the Click event procedure to perform any action you choose.
There are many ways to choose a command button at run time:
- Use a mouse to click the button
- Move the focus to the button by pressing the TAB key, and then choose the button by pressing the SPACEBAR or ENTER. (See "Understanding Focus" later in this chapter.)
- Press an access key (ALT+ the underlined letter) for a command button.
- Set the command button's Value property to True in code:
cmdClose.Value = True - Invoke the command button's Click event in code
cmdClose_Click - If the command button is the default command button for the form, pressing ENTER chooses the button, even if you change the focus to a different control other than a command button. At design time, you specify a default command button by setting that button's Default property to True.
- If the command button is the default Cancel button for the form, then pressing ESC chooses the button, even if you change the focus to another control. At design time, you specify a default Cancel button by setting that button's Cancel property to True.
You use the Caption property to display text on the button to tell the user what the button does. In Figure 3.4, the Test Buttons example from the Controls sample application contains a command button with its Caption property set to "Change Signal." (For a working version of this example, see Button.frm in the Controls.vbp sample application.)
Notice that 'S' is the access key for this button, denoted by an underline. Inserting an ampersand (&) in the text of the Caption property makes the character following it the access key for that button (for example, Change &Signal).
When a user clicks the command button, the code in the command button's Click event procedure is executed. In the example, a different traffic light icon is displayed each time the button is clicked. Using Labels to Display Text
A label control displays text that the user cannot directly change. You can use labels to identify controls, such as text boxes and scroll bars, that do not have their own Caption property. The actual text displayed in a label is controlled by the Caption property, which can be set at design time in the Properties window or at run time by assigning it in code. By default, the caption is the only visible part of the label control. However, if you set the BorderStyle property to 1 (which you can do at design time), the label appears with a border — giving it a look similar to a text box. You can also change the appearance of the label by setting the BackColor, BackStyle, ForeColor, and Font properties.
The WordWrap property causes the label to grow vertically to fit its contents, while retaining the same width, as shown in Figure 3.6. For a working version of this example, see Wordwrap.frm in the Controls.vbp sample application.
WordWrap example
Working with Text in a Text Box
You can control the insertion point and selection behavior in a text box with the SelStart, SelLength and SelText properties. These properties are only available at run time.
When a text box first receives the focus, the default insertion point or cursor position within the text box is to the left of any existing text. It can be moved by the user from the keyboard or with the mouse. If the text box loses and then regains the focus, the insertion point will be wherever the user last placed it.
In some cases, this behavior can be disconcerting to the user. In a word processing application, the user might expect new characters to appear after any existing text. In a data entry application, the user might expect their typing to replace any existing entry. The SelStart and SelLength properties allow you to modify the behavior to suit your purpose
The SelLength property is a numeric value that sets the width of the insertion point. Setting the SelLength to a number greater than 0 causes that number of characters to be selected and highlighted, starting from the current insertion point. Figure 3.8 shows the selection behavior.
If the user starts typing while a block of text is selected, the selected text will be replaced. In some cases, you might want to replace a text selection with new text by using a paste command. The SelText property is a string of text that you can assign at run time to replace the current selection. If no text is selected, SelText will insert its text at the current insertion point. Option with check Box
A check box indicates whether a particular condition is on or off. You use check boxes in an application to give users true/false or yes/no options. Because check boxes work independently of each other, a user can select any number of check boxes at the same time.
Check box example
Events in the Check Box Application
The Click event for the check box occurs as soon as you click the box. This event procedure tests to see whether the check box has been selected (that is, if its Value = vbChecked). If so, the text is converted to bold or italic by setting the Bold or Italic properties of the Font object returned by the Font property of the text box.
Private Sub chkBold_Click ()
If ChkBold.Value = vbChecked Then ' If checked.
txtDisplay.Font.Bold = True
Else ' If not checked.
txtDisplay.Font.Bold = False
End If
End Sub
Private Sub chkItalic_Click ()
If ChkItalic.Value = vbChecked Then ' If checked.
txtDisplay.Font.Italic = True
Else ' If not checked.
txtDisplay.Font.Italic = False
End If
End Sub
Grouping Options with Option Buttons
Option buttons present a set of two or more choices to the user. Unlike check boxes, however, option buttons should always work as part of a group; selecting one option button immediately clears all the other buttons in the group. Defining an option button group tells the user, "Here is a set of choices from which you can choose one and only one."
For example Selecting an option button
Each time a new option button is selected, the code in its Click event updates the appropriate variable:
Private Sub opt586_Click()
strComputer = "Pentium"
Call DisplayCaption
End Sub
It then calls a sub procedure, called DisplayCaption, that concatenates the two variables and updates the label's Caption property:
Sub DisplayCaption()
lblDisplay.Caption = "You selected a " & _
strComputer & " running " & strSystem
End Sub
A sub procedure is used because the procedure of updating the Caption property is essentially the same for all five option buttons, only the value of the variables change from one instance to the next. This saves you from having to repeat the same code in each of the Click events.
Using List Boxes and Combo Boxes
List boxes and combo boxes present a list of choices to the user. By default, the choices are displayed vertically in a single column, although you can set up multiple columns as well. If the number of items exceeds what can be displayed in the combo box or list box, scroll bars automatically appear on the control. The user can then scroll up and down or left to right through the list.
Single-column list box
A combo box control combines the features of a text box and a list box. This control allows the user to select either by typing text into the combo box or by selecting an item from its list.
Combo box
In contrast to some other controls that contain a single value; for example the label's Caption property or the text box's Text property, list boxes and combo boxes contain multiple values or a collection of values. They have built-in methods for adding, removing and retrieving values from their collections at run time. To add several items to a list box named List1, the code would look like this:
List1.AddItem "Paris"
List1.AddItem "New York"
List1.AddItem "San Francisco"
List boxes and combo boxes are an effective way to present a large number of choices to the user in a limited amount of space.
Using Scroll Bars as Input Devices
Although scroll bars are often tied to text boxes or windows, you'll sometimes see them used as input devices. Because these controls can indicate the current position on a scale, scroll bar controls can be used individually to control program input — for example, to control the sound volume or to adjust the colors in a picture. The HScrollBar (horizontal) and VScrollBar (vertical) controls operate independently from other controls and have their own set of events, properties, and methods. Scroll bar controls are not the same as the built-in scroll bars that are attached to text boxes, list boxes, combo boxes, or MDI forms (text boxes and MDI forms have a ScrollBars property to add or remove scroll bars that are attached to the control).
Windows interface guidelines now suggest using slider controls as input devices instead of scroll bars. Examples of slider controls can be seen in the Windows 95/98 control panel. A slider control of this type is included in the Professional and Enterprise editions of Visual Basic.
Working With the Picture Box Control
The primary use for the picture box control is to display a picture to the user. The actual picture that is displayed is determined by the Picture property. The Picture property contains the file name (and optional path) for the picture file that you wish to display.
To display or replace a picture at run time, you can use the LoadPicture function to set the Picture property. You supply the name (and optional path) for the picture and the LoadPicture function handles the details of loading and displaying it: picMain.Picture = LoadPicture("VANGOGH.gif")
The picture box control has an AutoSize property that, when set to True, causes the picture box to resize automatically to match the dimensions of its contents. Take extra care in designing your form if you plan on using a picture box with the AutoSize enabled. The picture will resize without regard to other controls on the form, possibly causing unexpected results, such as covering up other controls. It's a good idea to test this by loading each of the pictures at design time.
Using the Picture Box as a Container
The picture box control can also be used as a container for other controls. Like the frame control, you can draw other controls on top of the picture box. The contained controls move with the picture box and their Top and Left properties will be relative to the picture box rather than the form.
A common use for the picture box container is as a toolbar or status bar. You can place image controls on it to act as buttons, or add labels to display status messages. By setting the Align property to Top, Bottom, Left, or Right, the picture box will "stick" to the edge of the form.
Picture box used as a status bar
The Images Application
The form shown in Figure 3.18 uses four image controls, a shape control, a picture box, and a command button. When the user selects a playing card symbol, the shape control highlights the symbol and a description is displayed in the picture box. For a working version of this example, see Images.frm in the Controls.vbp sample application.
Image and shape control example
The code in the image control Click event looks like this:
Private Sub imgHeart_Click()
shpCard.Left = imgClub.Left
picStatus.Cls
picStatus.Print "Selected: Club"
shpCard.Visible = True
End Sub
Additional Controls
Several other standard controls are included in the Visual Basic toolbox. Some controls are useful for working with large amounts of data contained in an external database. Other controls can be used to access the Windows file system. Still other controls defy categorization, but are useful nonetheless.
You can also use ActiveX controls, previously called custom or OLE controls, in a Visual Basic application in the same way that you use the standard controls. The Professional and Enterprise editions of Visual Basic include several ActiveX controls as well as the capability to build your own controls. Additional ActiveX controls for just about any purpose imaginable are available for purchase from numerous vendors.
Data Access Controls
In today's business, most information is stored in one or more central databases. Visual Basic includes several data access controls for accessing most popular databases, including Microsoft Access and SQL Server.
- The ADO Data control is used to connect to a database. Think of it as a pipeline between the database and the other controls on your form. Its properties, methods, and events allow you to navigate and manipulate external data from within your own application.
- The DataList control is similar to the list box control. When used in conjunction with an ADO Data control, it can be automatically filled with a list of data from a field in an external database.
- The DataCombo control is like a combination of the DataList control and a text box. The selected text in the text box portion can be edited, with the changes appearing in the underlying database.
- The DataGrid control displays data in a grid or table. When used in conjunction with an ADO Data control, it presents fully editable data from multiple fields in an external database.
- The Microsoft Hierarchical FlexGrid control is a unique control for presenting multiple views of data. Think of it as a combination of a grid and a tree or outline control. At run time, the user can rearrange columns and rows to provide different views of the data.
Visual Basic includes three controls for adding file handling capabilities to your application. These controls are normally used together to provide a view of drives, directories and files; they have special properties and events that tie them together.
- The DriveListBox control looks like a combo box. It provides a drop-down list of drives from which the user can select.
- The DirListBox is similar to a list box control, but with the built-in capability of displaying a list of directories in the currently selected drive.
- The FileListBox control also looks like a list box with a list of file names in a selected directory.
Several other standard controls are included in Visual Basic. Each serves a unique purpose.
- The timer control can be used to create an event in your application at a recurring interval. This is useful for executing code without the need for user interaction.
- The OLE container control is an easy way to add capabilities like linking and embedding to your application. Through the OLE container control, you can provide access to the functionality of any OLE-enabled application such as Microsoft Excel, Word and many others.
- The common dialog control adds built-in dialog boxes to your application for the selection of files, colors, fonts, and printing functions.
Understanding Focus
Focus is the ability to receive user input through the mouse or keyboard. When an object has the focus, it can receive input from a user. In the Microsoft Windows interface, several applications can be running at any time, but only the application with the focus will have an active title bar and can receive user input. On a Visual Basic form with several text boxes, only the text box with the focus will display text entered by means of the keyboard.
The GotFocus and LostFocus events occur when an object receives or loses focus. Forms and most controls support these events.
You can give focus to an object by:
- Selecting the object at run time.
- Using an access key to select the object at run time.
- Using the SetFocus method in code.
An object can receive focus only if its Enabled and Visible properties are set to True. The Enabled property allows the object to respond to user-generated events such as keyboard and mouse events. The Visible property determines whether an object is visible on the screen.
Validate Event of Controls
Controls also have a Validate event, which occurs before a control loses focus. However, this event occurs only when the CausesValidation property of the control that is about to receive the focus is set to True. In many cases, because the Validate event occurs before the focus is lost, it is more appropriate than the LostFocus event for data validation. For more information, see "Validating Control Data By Restricting Focus" in "Using Visual Basic's Standard
Controls That Can't Receive Focus
Some controls, such as the lightweight controls, cannot receive focus. Lightweight controls include the following:
- Frame control
- Image control
- Label control
- Line control
- Shape control
Setting the Tab Order
The tab order is the order in which a user moves from one control to another by pressing the TAB key. Each form has its own tab order. Usually, the tab order is the same as the order in which you created the controls.
Tab example
To change the tab order for a control, set the TabIndex property. The TabIndex property of a control determines where it is positioned in the tab order. By default, the first control drawn has a TabIndex value of 0, the second has a TabIndex of 1, and so on. When you change a control's tab order position, Visual Basic automatically renumbers the tab order positions of the other controls to reflect insertions and deletions. For example, if you make Command1 first in the tab order, the TabIndex values for the other controls are automatically adjusted upward, as shown in the following table.
The highest TabIndex setting is always one less than the number of controls in the tab order (because numbering starts at 0). Even if you set the TabIndex property to a number higher than the number of controls, Visual Basic converts the value back to the number of controls minus 1. Removing a Control from the Tab Order
Usually, pressing TAB at run time selects each control in the tab order. You can remove a control from the tab order by setting its TabStop property to False (0).
A control whose TabStop property has been set to False still maintains its position in the actual tab order, even though the control is skipped when you cycle through the controls with the TAB key.
Note An option button group has a single tab stop. The selected button (that is, the button with its Value set to True) has its TabStop property automatically set to True, while the other buttons have their TabStop property set to False
Menu Basics
If you want your application to provide a set of commands to users, menus offer a convenient and consistent way to group commands and an easy way for users to access them.
The elements of a menu interface on a Visual Basic form
The menu bar appears immediately below the title bar on the form and contains one or more menu titles. When you click a menu title (such as File), a menu containing a list of menu items drops down. Menu items can include commands (such as New and Exit), separator bars, and submenu titles. Each menu item the user sees corresponds to a menu control you define in the Menu Editor (described later in this chapter).
To make your application easier to use, you should group menu items according to their function. In Figure 3.21, for example, the file-related commands New, Open, and Save As… are all found on the File menu.
Some menu items perform an action directly; for example, the Exit menu item on the File menu closes the application. Other menu items display a dialog box — a window that requires the user to supply information needed by the application to perform the action. These menu items should be followed by an ellipsis (…). For example, when you choose Save As… from the File menu, the Save File As dialog box appears.
A menu control is an object; like other objects it has properties that can be used to define its appearance and behavior. You can set the Caption property, the Enabled and Visible properties, the Checked property, and others at design time or at run time. Menu controls contain only one event, the Click event, which is invoked when the menu control is selected with the mouse or using the keyboard.
Pop-up Menus A pop-up menu is a floating menu that is displayed over a form, independent of the menu bar, as shown in Figure 3.22. The items displayed on the pop-up menu depend on the location of the pointer when the right mouse button is pressed; therefore, pop-up menus are also called context menus. (In Windows 95 or later, you activate context menus by clicking the right mouse button.) You should use pop-up menus to provide an efficient method for accessing common, contextual commands.
A pop-up menu
Any menu that has at least one menu item can be displayed at run time as a pop-up menu. To display a pop-up menu, use the PopupMenu method
Using the Menu Editor
With the Menu Editor, you can add new commands to existing menus, replace existing menu commands with your own commands, create new menus and menu bars, and change and delete existing menus and menu bars. The main advantage of the Menu Editor is its ease of use. You can customize menus in a completely interactive manner that involves very little programming.
To display the Menu Editor
- From the Tools menu, choose Menu Editor.
While most menu control properties can be set using the Menu Editor; all menu properties are also available in the Properties window. You would normally create a menu in the Menu Editor; however, to quickly change a single property, you could use the Properties window.
Prompting the User with Dialog Boxes In Windows-based applications, dialog boxes are used to prompt the user for data needed by the application to continue or to display information to the user. Dialog boxes are a specialized type of form object that can be created in one of three ways:
- Predefined dialog boxes can be created from code using the MsgBox or InputBox functions.
- Customized dialog boxes can be created using a standard form or by customizing an existing dialog box.
- Standard dialog boxes, such as Print and File Open, can be created using the common dialog control.
This dialog is displayed when you invoke the MsgBox function in code. The code for displaying the dialog box shown in Figure 3.24 looks like this:
MsgBox "Error encountered while trying to open file," & vbCrLf & "please retry.", vbExclamation, "Text Editor"
You supply three pieces of information, or arguments, to the MsgBox function: the message text, a constant (numeric value) to determine the style of the dialog box, and a title. Styles are available with various combinations of buttons and icons to make creating dialog boxes easy.
Because most dialog boxes require user interaction, they are usually displayed as modal dialog boxes. A modal dialog box must be closed (hidden or unloaded) before you can continue working with the rest of the application. For example, a dialog box is modal if it requires you to click OK or Cancel before you can switch to another form or dialog box.
Modeless dialog boxes let you shift the focus between the dialog box and another form without having to close the dialog box. You can continue to work elsewhere in the current application while the dialog box is displayed. Modeless dialog boxes are rare; you will usually display a dialog because a response is needed before the application can continue. From the Edit menu, the Find dialog box in Visual Basic is an example of a modeless dialog box. Use modeless dialog boxes to display frequently used commands or information.
Introduction to Visual Basic Controls
The Visual Basic toolbox contains the tools you use to draw controls on your forms
The Visual Basic toolbox
Control Categories
Intrinsic controls, such as the command button and frame controls. These controls are contained inside the Visual Basic .exe file. Intrinsic controls are always included in the toolbox, unlike ActiveX controls and insertable objects, which can be removed from or added to the toolbox.
ActiveX controls, which exist as separate files with a .ocx file name extension. These include controls that are available in all editions of Visual Basic (DataCombo, DataList controls, and so on) and those that are available only in the Professional and Enterprise editions (such as Listview, Toolbar, Animation, and Tabbed Dialog). Many third-party ActiveX controls are also available.
Note Controls with the .vbx file name extension use older technology and are found in applications written in earlier versions of Visual Basic. When Visual Basic opens a project containing a .vbx control, the default behavior is to replace the .vbx control with an .ocx control, but only if an .ocx version of the control is available. See "Updating Older Versions of Visual Basic Controls" later in this chapter for information on updating controls to the .ocx format.
Insertable Objects, such as a Microsoft Excel Worksheet object containing a list of all your company's employees, or a Microsoft Project Calendar object containing the scheduling information for a project. Since these can be added to the toolbox, they can be considered controls. Some of these objects also support Automation (formerly called OLE Automation), which allows you to program another application's objects from within a Visual Basic application.
Intrinsic Controls
The following table summarizes the intrinsic controls found in the Visual Basic toolbox.
Icon
Control name Class name Description Check box CheckBox Displays a True/False or Yes/No option. You can check any number of check boxes on a form at one time. Combo box ComboBox Combines a text box with a list box. Allows a user to type in a selection or select an item from a drop-down list. Command button CommandButton Carries out a command or action when a user chooses it. Data Data Enables you to connect to an existing database and display information from it on your forms. Directory list box DirListBox Displays and allows a user to select directories and paths. Drive list box DriveListBox Displays and allows a user to select valid disk drives. File list box FileListBox Displays and allows a user to select from a list of files. Frame Frame Provides a visual and functional container for controls.
Horizontal and vertical scroll bars HScrollBar and VScrollBar Allow a user to add scroll bars to controls that do not automatically provide them. (These are not the same as the built-in scroll bars found with many controls.) Image Image Displays bitmaps, icons, or Windows metafiles, JPEG, or GIF files; acts like a command button when clicked. Label Label Displays text a user cannot interact with or modify Line Line Adds a straight-line segment to a form. List box ListBox Displays a list of items that a user can choose from. OLE container OLE Embeds data into a Visual Basic application. Option button OptionButton The Option Button control, as part of an option group with other option buttons, displays multiple choices, from which a user can choose only one. Picture box PictureBox Displays bitmaps, icons, or Windows metafiles, JPEG, or GIF files. It also displays text or acts as a visual container for other controls. Shape Shape Adds a rectangle, square, ellipse, or circle to a form, frame, or picture box. Text box TextBox Provides an area to enter or display text. Timer Timer Executes timer events at specified time intervals.
Standard ActiveX Controls
The Learning edition of Visual Basic contains a number of ActiveX controls (referred to as standard ActiveX controls) that allow you to add advanced features to your applications. ActiveX controls have the file name extension .ocx and can be used in your project by manually adding them to the toolbox.
The following table summarizes the standard ActiveX controls available in the Learning edition of Visual Basic.
Icon Control name Class name Description ADO Data Control ADODC Creates a connection to a database using ADO. Assignable to the DataSource property of other controls such as the DataGrid. Common dialog CommonDialog Provides a standard set of dialog boxes for operations such as opening and saving files, setting print options, and selecting colors and fonts. DataCombo DataCombo Provides most of the features of the standard combo box control, plus increased data access capabilities. DataGrid DataGrid A grid control that allows can be data-bound to a data source such as the ADO Data Control. Reading and editing the recordset is possible. DataList DataList Provides most of the features of the standard list box control, plus increased data access capabilities. Microsoft Hierarchical FlexGrid MSHFlexGrid A read-only grid control that can be bound the Data Environment designer to show hierarchical recordsets.
Adding and Removing ActiveX Controls
You move ActiveX controls to and from the toolbox using the following procedures.
From the Project menu, choose Components.
Select the check box next to the name of the .ocx control, and then choose OK. Once a control is placed in the toolbox, you can add it to a form just as you would an intrinsic control.
To remove an ActiveX control
Remove all instances of the control from the forms in your project. Delete any references to the control in the project's code. If references to a deleted control are left in your code, an error message will display when you compile the application.
From the Project menu, choose Components.
Clear the check box next to the name of the .ocx control, and then choose OK. An error message will display if there are remaining instances of the control in your project.
Using the Value of a Control
All controls have a property that you can use to store or retrieve values just by referring to the control, without using the property name. This is called the value of the control and is usually the most important or most commonly used property of the control. The following table lists the property that is considered to be the value for each control.
Control Value Check box Value Combo box Text Command button Value Common dialog Action Data Caption DataCombo Text DataGrid Text DataList Text Directory list box Path Drive list box Drive File list box FileName FlexGrid Text Frame Caption Horizontal scroll bar Value Image Picture Label Caption Line Visible List box Text Option button Value Picture box Picture Shape Shape Text box Text Timer Enabled Vertical scroll bar Value
Whenever you want to refer to a property on a control that happens to be the value of that control, you can do so without specifying the property name in your code. For example, this line sets the value of the Text property of a text box control: Text1 = "This text is assigned to the Text property _
of Text1"
In this example, the Caption property of Label1 is set to the FileName property of File1 whenever the user clicks a file in the file list box:
Private Sub File1_Click ()
Label1 = File1
End Sub
Note Because using the value of a control makes your code somewhat less readable, the examples in this guide do not use it but instead refer explicitly to the properties on all controls. You may want to try writing your code both ways, and decide to use the value of controls in your code if you have no trouble reading it.
Working with Control Arrays
A control array is a group of controls that share the same name and type. They also share the same event procedures. A control array has at least one element and can grow to as many elements as your system resources and memory permit; its size also depends on how much memory and Windows resources each control requires. The maximum index you can use in a control array is 32767. Elements of the same control array have their own property settings. Common uses for control arrays include menu controls and option button groupings.
Note Visual Basic includes the ability to dynamically add unreferenced controls to the Controls collection at run time. This topic refers only to referenced controls added at design time by cutting and pasting a control onto the form. For more information about adding controls at run time, see the reference topic "Add Method (Controls Collection)" and "Add Method (Licenses Collection)."
Why Use Control Arrays?
Adding controls with control arrays uses fewer resources than simply adding multiple controls of the same type to a form at design time. Control arrays are also useful if you want several controls to share code. For example, if three option buttons are created as a control array, the same code is executed regardless of which button was clicked.
If you want to create a new instance of a control at run time, that control must be a member of a control array. With a control array, each new element inherits the common event procedures of the array.
Using the control array mechanism, each new control inherits the common event procedures already written for the array. For example, if your form has several text boxes that each receive a date value, a control array can be set up so that all of the text boxes share the same validation code.
Sample Application: Calc.vbp
The Name and Index property values for the control arrays in the Calculator example are listed in the following table.
Number(n) Operator(n) 0 = Number(0) + = Operator(1) 1 = Number(1) – = Operator(2) 2 = Number(2) X = Operator(3) 3 = Number(3) / = Operator(4) 4 = Number(4) = = Operator(5) 5 = Number(5) 6 = Number(6) 7 = Number(7) 8 = Number(8) 9 = Number(9)
Notice how each control is referred to with the syntax object(index). You specify the index of a control when you create it. In fact, specifying any index for a control at design time makes that control part of an array.
The Index property distinguishes one element of the control array from another. When one of the controls in the array recognizes an event, Visual Basic calls a common event procedure and passes an argument (the value of the Index property) to identify which control actually recognizes the event.
For example, the first line of the Number_Click event procedure is:
Private Sub Number_Click (Index As Integer)
If Number(0) recognizes the event, Visual Basic passes 0 as the index argument, and if Number(1) recognizes the event, Visual Basic passes 1 as the index argument. Other than the index value, the remainder of the Number_Click code that is executed is the same for both Number(0) through Number(9).
Using the ADO Data Control
The ADO Data control uses Microsoft ActiveX Data Objects (ADO) to quickly create connections between data-bound controls and data providers. Data-bound controls are any controls that feature a DataSource property. Data providers can be any source written to the OLE DB specification. You can also easily create your own data provider using Visual Basic's class module.
Although you can use the ActiveX Data Objects directly in your applications, the ADO Data control has the advantage of being a graphic control (with Back and Forward buttons) and an easy-to-use interface that allows you to create database applications with a minimum of code.
The ADO Data Control
Several of the controls found in Visual Basic's Toolbox can be data-bound, including the CheckBox, ComboBox, Image, Label, ListBox, PictureBox, and TextBox controls. Additionally, Visual Basic includes several data-bound ActiveX controls such as the DataGrid, DataCombo, Chart, and DataList controls. You can also create your own data-bound ActiveX controls, or purchase controls from other vendors.
Previous versions of Visual Basic featured the intrinsic Data control and the Remote Data control (RDC) for data access. Both controls are still included with Visual Basic for backward compatibility. However, because of the flexibility of ADO, it's recommended that new database applications be created using the ADO Data Control.
Creating a Front-end Database Application with Minimal Code
It's possible to create a database application using a minimum of code by setting a few properties at design time. If you are using an OLE DB data source, the Microsoft Data Link Name (.UDL) must be created on your machine. See "Creating the Northwind OLE DB Data Link" for a step-by-step example.
To create a simple front-end database application Draw an ADO Data Control on a form. (The icon's ToolTip is "ADODC.")
If the control is not available in the Toolbox, press CTRL+T to display the Components dialog box. In the Components dialog, click Microsoft ADO Data Control.
On the Toolbox, click the ADO Data Control to select it. Then press F4 to display the Properties window.
In the Properties window, click ConnectionString to display the ConnectionString dialog box.
If you have created a Microsoft Data Link file (.UDL), select Use OLE DB File and click Browse to find the file on the computer. If you use a DSN, click Use ODBC Data Source Name and select a DSN from the box, or click New to create one. If you wish to use create a connection string, select Use ConnectionString, and then click Build, and use the Data Link Properties dialog box to create a connection string. After creating the connection string, click OK. The ConnectionString property will be filled with a string like:
driver={SQL Server};server=bigsmile;uid=sa;pwd=pwd;database=pubs
In the Properties window, set the RecordSource property to a SQL statement. For example,
SELECT * FROM Titles WHERE AuthorID = 72
You should always include a WHERE clause when accessing a table. Failing to do so will lock the entire table, which would be a major hindrance to other users.
Draw a TextBox control on the form to display the database information.
In the Properties window, set the DataSource property for Text1 to the name of the ADO Data control (ADODC1). This binds the text box to the ADO Data control.
In the Properties window, click DataField and a list of available fields will drop down. Click the name of the field you want to display.
Repeat steps 6, 7, and 8 for each additional field you want to access.
Press F5 to run the application. You can use the four arrow buttons on the ADO Data control to move to the beginning of the data, to the end of the data, or from record to record through the data.
Setting the ConnectionString, Source, DataSource, and DataField Programmatically
The code below shows how to set these four properties programmatically. Note that setting the DataSource property requires the Set statement.
Private Sub Form_Load()
With ADODC1
.ConnectionString = "driver={SQL Server};" & _
"server=bigsmile;uid=sa;pwd=pwd;database=pubs"
.RecordSource = "Select * From Titles Where AuthorID = 7"
End With
Set Text1.DataSource = ADODC1
Text1.DataField = "Title"
End Sub
ADO Data Control Events
The ADO Data control features several events that you can program. The table below shows the events and when they occur; however the table is not meant to be a complete list all of the conditions when the events occur.
WillMove
On Recordset.Open, Recordset.MoveNext, Recordset.Move, Recordset.MoveLast, Recordset.MoveFirst, Recordset.MovePrevious, Recordset.Bookmark, Recordset.AddNew, Recordset.Delete, Recordset.Requery, Recordset.Resync
MoveComplete
After WillMove
WillChangeField
Before the Value property changes
FieldChangeComplete
After WillChangeField
WillChangeRecord
On Recordset.Update, Recordset.Delete, Recordset.CancelUpdate, Recordset.UpdateBatch, Recordset.CancelBatch
RecordChangeComplete
After WillChangeRecord
WillChangeRecordset
On Recordset.Requery, Recordset.Resync, Recordset.Close, Recordset.Open, Recordset.Filter
RecordsetChangeComplete
After WillChangeRecordset
InfoMessage
When the data provider returns a result
Setting Database-Related Properties of the ADO Data Control
When creating a connection, you can use one of three sources: a Connection String, an OLE DB file (.UDL), or an ODBC Data Source Name (DSN). When using a DSN, it's likely you will not have to alter any of the other properties of the control.
However, if you are familiar with database technology, you can alter some of the other properties exposed on the ADO Data Control. The following list describes database-related properties of the control. The list also suggests a logical order for setting the properties.
Note Database technology is complex, and the suggestions below are not meant to be taken as rules.
ConnectionString — The ConnectionString property is a string that can contain all the settings necessary to make a connection. The parameters passed in the string are driver-dependent. For example, ODBC drivers allow the string to contain driver, provider, default database, server, username, and password.
UserName — The name of the user, necessary if the database is password-protected. Like the Provider property, this property can be specified in the ConnectionString. If you provide both a ConnectionString and a UserName, the ConnnectionString value will override the UserName property's value.
Password — Also needed when accessing a protected database. Like Provider and UserName, this property's value will be overridden if the value is specified in the ConnectionString.
RecordSource — This property generally contains a statement that determines what will be retrieved from the database.
CommandType — The CommandType property instructs the data provider if the Source is a SQL statement, a table name, a stored procedure, or an unknown type.
CursorLocation — This property specifies where the cursor is located, on the client or on the server. The consequences of this decision affect the next few properties you set.
CursorType — The CursorType property determines if the recordset is static, dynamic, or a keyset cursor type.
LockType — The LockType determines how the data is locked when others attempt to change data you are editing. How you set the LockType is a complex decision, dependent on many factors.
Mode — The Mode property determines what you intend to do with the recordset. For example, you can achieve some performance gains by setting it to read-only, if you are only interested in creating reports.
MaxRecords — This property determines how large the cursor will be. How you determine this depends on the size of the records you are retrieving and the resources available on your computer (memory). A large record (one with many columns and large strings) would take more resources than a smaller record. Consequently the MaxRecords property should be no larger than necessary.
ConnectionTimeout — Set the ConnectionTimeout to the number of seconds to wait while establishing the connection. An error is returned if the connection times out.
CacheSize — The CacheSize property specifies how many records can be retrieved from the cursor. If you've set the CursorLocation to client side, then this property can be set to a smaller number (as small as 1) with no adverse effects. If it's on the server side, you should optimize this figure to suit the number of rows you want to view at any one time. For example, if you use the DataGrid control to view 30 rows, set the CacheSize to 60, allowing you to scroll without retrieving more data.
BOFAction, EOFAction — These two properties determine what will happen when the control is at the beginning and end of the cursor. Choices include staying at the beginning or end, moving to the first or last record, or adding a new record (at the end only).
Using the Check Box Control
The check box control displays a check mark when it is selected. It is commonly used to present a Yes/No or True/False selection to the user. You can use check box controls in groups to display multiple choices from which the user can select one or more.
Using the Common Dialog Control
The common dialog control provides a standard set of dialog boxes for operations such as opening and saving files, setting print options, and selecting colors and fonts. The control also has the ability to display Help by running the Windows Help engine.
The common dialog control provides an interface between Visual Basic and the procedures in the Microsoft Windows dynamic-link library Commdlg.dll. To create a dialog box using this control, Commdlg.dll must be in your Microsoft Windows \System directory.
You use the common dialog control in your application by adding it to a form and setting its properties. The dialog displayed by the control is determined by the methods of the control. At run time, a dialog box is displayed or the Help engine is executed when the appropriate method is invoked; at design time, the common dialog control is displayed as an icon on a form. This icon can't be sized.
The common dialog control allows you to display these commonly used dialog boxes:
- Open
- Save As
- Color
- Font
- If you haven't already done so, add the common dialog control to the toolbox by selecting Components from the Project menu. Locate and select the control in the Controls tabbed dialog, then click the OK button.
- On the toolbox, click the CommonDialog control and draw it on a form.
When you draw a common dialog control on a form, it automatically resizes itself. Like the timer control, the common dialog control is invisible at run time. - At run time, use the appropriate method, as listed in the following table, to display the desired dialog.
Displaying Open and Save As Dialog Boxes
The Open dialog box allows the user to specify a drive, a directory, a file name extension, and a file name.
The Save As dialog box is identical to the Open dialog in appearance, except for the dialog's caption, and file names appearing dimmed out. At run time, when the user chooses a file and closes the dialog box, the FileName property is used to get the selected file name.
To display the Open dialog box
- Specify the list of file filters that are displayed in the Files of type list box.
You can do this by setting the Filter property using the following format: description1 | filter1 | description2 | filter2...
Description is the string displayed in the list box — for example, "Text Files (*.txt)." Filter is the actual file filter — for example, "*.txt." Each
description | filter set must be separated by a pipe symbol (|).
- Use the ShowOpen method to display the dialog box.
With all the common dialog boxes, when the CancelError property is True, an error is generated when the user clicks the dialog box's Cancel button. You detect that the Cancel button was pressed by trapping the error when the dialog box is displayed.
The following code displays an Open dialog box and uses the selected file name as an argument to a procedure that opens a file:
Private Sub mnuFileOpen_Click ()
' CancelError is True.
On Error GoTo ErrHandler
' Set filters.
CommonDialog1.Filter = "All Files (*.*)|*.*|Text _
Files (*.txt)|*.txt|Batch Files (*.bat)|*.bat"
' Specify default filter.
CommonDialog1.FilterIndex = 2
' Display the Open dialog box.
CommonDialog1.ShowOpen
' Call the open file procedure.
OpenFile (CommonDialog1.FileName)
Exit Sub
ErrHandler:
' User pressed Cancel button.
Exit Sub
End Sub
Using the Color Dialog Box
The Color dialog box allows the user to select a color from a palette or to create and select a custom color. At run time, when the user chooses a color and closes the dialog box, you use the Color property to get the selected color.
The Color dialog box
To display the Color dialog box
- Set the Flags property for the common dialog control to the Visual Basic constant cdlCCRGBInit.
- Use the ShowColor method to display the dialog box.
Private Sub Command1_Click ()
' Set Cancel to True.
CommonDialog1.CancelError = True
On Error GoTo ErrHandler
' Set the Flags property.
CommonDialog1.Flags = cdlCCRGBInit
' Display the Color dialog box.
CommonDialog1.ShowColor
' Set the form's background color to the selected
' color.
Form1.BackColor = CommonDialog1.Color
Exit Sub
ErrHandler:
' User pressed Cancel button.
Exit Sub
End Sub
Using the Font Dialog Box The Font dialog box allows the user to select a font by its size, color, and style. Once the user makes selections in the Font dialog box, the following properties contain information about the user's selection.
Property
Determines Color The selected color. To use this property, you must first set the Flags property to cdlCFEffects FontBold Whether bold was selected. FontItalic Whether italic was selected. FontStrikethru Whether strikethrough was selected. FontUnderline Whether underline was selected. FontName The selected font name. FontSize The selected font size. The Font dialog box
To display the Font dialog box
- Set the Flags property to one of the following Visual Basic constant values:
- cdlCFScreenFonts (screen fonts)
- cdlCFPrinterFonts (printer fonts)
- cdlCFBoth (for both screen and printer fonts)
Caution You must set the Flags property to one of these values before displaying the Font dialog box. Otherwise, the error No fonts exist occurs.
- Use the ShowFont method to display the dialog box.
Private Sub Command1_Click ()
' Set Cancel to True.
CommonDialog1.CancelError = True
On Error GoTo ErrHandler
' Set the Flags property.
CommonDialog1.Flags = cdlCFBoth Or cdlCFEffects
' Display the Font dialog box.
CommonDialog1.ShowFont
' Set text properties according to user's
' selections.
Text1.Font.Name = CommonDialog1.FontName
Text1.Font.Size = CommonDialog1.FontSize
Text1.Font.Bold = CommonDialog1.FontBold
Text1.Font.Italic = CommonDialog1.FontItalic
Text1.Font.Underline = CommonDialog1.FontUnderline
Text1.FontStrikethru = CommonDialog1.FontStrikethru
Text1.ForeColor = CommonDialog1.Color
Exit Sub
ErrHandler:
' User pressed Cancel button.
Exit Sub
End Sub
Using the Print Dialog Box
The Print dialog box allows the user to specify how output should be printed. The user can specify a range of pages to be printed, a print quality, a number of copies, and so on. This dialog box also displays information about the currently installed printer and allows the user to configure or reinstall a new default printer.
Note This dialog box does not actually send data to a printer. It allows users to specify how they want data printed. You must write code to print the data in the format they select.
At run time, when the user makes selections in the Print dialog box, the following properties contain information about the user's selection. Property
Determines Copies The number of copies to print. FromPage The page to start printing. ToPage The page to stop printing. hDC The device context for the selected printer. Orientation The page's orientation (portrait or landscape).
The Print dialog box
To display the Print dialog box
- Set any desired default settings for the dialog by setting the appropriate Print dialog properties.
For example, to display 2 in the Copies box when the dialog is displayed, set the Copies property to 2:
CommonDialog1.Copies = 2 - Use the ShowPrinter method to display the Print dialog box.
Private Sub Command1_Click ()
Dim BeginPage, EndPage, NumCopies, Orientation, i
' Set Cancel to True.
CommonDialog1.CancelError = True
On Error GoTo ErrHandler
' Display the Print dialog box.
CommonDialog1.ShowPrinter
' Get user-selected values from the dialog box.
BeginPage = CommonDialog1.FromPage
EndPage = CommonDialog1.ToPage
NumCopies = CommonDialog1.Copies
Orientation = CommonDialog1.Orientation
For i = 1 to NumCopies
' Put code here to send data to your printer.
Next
Exit Sub
ErrHandler:
' User pressed Cancel button.
Exit Sub
End Sub
Note If the PrinterDefault property is set to True, you can print to the Visual Basic Printer object. In addition, when the PrinterDefault property is True, any changes the user makes in the Setup portion of the Print dialog box are used to change the printer settings in the user's Printer setup.
Using the ShowHelp Method to Display a Help File
The ShowHelp method of the common dialog control allows you to display a Help file.
To display a Help file using the ShowHelp method
- Set the HelpCommand and HelpFile properties.
- Use the ShowHelp method to display the specified Help file.
Private Sub Command1_Click()
' Set Cancel to True.
CommonDialog1.CancelError = True
On Error GoTo ErrHandler
' Set the HelpCommand Property
CommonDialog1.HelpCommand = cdlHelpForceFile
' Specify the Help file.
CommonDialog1.HelpFile = "c:\Windows\Cardfile.hlp"
' Display the Windows Help engine.
CommonDialog1.ShowHelp
Exit Sub
ErrHandler:
' User pressed Cancel button.
Exit Sub
End Sub
Using the Data Control
The intrinsic Data control implements data access by using the Microsoft Jet Database engine –the same database engine that powers Microsoft Access. This technology gives you seamless access to many standard database formats and allows you to create data-aware applications without writing any code. The intrinsic Data control is best suited to smaller (desktop) databases, such as Access and ISAM databases.
You can use the intrinsic Data control to create applications that display, edit, and update information from many types of existing databases, including Microsoft Access, Btrieve, dBASE, Microsoft FoxPro®, and Paradox. You can also use it to access Microsoft Excel, Lotus 1-2-3, and standard ASCII text files as if they were true databases. In addition, the data control allows you to access and manipulate remote Open Database Connectivity (ODBC) databases such as Microsoft SQL Server and Oracle.
Note Both the Data control and Remote Data control are included with Visual Basic for backward compatibility. However, because of the flexibility of ActiveX Data Objects (ADO), it's recommended that you create new database applications using the ADO Data control. For more details, see "Using the ADO Data Control." The Data control, Remote Data control, and the ADO Data control are all conceptually similar: all three are "data controls" that connect a data source to a data-bound control. All three also share the same appearance — a set of four buttons that allow the user to go immediately to the beginning of the recordset, end of the recordset, and scroll backwards and forwards through the recordset.
Creating a Simple Database Application with the Data Control
To create a simple database application with the Data control
- Draw a Data control on the form. The Data control is an intrinsic control and is always available.
- Click the Data control to select it, and press F4 the display the Properties window.
- In the Properties window, set the Connect property to the type of database you want to use.
- In the Properties window, set the DatabaseName property to the file or directory name of the database you want to connect to.
- On the Properties window, set the RecordSource property to the name of the database table you want to access.
- Draw a TextBox control on the form.
- Click the TextBox control to select it, and on the Properties window set the DataSource property to the Data control.
- On the Properties window, set the DataField property to the name of the field in the database you want to view or modify.
- Repeat steps 6, 7, and 8 for each additional field you want to access.
- Press F5 to run the application.
The following data-related properties can be set at design time. The list suggests a logical order for setting the properties:
Note Database technology is a complex science, and the suggestions below are not meant to be taken as rules.
- RecordsetType — The RecordsetType property determines if the recordset is a table, dynaset, or snapshot. The choice affects what recordset properties are available. For example, snapshot-type recordsets are more limited than dynaset recordsets.
- DefaultType — The DefaultType property specifies whether JET or ODBCDirect workspaces are used.
- DefaultCursorType — The DefaultCursorType property determines the location of the cursor. You can allow the ODBC driver to determine the cursor location, or specify server or ODBC cursors. The DefaultCursorType property is valid only when using ODBCDirect workspaces.
- Exclusive — Determines if the data is for a single- or multi-user environment.
- Options — The property determines the characteristics of the recordset. For example, in a multi-user environment, you can set the Options property to deny changes made by others.
- BOFAction, EOFAction — These two properties determine what will happen when the control is at the beginning and end of the cursor. Choices include staying at the beginning or end, moving to the first or last record, or adding a new record (at the end only).
The DataGrid control is a spreadsheet-like bound control that displays a series of rows and columns representing records and fields from a Recordset object. You can use the DataGrid to create an application that allows the end user to read and write to most databases. The DataGrid control can be quickly configured at design time with little or no code. When you set the DataGrid control's DataSource property at design time, the control is automatically filled and its column headers are automatically set from the data source's recordset. You can then edit the grid's columns; delete, rearrange, add column headers to, or adjust any column's width.
At run time, the DataSource can be programmatically switched to view a different table, or you can modify the query of the current database to return a different set of records.
Note The DataGrid control is code-compatible with the DBGrid control that shipped in Visual Basic 5.0 with one exception: the DataGrid control doesn't support the DBGrid notion of "unbound mode." The DBGrid control is included with Visual Basic in the Tools directory.
Possible Uses
- View and edit data on a remote or local database.
- Used in conjunction with another data-bound control, such as the DataList control, use the DataGrid control to display records from one table that are linked through a common field to another table displayed by the second data-bound control.
You can create a database application with the DataGrid control without writing a line of code by taking advantage of its design-time features. The following instructions outline the general steps needed to implement the DataGrid control in a typical use. For complete step-by-step instructions, see the topic "DataGrid Scenario 1: Create a Simple Database Application with the DataGrid Control."
To implement a DataGrid control at design-time
- Create a Microsoft Data Link (.UDL) file for the database you wish to access. See the topic "Creating the Northwind OLE DB Data Link" for an example.
- Place an ADO Data control on a form, and set the ConnectionString property to the OLE DB data source created in step 1.
- In the RecordSource field of the Ado Data control, type a SQL statement that returns a recordset. For example,
Select * From MyTableName Where CustID = 12 - Place a DataGrid control on a form, and set the DataSource property to the ADO Data control.
- Right-click the DataGrid control and then click Retrieve Fields.
- Right-click the DataGrid control and then click Edit.
- Resize, delete, or add columns to the grid.
- Right-click the DataGrid control and then click Properties.
- Using the Property Pages dialog box, set the appropriate properties of the control to configure the grid as you wish it to appear and behave.
Once you have created a grid using the design-time features, you may also wish to dynamically change the data source of the grid at run time. The general methods for accomplishing this are discussed below.
Changing the RecordSource of the DataSource
The most common method of changing displayed data is to alter the query of the DataSource. For example, if the DataGrid control uses an ADO Data control as its DataSource, rewriting the RecordSource and refreshing the ADO Data control will change the data displayed. ' The ADO Data control is connected to the Northwind database's
' Products table. The new query asks for all records which have
' the SupplierID = 12.
Dim strQuery As String
strQuery = "SELECT * FROM Suppliers WHERE SupplierID = 12"
Adodc1.RecordSource = strQuery
Adodc1.Refresh
Changing the DataSource
At run-time you can reset the DataSource property to a different data source. For example, you may have several ADO Data controls, each connected to different databases, or set to different RecordSource properties. Simply reset the DataSource from one ADO Data control to another: ' Reset the DataSource to an ADO Data control that is connected to
' the Pubs database, using the Authors table.
Set DataGrid1.DataSource = adoPubsAuthors
Rebind the DataSource
When using the DataGrid control with a remote database such as SQLServer, it's possible that the structure of the table may become altered. For example, a field may be added to the table. In that case, you can invoke the Rebind method to recreate the grid from the new structure. Note that if you have altered the columns' layout of the grid at design-time, the DataGrid control will attempt to recreate the current layout, including any empty columns. You can, however, force the grid to reset all columns by first invoking the ClearFields method. Returning Values from the DataGrid
Once the DataGrid is connected to a database, you may want to monitor which cell the user has clicked. Use the RowColChange event — not the Click event — as shown below: Private Sub DataGrid1_RowColChange(LastRow As Variant, ByVal LastCol As Integer)
' Print the Text, row, and column of the cell the user clicked.
Debug.Print DataGrid1.Text; DataGrid1.Row; DataGrid1.Col
End Sub
Using the CellText and CellValue Methods
The CellText and CellValue properties are useful when a column has been formatted using the NumberFormat property. The NumberFormat property changes the format of any column that contains a number without changing the format of the actual data. For example, given a grid with a column named ProductID that contains integers, the code below will cause the DataGrid to display the values in the format "P-0000." In other words, although the actual value held in the ProductID field is "3," the value displayed by the grid will be "P-0003." Private Sub Form_Load()
DataGrid1.Columns("ProductID").NumberFormat = "P-0000"
End Sub
To return the actual value contained in the database, use the CellValue method, as shown below:
Private Sub DataGrid1_RowColChange(LastRow As Variant, ByVal LastCol As Integer)
Debug.Print _
DataGrid1.Columns("ProductID").CellValue(DataGrid1.Bookmark)
End Sub
Note Both the CellValue used above, and the CellText value used below, require the bookmark property as an argument to function correctly.
Conversely, if you want to return the formatted value of the field, use the CellText method:
Private Sub DataGrid1_RowColChange(LastRow As Variant, ByVal LastCol As Integer)
Debug.Print _
DataGrid1.Columns("ProductID").CellText(DataGrid1.Bookmark)
End Sub
Note The CellText method above is equivalent to using the Text property of the DataGrid control. Creating the Northwind OLE DB Data Link
An essential step in accessing data is creating an OLE DB data source for each database you want to access. The steps below create such an object for the Nwind.mdb (Northwind), supplied with Visual Basic. This data source is used in some of the example procedures supplied with the Visual Basic documentation. You need to create the OLE DB data source only once on a computer. To create the Northwind OLE DB data source
- Open Windows Explorer, or Windows NT Explorer.
- Open a directory where you want to create the OLE DB data source. In this example, open Program Files, Microsoft Visual Studio, and VB98.
- Right-click the right pane of Explorer, and then click New on the context menu. From the list of file types, click Microsoft Data Link.
- Rename the new file Northwind.UDL.
- Right-click the file and click Properties on the context menu to display the Northwind.UDL Properties dialog box.
- Click the Provider tab.
- Select Microsoft Jet 3.51 OLE DB Provider
- Click the Next button to go to the Connection tab.
- Click the ellipsis button (…) next to the first text box.
- Use the Select Access Database dialog box to navigate to the nwind.mdb file, which is installed in the Program Files\Microsoft Visual Studio\Vb98 directory.
- Click Test Connection to check the connection.
- If the connection passes, click OK.
Using only a DataGrid and an ADO Data control you can create a database application that allows the end user to read and write to a recordset.
Create a simple database application using the ADO Data Control
- Create an OLE DB data source for the Northwind database.
If a Data Source has not been created, follow the steps in "Creating the Northwind OLE DB Data Link." - Create a new Standard EXE project in Visual Basic.
If the DataGrid control is not present in the Toolbox, right-click the Toolbox, and use the Components dialog box to load it. Also load the ADO control. - Place an instance of each control on the blank form.
- Set the ADO Control's ConnectionString property to the Northwind data source.
Click the ADO Data control to select it, and press F4 to make the Properties window appear. Click ConnectionString and then select Use Data Link File. Click Browse. Select the Northwind Data Link file and then click Open. Click OK. - Set the ADO Control's RecordSource property.
On the Properties window, click RecordSource and type a SQL statement to populate the DataGrid control. In this case, type in Select * From Products. - Set the DataGrid Control's DataSource property to the ADO Data control.
Click the DataGrid control to select it. On the Properties window, click DataSource and a drop-down list of all data controls will be presented — in this case only the ADO Data control. Click the control. - Press F5 to run the project.
A common use of the DataGrid is to show "details" supplied by one table in a database. For example, the Northwind (Nwind.mdb) database includes two tables, one named "Suppliers," and the other named "Products." In this example, we'll use the DataList control to show the company names of suppliers from the "Suppliers" table. When the user clicks on any company name, the DataList control will furnish the SupplierID for the company. Using that ID, a query can be constructed to retrieve all records in the "Products" table which have a matching SupplierID. In other words, when the user clicks on a company (in the DataList control), all of the products produced by that company will appear in the DataGrid control.
To fill a DataGrid Control with products from a particular supplier
- Ensure that an OLE DB data source for the Northwind database is present on the machine; if such a datas source has not been created, follow the steps in "Creating the Northwind OLE DB Data Link."
- Create a new standard EXE project in Visual Basic.
If the DataGrid and DataList and ADO Data controls are not present in the Toolbox, right-click the Toolbox, and click Components. In the Components dialog box double-click Microsoft DataGrid Control, Microsoft DataList Controls and Microsoft ADO Control - Place an instance of the DataGrid and DataList controls on the blank form.
Place the DataList control in the top left corner of the form, and place the DataGrid control somewhat below it. - Place two instances of the ADO Data control on the form.
Select the first ADO Data control and press F4 to display its Properties page. Set the Name property of the control to adoSuppliers. Select the second ADO Data control and set its Name property to adoProducts. Place the first control directly underneath the DataList Control, and the second directly below the DataGrid control.. - Set the ConnectionString property of the two ADO Data controls to the Northwind OLE DB data source.
Select the control named adoSuppliers and set the ConnectionString property to the Northwind OLE DB data source (Northwind.udl). Select the control named adoProducts and repeat the operation. - ADO Data controls.
Select adoSuppliers and click RecordSource on the Properties page. Type Select * From Suppliers. This query instructs the ADO Data control to return all records in the Suppliers table. Select adoProducts, click RecordSource, and type Select * From Products. This query returns all the records from the Products table. - Set the RowSource property of the DataList Control to adoSuppliers.
The RowSource property determines which data source supplies the data for the ListField property. - Set the ListField property of the DataList control to CompanyName.
The ListField property is set to the name of a field in the table named Suppliers. At run-time, the DataList control displays the value of the field specified in this property. In this example, the property will display the name of a company found in the Suppliers table. - Set the BoundColumn property of the DataList control to SupplierID.
The BoundColumn property is set to a second field in the Suppliers table. In this case, the property is set to the SupplierID field. When the DataList control is clicked, the BoundText property returns the value of the SupplierID field associated with the company displayed in the DataList control. This value will be used in a query of the Products table, which provides data for the DataGrid control. - Set the DataSource property of the DataGrid control to adoProducts.
The DataSource property specifies the data source for the control. In this case, the property is set to the ADO Data control named adoProducts, which returns all of the records in the Products table. - In the code module for the form, add the following:
Private Sub Datalist1_Click()
' Declare a string variable that will contain a new query. The
' new query uses the BoundText property of the DataList control
' to supply a SupplierID value. The new query simply asks for
' all products with the same SupplierID. This query is assigned
' to the RecordSource property of the ADO Data control named
' adoProducts. After refreshing the control, the DataGrid is
' updated with the new recordset of all products that are
' supplied by the same company. Dim strQuery As String
strQuery = "Select * FROM Products WHERE SupplierID = " & _
Datalist1.BoundText
With adoProducts
.RecordSource = strQuery
.Refresh
End With
With DataGrid1
.ClearFields
.ReBind
End With
End Sub
- Run the project.
Click any company name in the DataList control, and the DataGrid control is automatically updated with all products supplied by the company.
The DataGrid control has several methods and menu commands that affect both the layout of the grid, and how the control is bound to data. These methods and commands include:
- Clear Fields menu command (right-click the control, and click Clear Fields)
- ClearFields method
- HoldFields method
- Rebind method
- Retrieve Structure menu command (right-click the controls, and click Retrieve Structure)
One feature of the DataGrid is its design-time features. You can configure a data source, such as the ADO Data control, connect it to the DataGrid control, invoke the Retrieve Structure menu command, and customize the layout of the grid.
- Place a DataGrid control and an ADO Data control on a form.
- Configure the ADO Data control to connect to a database.
- Set the DataGrid control's DataSource property to the ADO Data Control.
- Right-click the DataGrid control and click Retrieve fields.
The grid is now configured with a default layout: one column per field in the recordset, and each column set to the same width. - Right-click the DataGrid control and click Edit. This puts the control in edit mode.
- Using the mouse, edit the column layout of the grid. Decrease or expand width, or hide columns altogether.
- Alternatively, right-click the control and click Properties. Use the dialog box to customize the layout of the grid.
Many applications must present information about disk drives, directories, and files. To allow users of your applications to explore the file system, Visual Basic provides two alternatives. You can use the standard dialog boxes provided by the common dialog control, or you can build custom dialogs using your own combinations of three specialized controls: the drive list box, the directory list box, and the file list box.
You can use the file-system controls to allow users to investigate and choose among available disk files in your applications. Consider using the common dialog control if you just need a standard File Open or Save dialog box.
Examining the File System
Each of the file-system controls has been carefully designed to combine flexible and sophisticated file-system inspection capabilities with easy programming. Each control performs its file-data retrieval tasks automatically, but you can write code both to customize their appearance and to specify which information they display.
The file-system controls
You can use file-system controls singly or in combination. With combinations, you can write code in each control's event procedures to determine how they interact. Or you can let them act independently.
The file-system controls used together
File-system controls obtain all their information from the operating system automatically; you can access this information or determine what is displayed by each control through its properties. For example, the contents of the current working directory is displayed by default (that is, the directory from which the application was launched, or what became the current directory as the result of a ChDir statement).
Your application can also display a list of the files with names matching a pattern, such as *.frm. Simply draw a file list box on the form and set its Pattern property to *.frm. You can specify the Pattern property at run time with the following code:
File1.Pattern = "*.FRM"
The file-system controls give you the flexibility that is not available with the common dialog control. You can mix and match them in a variety of ways, and you control their appearance and how they interact.
If your goal is simply to allow users to open and save files, a common dialog control provides a ready-to-run set of dialog boxes for these and other operations. These are the same dialog boxes used by many other Microsoft Windows – based applications, so they provide a standardized look-and-feel. They also recognize network drives when they're available.
The Drive List Box
The drive list box is a drop-down list box. By default, the current drive is displayed on the user's system. When this control has the focus, the user can type in any valid drive designation or click the arrow at the right of the drive list box. When the user clicks the arrow, the list box drops down to list all valid drives. If the user selects a new drive from the list, that drive appears at the top of the list box.
You can use code to examine the Drive property of the drive list box to determine which drive is currently selected. Your application can also specify which drive appears at the top of the list box with this simple assignment:
Drive1.Drive = "c:\"
The drive list box displays valid available drives. Choosing a drive from the list box doesn't automatically change the current working drive; however, you can use the Drive property to change drives at the operating system level by specifying it as an argument to the ChDrive statement:
ChDrive Drive1.Drive
The Directory List Box The directory list box displays the directory structure of the current drive on the user's system, beginning with the top-level directory. Initially, the name of the current directory appears highlighted and indented from directories above it in the hierarchy, back to the root. Subdirectories are indented beneath the current directory in the directory list box. As a user moves up or down the list, each of the items is highlighted in turn.
Identifying Individual Directories Each directory in the box has an integer identifier associated with it that allows you to identify individual directories. This capability is not provided by the common dialog control. The directory specified by the Path property (Dir1.Path) always has the ListIndex value of – 1. The directory immediately above it has the ListIndex value of – 2, the one above that of – 3, and so on up to the root. The first subdirectory of Dir1.Path has the ListIndex 0. If there are multiple directories at the first subdirectory level, the next has the ListIndex value of 1, then 2, and so on.
A directory structure displayed in the directory list box
Setting the Current Directory
Use the Path property of the directory list box to set or return the current directory in the box (ListIndex = – 1). For example, if you assign "c:\payroll" to Drive1.Path in Figure 7.18, the \Payroll directory becomes selected as the current working directory.
Similarly, you can assign the Drive property of the drive list box to the Path property of the directory list box:
Dir1.Path = Drive1.Drive
When this assignment is executed, the directory list box displays all the available directories and subdirectories on that drive. By default, the directory list box also displays all directories above, and any subdirectories immediately below, the current directory of a drive assigned to the Dir1.Path property. The directory list box doesn't set the current directory at the operating system level; it merely highlights the directory and gives it the ListIndex value of – 1.
To set the current working directory, use the ChDir statement. For example, the following statement changes the current directory to the one displayed in the directory list box:
ChDir Dir1.Path
In an application that uses file controls, you can set the current directory to the directory where the application's executable (.exe) file is located with the Application object:
ChDrive App.Path ' Set the drive.
ChDir App.Path ' Set the directory.
Note The Path property is available only at run time, not at design time.
Clicking a Directory Item
When a user clicks an item in a directory list box, that item is highlighted. When an item is double-clicked, it is assigned to the Path property, its ListIndex property gets the value –1, and the directory list box is redrawn to show its immediate subdirectories.
Finding a Directory's Relative Position
The ListCount property returns the number of directories below the currently expanded directory, not the total number of items in the directory list box. Because the ListIndex value of the currently expanded directory is always –1, you can write code to determine how far down from the root the currently expanded directory is in the hierarchy. For example:
' Initialize for currently expanded directory.
GoHigher = 0
' Dir1.List(x) returns empty string if the directory
' doesn't exist.
Do Until Dir1.List(GoHigher) = ""
GoHigher = GoHigher - 1
Loop
' Convert to positive number, if desired.
LevelsAbove = Abs(GoHigher)
The File List Box
The file list box displays files contained in the directory specified by the Path property at run time. You can display all the files in the current directory on the current drive using the following statement:
File1.Path = Dir1.Path
You can then display a subset of these files by setting the Pattern property — for example, *.frm displays only files with that extension. The Pattern property can also accept a list delimited by semicolons. For example, a line with the following code displays all files with the extensions .frm and .bas:
File1.Pattern = "*.frm; *.bas"
Visual Basic supports the ? wildcard character. For instance, ???.txt displays files that have base names of only three characters with the extension .txt.
Working with File Attributes The attributes of the currently selected file (Archive, Normal, System, Hidden, and ReadOnly) are also available through file list box properties. You use these properties to specify which kinds of files to display in a file list box. The default value for the System and Hidden attributes is False. The default value for the Normal, Archive, and ReadOnly attributes is True.
To display only read-only files in the list box, for example, simply set the ReadOnly property to True and the other attribute properties to False:
File1.ReadOnly = True
File1.Archive = False
File1.Normal = False
File1.System = False
File1.Hidden = False
When Normal = True, those files without the System or Hidden attribute are displayed. When Normal = False, you can still display files with ReadOnly and/or Archive attributes by setting these attributes to True.
Note You cannot use the attribute properties to set file attributes. To set file attributes, use the SetAttr statement.
By default, you can highlight only a single selection in a file list box. To select multiple files, use the MultiSelect property.
Using File-System Controls Together
If you use a combination of file-system controls, you can synchronize the information they display. For example, if you have a drive list box, a directory list box, and a file list box with the default names Drive1, Dir1, and File1, the sequence of events might work like this:
- The user selects a drive in the Drive1 list box.
- A Drive1_Change event is generated, and the display in Drive1 is updated to reflect the new drive.
- Code in the Drive1_Change event procedure assigns the new selection (the Drive1.Drive property) to the Path property of the Dir1 list box with the following statements:
Private Sub Drive1_Change ()
Dir1.Path = Drive1.Drive
End Sub - The assignment to the Path property generates a Dir1_Change event and updates the display in Dir1 to reflect the current directory of the new drive.
- Code in the Dir1_Change event procedure assigns the new path (the Dir1.Path property) to the File1.Path property of the File1 list box:
Private Sub Dir1_Change ()
File1.Path = Dir1.Path
End Sub - The assignment to the File1.Path property causes the display in the File1 list box to reflect the Dir1 path specification.
- The event procedures you use and the properties you change depend on the way your application uses the combination of file-system controls. The code in "File-System Controls Scenario: A File Seeker Application" illustrates the synchronization of controls described here.
Because users often want to find a file or group of files available to an application quickly, many applications provide capabilities for investigating the file system. The Winseek.vbp sample application helps the user browse drives and directories, and displays any category of files
File-system controls in the WinSeek application
The following table summarizes the controls in Seek.frm from the WinSeek application.
Control Property Setting Drive list box Name drvList Directory list box Name dirList File list box Name
Pattern filList
*.* First command button Name
Caption
Default cmdSearch
&Search
True Second command button Name
Caption cmdExit
E&xit List box Name lstFoundFiles
Note The file-system controls do not have caption properties, although you can label them and give them access keys. For more information on using labels this way, see "Using the Label Control" later in this chapter. The Drive List Box's Change Event
When the user clicks an item in the drive list box, its Change event is generated. The drvList_Change event procedure is invoked, and the following code is run:
Private Sub drvList_Change ()
On Error GoTo DriveHandler
' If new drive was selected, the Dir1 box
' updates its display.
dirList.Path = drvList.Drive
Exit Sub
' If there is an error, reset drvList.Drive with the
' drive from dirList.Path.
DriveHandler:
drvList.Drive = dirList.Path
Exit Sub
End Sub
Notice that the Change event in a drive list box occurs when a new drive is selected, either with a single mouse click or when the user moves the selection (for example, with an arrow key). The error handler is triggered by actions such as attempting to access a floppy disk drive while the drive door is open or selecting a network drive that has been inadvertently disconnected. Because the error prevents the original assignment, dirList.Path still contains the previous valid drive. Reassigning dirList.Path to drvList.Drive corrects this error.
For More Information See "Debugging Your Code and Handling Errors" for more information.
The Directory List Box's Change Event
If the user double-clicks an item in the directory list box, or if the Path property of dirList is changed in code (as in the drvList_Change procedure), the dirList_Change event is initiated. The following code responds to that event:
Private Sub dirList_Change ()
' Update file list box to synchronize with the
' directory list box.
filList.Path = dirList.Path
End Sub
This event procedure assigns the Path property of the dirList box to the Path property of the filList box. This causes a PathChange event in the filList list box, which is redrawn; you don't need to add code to the filList_PathChange procedure, because in this application, the event chain ends in the filList list box.
The Command Button's Click Event
This event procedure determines whether the highlighted item in the dirList list box is the same as the dirList.Path. If the items are different, then dirList.Path is updated. If the items are the same, then the search is performed.
Private Sub cmdSearch_Click ()
.
.
.
' If the dirList.Path is different from the
' currently selected directory, update it;
' otherwise perform the search.
If dirList.Path <> dirList.List _
(dirList.ListIndex) Then
dirList.Path = dirList.List(dirList.ListIndex)
Exit Sub
End If
' Continue with search.
.
.
.
End Sub
Note You can enhance the WinSeek application with additional features. For example, you might want to use a file control's attribute properties. You could use check boxes to allow the user to set different combinations of file attributes so that the file list box displays files that are Hidden, System, and so on. This would restrict a search to conforming files.
Using the Frame Control Frame controls are used to provide an identifiable grouping for other controls. For example, you can use frame controls to subdivide a form functionally — to separate groups of option button controls.
The frame control
In most cases, you will use the frame control passively — to group other controls — and will have no need to respond to its events. You will, however, most likely change its Name, Caption, or Font properties.
Adding a Frame Control to a Form
When using the frame control to group other controls, first draw the frame control, and then draw the controls inside of it. This enables you to move the frame and the controls it contains together. Drawing Controls Inside the Frame
To add other controls to the frame, draw them inside the frame. If you draw a control outside the frame, or use the double-click method to add a control to a form, and then try to move it inside the frame control, the control will be on top of the frame and you'll have to move the frame and controls separately.
Controls inside a frame
Note If you have existing controls that you want to group in a frame, you can select all the controls, cut them to the clipboard, select the frame control, and then paste them into the frame control.
Selecting Multiple Controls in a Frame
To select multiple controls in a frame , hold down the CTRL key while using the mouse to draw a box around the controls. When you release the mouse, the controls inside the frame will be selected.
Selecting controls inside a frame
Using the Horizontal and Vertical Scroll Bar Controls
Scroll bars provide easy navigation through a long list of items or a large amount of information by scrolling either horizontally or vertically within an application or control. Scroll bars are a common element of the Windows and Windows NT interface.
The horizontal and vertical scroll bar controls
The horizontal and vertical scroll bar controls are not the same as the built-in scroll bars found in Windows or those that are attached to text boxes, list boxes, combo boxes, or MDI forms within Visual Basic. Those scroll bars appear automatically whenever the given application or control contains more information than can be displayed in the current window size (or, in the case of text boxes and MDI forms, when the ScrollBars property is also set to True). In previous versions of Visual Basic, scroll bars were most often used as input devices. Windows interface guidelines now suggest, however, that slider controls be used as input devices instead of scroll bar controls. A slider control (of the type found in 32-bit Windows operating systems) is included in the Professional and Enterprise versions of Visual Basic.
Scroll bar controls are still of value in Visual Basic because they provide scrolling to applications or controls that do not provide them automatically. See "Scroll Bar Controls Scenario: Creating a Scrollable Graphics Viewport" for information on using scroll bars in this manner.
How the Scroll Bar Controls Work
The scroll bar controls use the Scroll and Change events to monitor the movement of the scroll box (sometimes referred to as the thumb) along the scroll bar.
Event Description Change Occurs after the scroll box is moved Scroll Occurs as the scroll box is moved. Does not occur if the scroll arrows or scroll bar is clicked. Using the Scroll event provides access to the scroll bar value as it is being dragged. The Change event occurs after the scroll box is released or when the scroll bar or scroll arrows are clicked.
The Value Property
The Value property (which, by default, is 0) is an integer value corresponding to the position of the scroll box in the scroll bar. When the scroll box position is at the minimum value, it moves to the leftmost position (for horizontal scroll bars) or the top position (for vertical scroll bars). When the scroll box is at the maximum value, the scroll box moves to the rightmost or bottom position. Similarly, a value halfway between the bottom and top of the range places the scroll box in the middle of the scroll bar.
In addition to using mouse clicks to change the scroll bar value, a user can also drag the scroll box to any point along the bar. The resulting value depends on the position of the scroll box, but it is always within the range of the Min to Max properties set by the user.
Note Min can be larger than Max if you want your scroll bar to display information changing from a larger to a smaller value.
The LargeChange and SmallChange Properties
To specify the amount of change to report in a scroll bar, use the LargeChange property for clicking in the scroll bar, and the SmallChange property for clicking the arrows at the ends of the scroll bar. The scroll bar's Value property increases or decreases by the values set for the LargeChange and SmallChange properties. You can position the scroll box at run time by setting Value between 0 and 32,767, inclusive.
Using the Image Control
The image control is used to display graphics. Image controls can display graphics in the following formats: bitmap, icon, metafile, enhanced metafile, or as JPEG or GIF files.
In addition, image controls respond to the Click event and can be used as a substitute for command buttons, as items in a toolbar, or to create simple animations.
When to Use an Image Control Instead of a Picture Box Control
The image control uses fewer system resources and repaints faster than a picture box control, but it supports only a subset of the picture box control's properties, events, and methods. Both controls support the same picture formats. However, you can stretch pictures in an image control to fit the control's size. You cannot do this with the picture box control. Supported Graphic Formats
The image control can display picture files in any of the following standard formats.
Picture format Description Bitmap A bitmap defines an image as a pattern of dots (pixels). A bitmap has the file name extensions .gif or .dib. Bitmaps are also called "paint-type" graphics.
You can use bitmaps of various color depths, including 2, 4, 8, 16, 24, and 32-bits, but a bitmap only displays correctly if the display device supports the color depth used by the bitmap. For example, an 8-bit-per-pixel (256 color) bitmap only displays in 16 colors when shown on a 4-bit-per-pixel (16 color) device.
Icon An icon is a special kind of bitmap. Icons have a maximum size of 32 pixels by 32 pixels, but under Microsoft Windows 95 or later, icons are also found in 16 by 16 pixel size. An icon has the file name extension .ico. Cursor Cursors, like icons, are essentially bitmaps. Cursors, however, also contain a hot spot, a pixel that tracks the location of the cursor by its x and y coordinates. Cursors have the file name extension .cur. Metafile coded lines and shapes. Conventional metafiles have the file name extension .wmf. Enhanced metafiles have the file name extension .emf. Only files that are compatible with Microsoft Windows can be loaded. Metafiles are also called "draw-type" graphics. JPEG JPEG (Joint Photographic Experts Group) is a compressed bitmap format which supports 8- and 24-bit color. It is a popular file format on the Internet. GIF GIF (Graphic Interchange Format) is a compressed bitmap format originally developed by CompuServe. It supports up to 256 colors and is a popular file format on the Internet.
Loading a Graphic Into the Image Control
Pictures can be loaded into the image control at design time by selecting the Picture property from the control's Properties window, or at run time by using the Picture property and the LoadPicture function.
Set Image1.Picture = LoadPicture("c:\Windows\Winlogo.cur", vbLPLarge, vbLPColor)
When a picture is loaded into the image control, the control automatically resizes to fit the picture — regardless of how small or large the image control was drawn on the form.
You may want to use icon (.ico) and cursor (.cur) files containing separate images at different sizes and color depths to support a range of display devices. The LoadPicture function's settings allow you to select images with specific color depths and sizes from an .ico or .cur file. In cases where an exact match to the requested settings isn't available, LoadPicture loads the image with the closest match available.
To clear the graphic from the image control, use the LoadPicture function without specifying a file name. For example:
Set Image1.Picture = LoadPicture
This will clear the image control even if a graphic was loaded into the Picture property at design time.
Using the Clipboard
You can also add a graphic to an image control at design time by pasting it from another application. For example, you may want to add a bitmap image that was created in Windows Paint. Simply copy the image to the Clipboard, select the image control, and either use the keyboard shortcut CTRL+V or the Paste command from the Edit menu.
The Stretch Property
The Stretch property determines whether the picture is stretched when the image control is resized at design time. If set to True, the picture loaded into the image control via the Picture property is stretched. Stretching a picture (especially a bitmap format) can produce a loss in image quality, as shown in Figure 7.28. Metafiles, which are "draw-type" graphics, are better suited for stretching. Stretching a bitmap image
Using the Label Control
Label controls are used to display text and cannot be edited by the user. They are used to identify objects on a form — provide a description of what a certain control will do if clicked, for example — or at run time, they can display information in response to an event or process in your application.
Labels are used in many instances, for many different purposes. Most commonly, they are used to label controls that don't have their own Caption properties. For example, you can use the label control to add descriptive labels to text boxes, list boxes, combo boxes and so on. They can also be used to add descriptive text to a form, for example, to provide the user with Help information.
You can also write code that changes the text displayed by a label control in response to events at run time. For example, if your application takes a few minutes to process a change, you can display a processing-status message in a label.
Because the label control cannot receive the focus, it can also be used to create access keys for other controls.
Setting the Label's Caption
To change the text displayed in the label control, use the Caption property. At design time, you can set this property by selecting it from the control's Properties window.
You can set the length of the Caption property up to a maximum of 1024 bytes.
Aligning Text The Alignment property allows you to set the alignment of the text within the label control to either Left Justify (0, the default) , Center (1), or Right Justify (2).
The AutoSize and WordWrap Properties
By default, when text entered into the Caption property exceeds the width of the control, the text wraps to the next line and is clipped if it exceeds the control's height.
To allow the control to automatically adjust to the size of its contents, set the AutoSize property to True. The control will expand horizontally to fit the entire contents of the Caption property. To allow the contents to wrap down and expand vertically, set the WordWrap property to True.
Using Labels to Create Access Keys Set the UseMnemonic property to True if you want to define a character in the Caption property of the label as an access key. When you define an access key in a label control, the user can press and hold down ALT+ the character you designate to move the focus to the next control in the tab order.
You can also create access keys for any other controls that have a Caption property by adding an ampersand (&) before the letter you want to use as the access key. To assign an access key to controls that don't have captions, use a label with the control. Because labels can't receive focus, focus automatically moves to the next control in the tab order. Use this technique to assign access keys to text boxes, picture boxes, combo boxes, list boxes, drive list boxes, directory list boxes, grids, and images.
To assign an access key to a control with a label
- Draw the label first, and then draw the control.
-or-
Draw the controls in any order and set the TabIndex property of the label to one less than the control. - Use an ampersand in the label's Caption property to assign the access key for the label.
The line control is used to create simple line segments on a form, a frame, or in a picture box.
The line control
You can control the position, length, color, and style of line controls to customize the look of applications. Figure 7.31 shows a line control used to graphically separate the label containing the text "Company Name" from the rest of the form.
A line control on a form
The line control has limited functionality and is intended for simple uses — display and printing. Line segments cannot be joined to form other shapes, for instance. For more advanced uses you need to use the line method.
Setting Border Style and Color
You set the color and style of a line segment by using the BorderStyle and BorderColor properties.
The BorderStyle property provides you with six line styles:
- Transparent
- Solid
- Dash
- Dot
- Dash-Dot
- Dash-Dot-Dot
- Inside Solid
The BackColor property is used to specify the color of the line.
At design time, you can set the line color by choosing the BorderColor property from the Properties window of the line control and then selecting from the available palette or system colors.
To set colors at run time, use the Visual Basic color constants (vbGreen, for example) or the system color constants (vbWindowBackground, for example) or the RGB function to specify border colors.
Note When BorderStyle is set to 0 (Transparent), the BorderColor property is ignored.
Moving and Sizing a Line Segment
You can move or resize the line control at run time by altering its X1, X2, Y1, and Y2 properties. The X1 and Y1 properties set the horizontal and vertical positions of the left end of the line segment. The X2 and Y2 properties set the horizontal and vertical positions of the right end of the line segment. You can't move a line segment using the Move method.
Drawing Lines on a Form You can use the line control to draw simple lines on forms.
To draw a line on a form
- In the toolbox, select the line control.
When the pointer moves onto the form, it changes to a cross hair. - Click the form where you want the line to begin and hold down the mouse button.
- Drag the cross hair to where you want the line to end and release the mouse button.
- From the Properties window, select the BorderStyle property if you want to change the appearance of the line.
- In the Settings box, select the style you want.
List boxes present a list of choices to the user. By default, the choices are displayed vertically in a single column, although you can set up multiple columns as well. If the number of items exceeds what can be displayed in the list box, scroll bars automatically appear on the control. The user can then scroll up and down, or left to right through the list.
The Click and Double-Click Events
A recommended practice for list box events, especially when the list box appears as part of a dialog box, is to add a command button to use with the list box. The Click event procedure for this button should make use of the list-box selection, carrying out whatever action is appropriate for your application.
Double-clicking an item in the list should have the same effect as selecting the item and then clicking the command button. To do this, have the DblClick procedure for the list box call the Click procedure for the command button:
Private Sub List1_DblClick ()
Command1_Click
End Sub
Or, set the value of the command button's Value property to True, which will automatically invoke the event procedure:
Private Sub List1_DblClick ()
Command1.Value = True
End Sub
This provides mouse users with a shortcut, yet does not prevent keyboard users from performing the same action. Note that there is no keyboard equivalent for the DblClick event.
Adding Items to a List
To add items to a list box, use the AddItem method, which has the following syntax:
box.AddItem item[, index]
Argument Description box Name of the list box item String expression to add to the list. If item is a literal constant, enclose it in quotation marks. index Specifies where the new item is to be inserted in the list. An index of 0 represents the first position. If index is omitted, the item is inserted at the end (or in the proper sorted order).
While list items are commonly added in the Form_Load event procedure, you can use the AddItem method at any time. This gives you the ability to add items to the list dynamically (in response to user actions).
The following code places "Germany," "India," "France," and "USA" into a list box named List1:
Private Sub Form_Load ()
List1.AddItem "Germany"
List1.AddItem "India"
List1.AddItem "France"
List1.AddItem "USA"
End Sub
Whenever the form is loaded at run time, the list appears as shown in Figure
Adding an Item at a Specified Position To add an item to a list at a specific position, specify an index value for the new item. For example, the next line of code inserts "Japan" into the first position, adjusting the position of the other items downward:
List1.AddItem "Japan", 0
Notice that it is 0, not 1, that specifies the first item in a list
Adding an item to a list
Sorting a List
You can specify that items be added to a list in alphabetical order by setting the Sorted property to True and omitting the index. The sort is not case-sensitive; thus, the words "japan" and "Japan" are treated the same.
When the Sorted property is set to True, using the AddItem method with the index argument can lead to unpredictable, unsorted results.
Removing Items from a List You can use the RemoveItem method to delete items from a list box. RemoveItem has one argument, index, which specifies the item to remove:
box.RemoveItem index
The box and index arguments are the same as for AddItem.
For example, to remove the first entry in a list, you would add the following line of code:
List1.RemoveItem 0
To remove all list entries in bound or standard versions of the list and combo boxes, use the Clear method:
List1.Clear
Getting List Contents with the Text Property
Usually, the easiest way to get the value of the currently selected item is to use the Text property. The Text property always corresponds to a list item a user selects at run time.
For example, the following code displays information about the population of Canada if a user selects "Canada" from a list box:
Private Sub List1_Click ()
If List1.Text = "Canada" Then
Text1.Text = "Canada has 24 million people."
End If
End Sub
The Text property contains the currently selected item in the List1 list box. The code checks to see if "Canada" has been selected and, if so, displays the information in the Text box.
Accessing List Items with the List Property
The List property provides access to all items in the list. This property contains an array in which each item in the list is an element of the array. Each item is represented in string form. To refer to an item in the list, use this syntax:
box.List(index)
The box argument is a reference to a list box, and index is the position of the item. The top item has an index of 0, the next has an index of 1, and so on. For example, the following statement displays the third item (index = 2) in a list in a text box:
Text1.Text = List1.List(2)
Determining Position with the ListIndex Property If you want to know the position of the selected item in a list, use the ListIndex property. This property sets or returns the index of the currently selected item in the control and is available only at run time. Setting the ListIndex property for a list box also generates a Click event for the control.
The value of this property is 0 if the first (top) item is selected, 1 if the next item down is selected, and so on. ListIndex is – 1 if no item is selected.
Note The NewIndex property allows you to keep track of the index of the last item added to the list. This can be useful when inserting an item into a sorted list.
Returning the Number of Items with the ListCount Property
To return the number of items in a list box, use the ListCount property. For example, the following statement uses the ListCount property to determine the number of entries in a list box:
Text1.Text = "You have " & List1.ListCount & " _
entries listed"
Creating Multiple-Column and Multiple-Selection List Boxes
The Columns property allows you to specify the number of columns in a list box. This property can have the following values:
Value Description 0 Single-column list box with vertical scrolling 1 Single-column list box with horizontal scrolling. >1 Multiple-column list box with horizontal scrolling Visual Basic takes care of wrapping list items to the next line and adding a horizontal scroll bar to the list if needed; if the list fills a single column, no scroll bar is added. Wrapping to the next column also occurs automatically as needed. Note that if a list box entry is wider than the width of a column, the text is truncated.
You can allow users to select multiple items from a list. Multiple selection in standard list boxes is handled by setting the MultiSelect property, which can have the following values
Value Type of selection Description 0 None Standard list box. 1 Simple multiple selection A click or the SPACEBAR selects or deselects additional items in the list. 2 Extended multiple
selection The SHIFT+ click or SHIFT+ an arrow key extends the selection to include all the items between the current and previous selections. CTRL+ click selects or deselects an item in the list. List Box Control Scenario 1: Adding and Deleting Items
This example shows how you can use the AddItem, RemoveItem, and Clear methods with the ListIndex and ListCount properties to add and remove list entries at run time. The example in Figure 7.36 lets a user type a client's name in a text box, which can be added to the list box if the Add button is clicked. A user can remove a current list item by selecting the item and choosing the Remove button, or by choosing Clear to clear all list entries.
Figure A list box using the AddItem, RemoveItem, and Clear methods
The number of clients in the list box is displayed in a label that looks like a text box (BorderStyle is set to 1-Fixed Single). This label is updated every time a client name is added or removed. Because the Sorted property for the list box is set to True, items are added to the list box in alphabetical order.
Create a form with a text box, a list box, three labels, and four command buttons. The following table lists the property settings for the objects in the application.
Object Property Setting Top text box Name Text txtName
(Empty) Top label Name Caption lblName
&Name to add List box Name Sorted lstClient
True Bottom label Name
Caption lblClients
# Clients Number of clients label
(looks like a text box) Name
Caption
BorderStyle lblDisplay
(Empty)
1-Fixed Single First command button Name
Caption cmdAdd
&Add Second command button Name
Caption cmdRemove
&Remove Third command button Name
Caption cmdClear
C&lear Fourth command button Name
Caption cmdClose
&Close Events in the List Box Application
Add this code to the cmdAdd_Click event procedure:
Private Sub cmdAdd_Click ()
lstClient.AddItem txtName.Text ' Add to list.
txtName.Text = "" ' Clear text box.
txtName.SetFocus
' Display number.
lblDisplay.Caption = lstClient.ListCount
End Sub
Add this code to the cmdRemove_Click event procedure:
Private Sub cmdRemove_Click ()
Dim Ind As Integer
Ind = lstClient.ListIndex ' Get index.
' Make sure list item is selected.
If Ind >= 0 Then
' Remove it from list box.
lstClient.RemoveItem Ind
' Display number.
lblDisplay.Caption = lstClient.ListCount
Else
Beep
End If
' Disable button if no entries in list.
cmdRemove.Enabled = (lstClient.ListIndex <> -1)
End Sub
Add this code to the cmdClear_Click event procedure:
Private Sub cmdClear_Click ()
' Empty list box.
lstClient.Clear
' Disable Remove button.
cmdRemove.Enabled = False
' Display number.
lblDisplay.Caption = lstClient.ListCount
End Sub
Add this code to the cmdClose_Click event procedure:
Private Sub cmdClose_Click ()
Unload Me
End Sub
Add this code to the lstClient_Click event procedure: Private Sub lstClient_Click ()
cmdRemove.Enabled = lstClient.ListIndex <> -1
End Sub
Add this code to the txtName_Change event procedure:
Private Sub txtName_Change ()
' Enable the Add button if at least one character
' in the name.
cmdAdd.Enabled = (Len(txtName.Text) > 0)
End Sub
List Box Control Scenario 2: Creating Multiple-Column List Boxes
To create a multiple-column, multiple-selection list box, you need to set both the Columns and the MultiSelect properties of a list box. In the following example, these properties are used to create such a list box.
You'll notice that when you run the application, the list box contains two columns, as shown in Figure
If you draw the list box large enough to hold all the items in one column, the second column will be empty; the other items will wrap, and horizontal scroll bars will appear automatically only if the list box is not long enough. Try resizing the top list box and adding additional list items to see how Visual Basic automatically handles multiple columns. The example uses the Selected property — a Boolean array containing the selection status of a list box — to determine which items are selected. Each entry in the array corresponds to a list item and is set to True if the item is selected, or False if it is not selected. After the user selects items from the list, each array entry is checked to see if it is set (True). If so, the entry is added to the second list, a normal single-column list box, using the AddItem method.
Set the properties for the example as indicated in the following table.
Object
Property Setting Form Caption Multiple-Column List Box Top list box Name
Columns
MultiSelect lstTop
2
2-Extended Bottom list box Name lstBottom First command button Name
Caption cmdTransfer
&Transfer Second command button Name
Caption cmdClear
C&lear Third command button Name
Caption cmdClose
&Close The MultiSelect property allows you to select a range of values in a list box. If you click the first list item, and then press SHIFT and click the last item in the range (or use the SHIFT+ DOWN ARROW keys), all the items in the range are selected.
Events in the Multiple-Column List Box Application
Add code to the Form_Load procedure to initialize the top list, 1stTop:
Private Sub Form_Load ()
lstTop.AddItem "Paris"
lstTop.AddItem "New Orleans"
lstTop.AddItem "San Francisco"
lstTop.AddItem "Chicago"
lstTop.AddItem "Seattle"
lstTop.AddItem "Toronto"
lstTop.AddItem "New York"
lstTop.AddItem "Tbilisi"
lstTop.AddItem "Moscow"
lstTop.AddItem "Portland"
' Select a couple of items.
1stTop.Selected(0) = True
1stTop.Selected(1) = True
End Sub
Note You can add items to list boxes without repeatedly using the AddItem method by typing items in the List property of the Properties window. After entering each item, press CTRL+ENTER to go to the next line. This allows you to type multiple entries in a multiple-column list box.
Add the following code to the 1stTop_DblClick event procedure:
Private Sub 1stTop_DblClick ()
cmdTransfer.Value = True ' Press transfer button.
End Sub
Add the following code to the Click event procedure for the Transfer command button:
Private Sub cmdTransfer_Click ()
For n = 0 To (lstTop.ListCount - 1)
' If selected, add to list.
If lstTop.Selected(n) = True Then
lstBottom.AddItem lstTop.List(n)
End If
Next
cmdClear.Enabled = True
End Sub
Notice how the array index values start from 0 and go to ListCount -1.
Add the following code to the Click event procedure for the Clear command button:
Private Sub cmdClear_Click ()
lstBottom.Clear
cmdClear.Enabled = False
End Sub
Add the following code to the Click event procedure for the Close command button.
Private Sub cmdClose_Click ()
Unload Me
End Sub
Using the Option Button Control
Option button controls are used to display options, usually in option button groups, from which the user can choose one.
Figure:The option button control
While option button controls and check box controls may appear to function similarly, there is an important difference: when a user selects an option button, the other option button controls in the same group are automatically unavailable. In contrast, any number of check box controls can be selected.
Creating Option Button Groups
You group option button controls by drawing them inside a container such as a frame control, a picture box control, or a form. At run time, the user may select a single option button from each distinct group of option buttons. For example, if you add option buttons to a form and option buttons to a frame control on the form, you have created two distinct groups of option buttons. Figure:Creating option button groups
All option buttons that are added directly to the form become one group. To add additional groups, you need to place them inside frame or picture box controls.
To group option button controls in a frame or picture box, draw the frame or picture box first, and then draw the option button controls inside. At design time, option buttons contained within a frame or picture box control may be selected and moved as a single unit.
To select multiple controls contained inside a frame control, a picture box control, or a form, hold down the CTRL key while using the mouse to draw a box around the controls.
Selecting an Option Button at Run Time
An option button may be selected at run time in several ways: clicking it with the mouse, using the TAB key to shift the focus to the control, using the TAB key to select a group of option button controls and then using the arrow keys to select one within the group, creating a shortcut key in the option button's caption, or, by setting its Value property to True in code. The Click Event
When an option button is selected, its Click event is triggered. Depending upon the functionality of your application, you may or may not need to respond to this event. Responding to this event is useful when you want to update a label control's caption to provide the user with information about the option that has been selected, for example. The Value Property
The Value property of the option button control indicates whether the option button is selected. When selected, the value is changed to True. You can select an option button in code by setting its Value property. For example:
optPentium.Value = True
To make an option button the default within an option button group, set the Value property at design time using the Properties window or at run time in code, as shown above.
When you present the user with a dialog box containing option buttons, you are asking them to select options that will determine what your application will do next. You can use the Value property of each of the option button controls to determine which option or options were selected and then respond accordingly.
Creating Keyboard Shortcuts You can use the Caption property to create access key shortcuts for your option buttons by adding an ampersand (&) before the letter you want to use as the access key. For example, to create an access key for the option button caption "Pentium" you add an ampersand before the letter "P": "&Pentium". At run time, the letter "P" will be underlined and the user can select the command button by simultaneously pressing ALT+P.
Note To include an ampersand in a caption without creating an access key, include two ampersands (&&). A single ampersand is displayed in the caption and no characters are underlined.
Disabling an Option Button To disable an option button, set its Enabled property to False. At run time, the option button will appear dimmed, meaning that it is unavailable.
Using the Picture Box Control
The picture box control is used to display graphics, to act as a container for other controls, and to display output from graphics methods or text using the Print method.
Figure:The picture box control
The picture box control is similar to the image control in that each can be used to display graphics in your application — each supports the same graphic formats. The picture box control, however, contains functionality which the image control does not, for example: the ability to act as a container for other controls and support for graphics methods.
Loading a Graphic Into the Picture Box Control
Pictures can be loaded into the picture box control at design time by selecting the Picture property from the control's Properties window, or at run time by using the Picture property and the LoadPicture function.
Set Picture1.Picture = _
LoadPicture("c:\Windows\Winlogo.cur", vbLPLarge, vbLPColor)
You may want to use icon (.ico) and cursor (.cur) files containing separate images at different sizes and color depths to support a range of display devices. The LoadPicture function's settings allow you to select images with specific color depths and sizes from an .ico or .cur file. In cases where an exact match to the requested settings isn't available, LoadPicture loads the image with the closest match available.
To clear the graphic from the picture box control, use the LoadPicture function without specifying a file name. For example:
Set Picture1.Picture = LoadPicture
This will clear the picture box control even if a graphic was loaded into the Picture property at design time.
Using the Clipboard
You can also add a graphic to a picture box control at design time by pasting it from another application. For example, you may want to add a bitmap image that was created in Windows Paint. Simply copy the image to the clipboard, select the picture box control, and either use the keyboard shortcut CTRL+V or the Paste command from the Edit menu. Sizing a Picture
By default, graphics are loaded into a picture box at their original size, meaning that if the graphic is larger than the control, the image will be clipped — the picture box control does not provide scroll bars. To make a picture box control automatically resize to display an entire graphic, set its AutoSize property to True. The control will then size to the graphic — growing or shrinking.
Unlike the image control, the picture box control cannot stretch the image to fit the size of the control.
Using the Picture Box Control as a Container
You can use the picture box control as a container for other controls. For example, since the picture box can be placed inside the internal area of a MDI form, it is often used to manually create a toolbar or status bar.
Graphics Methods
Picture boxes, like forms, can be used to receive the output of graphics methods such as Circle, Line, and Point. For example, you can use the Circle method to draw a circle in a picture box by setting the control's AutoRedraw property to True.
Picture1.AutoRedraw = True
Picture1.Circle (1200, 1000), 750
Setting AutoRedraw to True allows the output from these methods to be drawn to the control and automatically redrawn when the picture box control is resized or redisplayed after being hidden by another object.
Using the Print Method
You can use the picture box control to output text by using the Print method and setting the AutoRedraw property to True. For example:
Picture1.Print "A text string"
When using the Print method you can also modify the font style and size or use the CurrentX, CurrentY, Height, and Width properties to align text within a picture box.
Using the RemoteData Control
The Remote Data control implements data access by using the Microsoft Remote Data Objects. This technology gives you seamless access to many standard database formats and allows you to create data-aware applications without writing any code. The Remote Data control is suited to larger client-server databases, including Open Database Connectivity (ODBC) databases such as Microsoft SQL Server and Oracle.
It should be noted that the RemoteData control does not work with Image data types.
Note Both the Remote Data control and Data control are included with Visual Basic for backward compatibility. However, because of the flexibility of ActiveX Data Objects (ADO), it's recommended that you create new database applications using the ADO Data control. For more details, see "Using the ADO Data Control.".
The Data control, Remote Data control, and the ADO Data control are all conceptually similar: all three are "data controls" that connect a data source to a data-bound control. All three also share the same appearance — a set of four buttons that allow the user to go immediately to the beginning of the recordset, end of the recordset, or scroll backwards and forwards through the recordset.
Creating a Simple Database Application with the Remote Data Control
The following procedures create a simple database application using the Data control
To create a simple database application with the Remote Data Control
- Draw a Remote Data control on the form. (The icon's tooltip control is "MSRDC.")
If the Remote Data control is not in the Toolbox, press CTRL+T to display the Components dialog box. In the Components dialog box, click Microsoft RemoteData Control. Click OK to add it to the Toolbox. - Click the Remote Data control to select it, and press F4 to display the Properties window.
- In the Properties window, set the DataSourceName property to the DSN of the database you want to connect to.
- On the Properties window, click the SQL property and type in a SQL statement, for example:
SELECT * FROM Products WHERE AuthorID = 72 You should always include a WHERE clause when accessing a table. Failing to do so will lock the entire table, which would be a major hindrance to other users.
- Draw a TextBox control on the form.
- Click the TextBox control to select it, and on the Properties window set the DataSource property to the Remote Data control.
- On the Properties window, set the DataField property to the name of the field in the database you want to view or modify.
- Repeat steps 5, 6, and 7 for each additional field you want to access.
- Press F5 to run the application.
The following data-related properties can be set at design time. The list suggests a logical order for setting the properties:
- Connect — The Connect property is a string that can contain all the settings necessary to make a connection. The parameters passed in the string are driver-dependent. For example, ODBC drivers allow the string to contain driver, database, user name, and password.
- UserName — Identifies a user to a protected database. The user must also supply a valid password that the database management system recognizes. The user name can also be contained in the Connect property value, making it redundant here.
- Password — Along with UserName, the Password allows the user to access protected data. The password can also be contained in the Connect property value, making it redundant here.
- SQL — The SQL property contains the SQL statement used to retrieve a resultset. The size of the resultset may determine whether you want to use a client-side or server-side cursor. For example, a small resultset can be managed on a client-side cursor.
- RowSetSize — Sets the number of rows returned in a resultset, if the cursor is a keyset. You can fine-tune this number to the computer's resources (memory) for performance gains.
- ReadOnly — Specifies if the data will be written to. If writing data is not needed, setting this property to True can give you a performance gain.
- CursorDriver — Determines the location and type of driver. The setting of this property affects how you set other properties. For example, selecting ODBC client-side cursors may give you performance increases as long as the resultset is small.
- LockType — The LockType determines how the data is locked when others attempt to change the data. If you do not expect others to change the data (while you are looking at it) you can set the LockType to rdConcurRowVer — "optimistic" — leaving the data free to be viewed and changed by others. If you set it to rdConcurLock — "pessimistic" — others cannot access the data while you are accessing it.
- BOFAction, EOFAction — These two properties determine what will happen when the control is at the beginning and end of the cursor. Choices include staying at the beginning or end, moving to the first or last record, or adding a new record (at the end only).
- ResultSetType — Determines if the cursor is static or a keyset type.
- KeySetSize — If the cursor is a keyset, you can optimize the size of the returned resultset with the KeySetSize property.
- LoginTimeout — Sets the number of seconds to wait until an error is returned.
- MaxRows — Specifies how large the cursor will be. How you determine this depends on the size of the records you are retrieving and the resources available on your computer (memory). A large record (one with many columns and large strings) would take more resources than a smaller record. The MaxRows property should be consequently decreased.
- Options — Specifies whether or not the control executes queries asynchronously. Use asynchronous operation when you expect a query to take more than a few minutes to execute.
- Prompt — When RDO opens a connection based on the parameters of the RemoteData control, the Connect property is expected to contain sufficient information to establish the connection. If information like the data source name, user name, or password is not provided, the ODBC driver manager exposes one or more dialog boxes to gather this information from the user. If you do not want these dialog boxes to appear, set the Prompt property accordingly to disable this feature.
- QueryTimeout — Sets the number of seconds to wait for a query to complete before returning an error.
- BatchSize — This property determines how many statements can be sent in a batch--if batch statements are allowed by the driver.
The shape control is used to create the following predefined shapes on forms, frames, or picture boxes: rectangle, square, oval, circle, rounded rectangle, or rounded square.
Figure:The shape control
You can set the shape style, color, fill style, border color, and border style of any of the shapes you draw on a form.
For simple uses, the shape control allows you to create a variety of shapes without writing any code. For more advanced functionality you need to use the Line and Circle methods.
Predefined Shapes
The Shape property of the shape control provides you with six predefined shapes. The following table lists all the predefined shapes, their values and equivalent Visual Basic constants:
Shape Style Constant Rectangle 0 vbShapeRectangle Square 1 vbShapeSquare Oval 2 vbShapeOval Circle 3 vbShapeCircle Rounded Rectangle 4 vbShapeRoundedRectangle Rounded Square 5 vbShapeRoundedSquare
Figure Predefined shapes
Fill and Line Styles
You can use the FillStyle and BorderStyle properties to set the fill style and border style of any of the shapes you draw on a form.
The FillStyle property, like the Style property, provides you with a number of predefined fill style patterns. These include: Solid, Transparent, Horizontal Line, Vertical Line, Upward Diagonal, Downward Diagonal, Cross, and Diagonal Cross.
The BorderStyle property provides you with a number of predefined border styles. These include: Transparent, Solid, Dash, Dot, Dash-Dot, Dash-Dot-Dot, and Inside Solid.
Setting Color Attributes
The BackColor and FillColor properties allow you to add color to the shape and its border.
At design time, you can set the fill or border colors by choosing either property from the Properties window of the shape control and then selecting from the available palette or system colors.
To set colors at run time, use the Visual Basic color constants (vbGreen, for example) or the system color constants (vbWindowBackground, for example) or the RGB function to specify fill colors.
Note When the FillStyle or BackStyle properties are set to 1 (Transparent), the FillColor and BackColor properties are ignored.
Drawing Shapes on a Form
You can use the shape control to draw rectangles (regular or rounded corners), squares (regular or rounded corners), ovals, and circles on a form.
To draw a shape on a form
- In the toolbox, select the Shape control.
When the pointer moves onto the form, it changes to a cross hair. - Click and drag the cross hair to make the item the size you want.
- From the Properties window, select the Shape property.
- From the Properties window, select the Shape property.
- In the Settings box, select the style you want.
Using the Text Box Control
The text box control is used to display information entered by the user at run time, or assigned to the Text property of the control at design or run time.
Figure :The text box control
In general, the text box control should be used for editable text, although you can make it read-only by setting its Locked property to True. Text boxes also allow you to display multiple lines, to wrap text to the size of the control, and to add basic formatting.
The Text Property
Text entered into the text box control is contained in the Text property. By default, you can enter up to 2048 characters in a text box. If you set the MultiLine property of the control to True, you can enter up to 32K of text.
Formatting Text
When text exceeds the boundaries of the control, you can allow the control to automatically wrap text by setting the MultiLine property to True and add scroll bars by setting the ScrollBars property to add either a horizontal or vertical scroll bar, or both. Automatic text wrapping will be unavailable, however, if you add a horizontal scroll bar because the horizontal edit area is increased by the presence of the scroll bar.
When the MultiLine property is set to True, you can also adjust the alignment of the text to either Left Justify, Center, or Right Justify. The text is left-justified by default. If the MultiLine property is False, setting the Alignment property has no effect.
Selecting Text
You can control the insertion point and selection behavior in a text box with the SelStart, SelLength and SelText properties.
Creating a Password Text Box
A password box is a text box that allows a user to type in his or her password while displaying placeholder characters, such as asterisks. Visual Basic provides two text box properties, PasswordChar and MaxLength, which make it easy to create a password text box.
PasswordChar specifies the character displayed in the text box. For example, if you want asterisks displayed in the password box, you specify * for the PasswordChar property in the Properties window. Regardless of what character a user types in the text box, an asterisk is displayed
Figure:Password example
With MaxLength, you determine how many characters can be typed in the text box. After MaxLength is exceeded, the system emits a beep and the text box does not accept any further characters.
Canceling Keystrokes in a Text Box
You can use the KeyPress event to restrict or transform characters as they are typed. The KeyPress event uses one argument, keyascii. This argument is an integer that represents the numeric (ASCII) equivalent of the character typed in the text box.
The next example demonstrates how to cancel keystrokes as they are typed. If the character typed is not within the specified range, the procedure cancels it by setting KeyAscii to 0. The text box for this example is named txtEnterNums, and the procedure prevents the text box from receiving any characters other than digits. Compare KeyAscii directly to the numeric (Asc) values of various characters.
Private Sub txtEnterNums_KeyPress (KeyAscii As Integer)
If KeyAscii < Asc("0") Or KeyAscii > Asc("9") Then
KeyAscii = 0 ' Cancel the character.
Beep ' Sound error signal.
End If
End Sub
Creating a Read-Only Text Box editing text box contents. Set the Locked property to True to allow users to scroll and highlight text in a text box without allowing changes. With the Locked property set to True, a Copy command will work in a text box, but Cut and Paste commands will not. The Locked property only affects user interaction at run time. You can still change text box contents programmatically at run time by changing the Text property of the text box.
Printing Quotation Marks in a String
Sometimes quotation marks (" ") appear in a string of text.
She said, "You deserve a treat!"
Because strings assigned to a variable or property are surrounded by quotation marks (" "), you must insert an additional set of quotation marks for each set to display in a string. Visual Basic interprets two quotation marks in a row as an embedded quotation mark.
For example, to create the preceding string, use the following code:
Text1.Text = "She said, ""You deserve a treat!"" "
To achieve the same effect, you can use the ASCII character (34) for a quotation mark:
Text1.Text = "She said, " & Chr(34) + "You deserve a treat!" & Chr(34)
Using the Timer Control
Timer controls respond to the passage of time. They are independent of the user, and you can program them to take actions at regular intervals. A typical response is checking the system clock to see if it is time to perform some task. Timers also are useful for other kinds of background processing.
Figure:The timer control
Each timer control has an Interval property that specifies the number of milliseconds that pass between one timer event to the next. Unless it is disabled, a timer continues to receive an event (appropriately named the Timer event) at roughly equal intervals of time.
The Interval property has a few limitations to consider when you're programming a timer control:
- If your application or another application is making heavy demands on the system — such as long loops, intensive calculations, or drive, network, or port access — your application may not get timer events as often as the Interval property specifies.
- The interval can be between 0 and 64,767, inclusive, which means that even the longest interval can't be much longer than one minute (about 64.8 seconds).
- The interval is not guaranteed to elapse exactly on time. To ensure accuracy, the timer should check the system clock when it needs to, rather than try to keep track of accumulated time internally.
- The system generates 18 clock ticks per second — so even though the Interval property is measured in milliseconds, the true precision of an interval is no more than one-eighteenth of a second.
Note The word "timer" is used in several ways in Visual Basic, each closely related to the workings of the timer control. In addition to the control name and control type, "timer" is used in the Timer event and the Timer function.
Placing a Timer Control on a Form
Placing a timer control on a form is like drawing any other control: Click the timer button in the toolbox and drag it onto a form.
The timer appears on the form at design time only so you can select it, view its properties, and write an event procedure for it. At run time, a timer is invisible and its position and size are irrelevant.
Initializing a Timer Control
A timer control has two key properties.
Property Setting Enabled If you want the timer to start working as soon as the form loads, set it to True. Otherwise, leave this property set to False. You might choose to have an outside event (such as a click of a command button) start operation of the timer. Interval Number of milliseconds between timer events. Note that the Enabled property for the timer is different from the Enabled property for other objects. With most objects, the Enabled property determines whether the object can respond to an event caused by the user. With the Timer control, setting Enabled to False suspends timer operation.
Remember that the Timer event is periodic. The Interval property doesn't determine "how long" as much as it determines "how often." The length of the interval should depend on how much precision you want. Because there is some built-in potential for error, make the interval one-half the desired amount of precision.
Note The more often a timer event is generated, the more processor time is used in responding to the event. This can slow down overall performance. Don't set a particularly small interval unless you need it.
Timer Control Scenario: Responding to the Timer Event
When a timer control's interval elapses, Visual Basic generates the Timer event. Typically, you respond to this event by checking some general condition, such as the system clock.
A digital clock is a very simple but very useful application involving a timer control. Once you understand how the application works, you can enhance it to work as an alarm clock, stopwatch, or other timing device.
The Digital Clock application includes a timer and a label with a border. At design time, the application looks like Figure
At run time, the timer is invisible. The following table lists the property settings for the Digital Clock application.
Control Property Setting Label1 BorderStyle Fixed Single Timer1 Interval 500 (half a second) Timer1 Enabled True
The only procedure in this application is an event procedure for the timer: Private Sub Timer1_Timer ()
If lblTime.Caption <> CStr(Time) Then
lblTime.Caption = Time
End If
End Sub
The procedure displays the system time by calling the intrinsic Time function. This function returns a Variant containing the current time as a date/time value (VarType 7). When you assign it to a string variable or property, such as the Caption property in this case, Visual Basic converts it to a string using the time format specified in the Control Panel. If you want to display it using a different format, you can use the Format function.
Multiple-Document Interface (MDI) Applications
The multiple-document interface (MDI) allows you to create an application that maintains multiple forms within a single container form. Applications such as Microsoft Excel and Microsoft Word for Windows have multiple-document interfaces.
An MDI application allows the user to display multiple documents at the same time, with each document displayed in its own window. Documents or child windows are contained in a parent window, which provides a workspace for all the child windows in the application. For example, Microsoft Excel allows you to create and display multiple-document windows of different types. Each individual window is confined to the area of the Excel parent window. When you minimize Excel, all of the document windows are minimized as well; only the parent window's icon appears in the task bar.
A child form is an ordinary form that has its MDIChild property set to True. Your application can include many MDI child forms of similar or different types.
Child forms displayed within the workspace of the MDI form
Note Your application can also include standard, non-MDI forms that are not contained in the MDI form. A typical use of a standard form in an MDI application is to display a modal dialog box. An MDI form is similar to an ordinary form with one restriction. You can't place a control directly on a MDI form unless that control has an Align property (such as a picture box control) or has no visible interface (such as a timer control).
Creating an MDI Application
Use the following procedure to create an MDI form and its child forms.
To create an MDI application
- Create an MDI form.
From the Project menu, choose Add MDI Form.
Note An application can have only one MDI form. If a project already has an MDI form, the Add MDI Form command on the Project menu is unavailable - Create the application's child forms.
To create an MDI child form, create a new form (or open an existing one) and set its MDIChild property to True.
At design time, child forms are not restricted to the area inside the MDI form. You can add controls, set properties, write code, and design the features of child forms just as you would with any other Visual Basic form.
You can determine whether a form is an MDI child by looking at its MDIChild property, or by examining the Project Explorer. If the form's MDIChild property is set to True, it is a child form. Visual Basic displays special icons in the Project Explorer for the MDI and MDI child forms
Icons in the Project Explorer identify MDI child, standard, and MDI forms
Run-Time Features of MDI Forms At run time, an MDI form and all of its child forms take on special characteristics
- All child forms are displayed within the MDI form's workspace. The user can move and size child forms like any other form; however, they are restricted to this workspace.
- When a child form is minimized, its icon appears on the MDI form instead of the taskbar. When the MDI form is minimized, the MDI form and all of its child forms are represented by a single icon. When the MDI form is restored, the MDI form and all the child forms are displayed in the same state they were in before being minimized.
- When a child form is maximized, its caption is combined with the caption of the MDI form and is displayed in the MDI form's title bar.
- By setting the AutoShowChildren property, you can display child forms automatically when forms are loaded (True), or load child forms as hidden (False).
- The active child form's menus (if any) are displayed on the MDI form's menu bar, not on the child form.
The MDI NotePad Application
The MDI NotePad sample application is a simple text editor similar to the NotePad application included with Microsoft Windows. The MDI NotePad application, however, uses a multiple-document interface (MDI). At run time, when the user requests a new document (implemented with the New command on the application's File menu), the application creates a new instance of the child form. This allows the user to create as many child forms, or documents, as necessary.
To create a document-centered application in Visual Basic, you need at least two forms — an MDI form and a child form. At design time, you create an MDI form to contain the application and a single child form to serve as a template for the application's document.
To create your own MDI NotePad application
- From the File menu, choose New Project
- From the Project menu, choose Add MDI Form to create the container form.
The project should now contain an MDI form (MDIForm1) and a standard form (Form1). - Create a text box (Text1) on Form1
- Set properties for the two forms and the text box as follows.
Object Property Setting MDIForm1 Caption MDI NotePad Form1 Caption
MDIChild Untitled
True Text1 MultiLine
Text
Left
Top True
(Empty)
0
0 - Using the Menu Editor (from the Tools menu), create a File menu for MDIForm1.
Caption
Name Indented &File mnuFile No &New mnuFileNew Yes - Add the following code to the mnuFileNew_Click procedure:
Private Sub mnuFileNew_Click ()
' Create a new instance of Form1, called NewDoc.
Dim NewDoc As New Form1
' Display the new form.
NewDoc.Show
End Sub This procedure creates and then displays a new instance (or copy) of Form1, called NewDoc. Each time the user chooses New from the File menu, an exact duplicate (instance) of Form1 is created, including all the controls and code that it contains.
- Add the following code to the Form_Resize procedure for Form1:
Private Sub Form_Resize ()
' Expand text box to fill the current child form.
Text1.Height = ScaleHeight
Text1.Width = ScaleWidth
End Sub - Press F5 to run the application
In addition to the basics of form design, you need to think about the beginning and end of your application. There are several techniques available for determining what the user will see when your application starts. It's also important to be aware of the processes that occur when an application is unloaded.
Setting the Startup Form
By default, the first form in your application is designated as the startup form. When your application starts running, this form is displayed (so the first code to execute is the code in the Form_Initialize event for that form). If you want a different form to display when your application starts, you must change the startup form.
To change the startup form
- From the Project menu, choose Project Properties.
- Choose the General tab
- In the Startup Object list box, select the form you want as the new startup form.
- Choose OK.
Sometimes you might want your application to start without any form initially loaded. For example, you might want to execute code that loads a data file and then displays one of several different forms depending on what is in the data file. You can do this by creating a Sub procedure called Main in a standard module, as in the following example:
Sub Main()
Dim intStatus As Integer
' Call a function procedure to check user status.
intStatus = GetUserStatus
' Show a startup form based on status.
If intStatus = 1 Then
frmMain.Show
Else
frmPassword.Show
End If
This procedure must be a Sub procedure, and it cannot be in a form module. To set the Sub Main procedure as the startup object, from the Project menu, choose Project Properties, select the General tab, and select Sub Main from the Startup Object box.
Displaying a Splash Screen on Startup If you need to execute a lengthy procedure on startup, such as loading a large amount of data from a database or loading several large bitmaps, you might want to display a splash screen on startup. A splash screen is a form, usually displaying information such as the name of the application, copyright information, and a simple bitmap. The screen that is displayed when you start Visual Basic is a splash screen.
To display a splash screen, use a Sub Main procedure as your startup object and use the Show method to display the form:
Private Sub Main()
' Show the splash screen.
frmSplash.Show
' Add your startup procedures here.
…
' Show the main form and unload the splash screen.
frmMain.Show
Unload frmSplash
End Sub
The splash screen occupies the user's attention while your startup procedures are executing, giving the illusion that the application is loading faster. When the startup procedures are completed, you can load your first form and unload the splash screen.
In designing a splash screen, it's a good idea to keep it simple. If you use large bitmaps or a lot of controls, the splash screen itself may be slow to load.
Ending an Application An event-driven application stops running when all its forms are closed and no code is executing. If a hidden form still exists when the last visible form is closed, your application will appear to have ended (because no forms are visible), but will in fact continue to run until all the hidden forms are closed. This situation can arise because any access to an unloaded form's properties or controls implicitly loads that form without displaying it.
The best way to avoid this problem when closing your application is to make sure all your forms are unloaded. If you have more than one form, you can use the Forms collection and the Unload statement. For example, on your main form you could have a command button named cmdQuit that lets a user exit the program. If your application has only one form, the Click event procedure could be as simple as this:
Private Sub cmdQuit_Click ()
Unload Me
End Sub
If your application uses multiple forms, you can unload the forms by putting code in the Unload event procedure of your main form. You can use the Forms collection to make sure you find and close all your forms. The following code uses the forms collection to unload all forms:
Private Sub Form_Unload (Cancel As Integer)
Dim i as integer
' Loop through the forms collection and unload
' each form.
For i = Forms.Count – 1 to 0 Step - 1
Unload Forms(i)
Next
End Sub
There may be cases where you need to end your application without regard for the state of any existing forms or objects. Visual Basic provides the End statement for this purpose.
The End statement ends an application immediately: no code after the End statement is executed, and no further events occur. In particular, Visual Basic will not execute the QueryUnload, Unload or Terminate event procedures for any forms. Object references will be freed, but if you have defined your own classes, Visual Basic will not execute the Terminate events of objects created from your classes.
In addition to the End statement, the Stop statement halts an application. However, you should use the Stop statement only while debugging, because it does not free references to objects
Creating Menus with the Menu Editor
You can use the Menu Editor to create new menus and menu bars, add new commands to existing menus, replace existing menu commands with your own commands, and change and delete existing menus and menu bars. To display the Menu Editor
From the Tools menu, choose Menu Editor.
–or–
Click the Menu Editor button on the toolbar.
The Menu Editor
While most menu control properties can be set using the Menu Editor, all menu properties are available in the Properties window. The two most important properties for menu controls are:
- Name — This is the name you use to reference the menu control from code.
- Caption — This is the text that appears on the control.
Using the List Box in the Menu Editor
The menu control list box (the lower portion of the Menu Editor) lists all the menu controls for the current form. When you type a menu item in the Caption text box, that item also appears in the menu control list box. Selecting an existing menu control from the list box allows you to edit the properties for that control.
For example, Figure 6.7 shows the menu controls for a File menu in a typical application. The position of the menu control in the menu control list box determines whether the control is a menu title, menu item, submenu title, or submenu item:
- A menu control that appears flush left in the list box is displayed on the menu bar as a menu title.
- A menu control that is indented once in the list box is displayed on the menu when the user clicks the preceding menu title.
- An indented menu control followed by menu controls that are further indented becomes a submenu title. Menu controls indented below the submenu title become tems of that submenu.
- A menu control with a hyphen (-) as its Caption property setting appears as a separator bar. A separator bar divides menu items into logical groups.
To create menu controls in the Menu Editor
- Select the form.
- From the Tools menu, choose Menu Editor.
–or–
Click the Menu Editor button on the toolbar. - In the Caption text box, type the text for the first menu title that you want to appear on the menu bar. Also, place an ampersand (&) before the letter you want to be the access key for that menu item. This letter will automatically be underlined in the menu.
The menu title text is displayed in the menu control list box. - In the Name text box, type the name that you will use to refer to the menu control in code. See "Menu Title and Naming Guidelines" later in this chapter.
- Click the left arrow or right arrow buttons to change the indentation level of the control.
- Set other properties for the control, if you choose. You can do this in the Menu Editor or later, in the Properties window.
- Choose Next to create another menu control.
–or–
Click Insert to add a menu control between existing controls.
You can also click the up arrow and down arrow buttons to move the control among the existing menu controls. - Choose OK to close the Menu Editor when you have created all the menu controls for that form.
The menu titles you create are displayed on the form. At design time, click a menu title to drop down its corresponding menu items.
A separator bar is displayed as a horizontal line between items on a menu. On a menu with many items, you can use a separator bar to divide items into logical groups. For example, the File menu in Visual Basic uses separator bars to divide its menu items into three groups
Separator bars
To create a separator bar in the Menu Editor
- If you are adding a separator bar to an existing menu, choose Insert to insert a menu control between the menu items you want to separate.
- If necessary, click the right arrow button to indent the new menu item to the same level as the menu items it will separate.
- Type a hyphen (-) in the Caption text box.
- Set the Name property
- Choose OK to close the Menu Editor.
Assigning Access Keys and Shortcut Keys Access keys allow the user to open a menu by pressing the ALT key and typing a designated letter. Once a menu is open, the user can choose a control by pressing the letter (the access key) assigned to it. For example, ALT+E might open the Edit menu, and P might select the Paste menu item.
Access keys
To assign an access key to a menu control in the Menu Editor
- Select the menu item to which you want to assign an access key.
- In the Caption box, type an ampersand (&) immediately in front of the letter you want to be the access key.
Shortcut keys run a menu item immediately when pressed. Frequently used menu items may be assigned a keyboard shortcut, which provides a single-step method of keyboard access, rather than a three-step method of pressing ALT, a menu title access character, and then a menu item access character. Shortcut key assignments include function key and control key combinations, such as CTRL+F1 or CTRL+A.
Shortcut keys
To assign a shortcut key to a menu item
- Open the Menu Editor.
- Select the menu item.
- Select a function key or key combination in the Shortcut combo box.
To remove a shortcut key assignment, choose "(none)" from the top of the list
Note Shortcut keys appear automatically on the menu; therefore, you do not have to enter CTRL+key in the Caption box of the Menu Editor.
Each menu you create can include up to five levels of submenus. A submenu branches off another menu to display its own menu items. You may want to use a submenu when:
- The menu bar is full.
- A particular menu control is seldom used.
- You want to emphasize one menu control's relationship to another.
If there is room on the menu bar, however, it's better to create an additional menu title instead of a submenu. That way, all the controls are visible to the user when the menu is dropped down. It's also good programming practice to restrict the use of submenus so users don't get lost trying to navigate your application's menu interface. (Most applications use only one level of submenus.)
To create a submenu
- Create the menu item that you want to be the submenu title.
- Create the items that will appear on the new submenu, and indent them by clicking the right arrow button.
Each indent level is preceded by four dots (....) in the Menu Editor. To remove one level of indentation, click the left arrow button.
A menu control array is a set of menu items on the same menu that share the same name and event procedures. Use a menu control array to:
- Create a new menu item at run time when it must be a member of a control array. The MDI Notepad sample, for example, uses a menu control array to store a list of recently opened files.
- Simplify code, because common blocks of code can be used for all menu items
To create a menu control array in the Menu Editor
- Select the form
- From the Tools menu, choose Menu Editor.
–or–
Click the Menu Editor button on the toolbar. - In the Caption text box, type the text for the first menu title that you want to appear on the menu bar.
The menu title text is displayed in the menu control list box. - In the Name text box, type the name that you will use to refer to the menu control in code. Leave the Index box empty.
- At the next indentation level, create the menu item that will become the first element in the array by setting its Caption and Name.
- Set the Index for the first element in the array to 0.
- Create a second menu item at the same level of indentation as the first.
- Set the Name of the second element to the same as the first element and set its Index to 1.
- Repeat steps 5 – 8 for subsequent elements of the array
Creating and Modifying Menus at Run Time
The menus you create at design time can also respond dynamically to run-time conditions. For example, if a menu item action becomes inappropriate at some point, you can prevent users from selecting that menu item by disabling it. In the MDI NotePad application, for example, if the clipboard doesn't contain any text, the Paste menu item is dimmed on the Edit menu, and users cannot select it.
You can also dynamically add menu items, if you have a menu control array. This is described in "Adding Menu Controls at Run Time," later in this topic.
You can also program your application to use a check mark to indicate which of several commands was last selected. For example, the Options, Toolbar menu item from the MDI NotePad application displays a check mark if the toolbar is displayed. Other menu control features described in this section include code that makes a menu item visible or invisible and that adds or deletes menu items.
Enabling and Disabling Menu Commands All menu controls have an Enabled property, and when this property is set to False, the menu is disabled and does not respond to user actions. Shortcut key access is also disabled when Enabled is set to False.
A disabled menu item
For example, this statement disables the Paste menu item on the Edit menu of the MDI NotePad application:
mnuEditPaste.Enabled = False
Disabling a menu title in effect disables the entire menu, because the user cannot access any menu item without first clicking the menu title. For example, the following code would disable the Edit menu of the MDI Notepad application:
mnuEdit.Enabled = False
Displaying a Check Mark on a Menu Control
Using the Checked property, you can place a check mark on a menu to:
- Tell the user the status of an on/off condition. Choosing the menu command alternately adds and removes the check mark.
- Indicate which of several modes is in effect. The Options menu of the MDI Notepad application uses a check mark to indicate the state of the toolbar.
You create check marks in Visual Basic with the Checked property. Set the initial value of the Checked property in the Menu Editor by selecting the check box labeled Checked. To add or remove a check mark from a menu control at run time, set its Checked property from code. For example:
Private Sub mnuOptions_Click ()
' Set the state of the check mark based on
' the Visible property.
mnuOptionsToolbar.Checked = picToolbar.Visible
End Sub
Making Menu Controls Invisible In the Menu Editor, you set the initial value of the Visible property for a menu control by selecting the check box labeled Visible. To make a menu control visible or invisible at run time, set its Visible property from code. For example:
mnuFileArray(0).Visible = True ' Make the control
' visible.
mnuFileArray(0).Visible = False ' Make the control
' invisible.
When a menu control is invisible, the rest of the controls in the menu move up to fill the empty space. If the control is on the menu bar, the rest of the controls on the menu bar move left to fill the space.
Note Making a menu control invisible effectively disables it, because the control is inaccessible from the menu, access or shortcut keys. If the menu title is invisible, all the controls on that menu are unavailable.
Adding Menu Controls at Run Time
Menu control array elements created and displayed at run time
You must use a control array to create a control at run time. Because the mnuRecentFile menu control is assigned a value for the Index property at design time, it automatically becomes an element of a control array — even though no other elements have yet been created.
When you create mnuRecentFile(0), you actually create a separator bar that is invisible at run time. The first time a user saves a file at run time, the separator bar becomes visible, and the first file name is added to the menu. Each time you save a file at run time, additional menu controls are loaded into the array, making the menu grow.
Controls created at run time can be hidden by using the Hide method or by setting the control's Visible property to False. If you want to remove a control in a control array from memory, use the Unload statement.
Writing Code for Menu Controls
When the user chooses a menu control, a Click event occurs. You need to write a Click event procedure in code for each menu control. All menu controls except separator bars (and disabled or invisible menu controls) recognize the Click event.
The code that you write in a menu event procedure is no different than that which you would write in any other control's event procedure. For example, the code in a File, Close menu's Click event might look like this:
Sub mnuFileClose_Click()
Unload Me
End Sub
Visual Basic displays a menu automatically when the menu title is chosen; therefore, it is not necessary to write code for a menu title's Click event procedure unless you want to perform another action, such as disabling certain menu items each time the menu is displayed.
Note At design time, the menus you create are displayed on the form when you close the Menu Editor. Choosing a menu item on the form displays the Click event procedure for that menu control.
Displaying Pop-up Menus A pop-up menu is a floating menu that is displayed over a form, independent of the menu bar. The items displayed on the pop-up menu depend on where the pointer was located when the right mouse button was pressed; therefore, pop-up menus are also called context menus. In Microsoft Windows 95 or later systems, you activate context menus by clicking the right mouse button.
Any menu that has at least one menu item can be displayed at run time as a pop-up menu. To display a pop-up menu, use the PopupMenu method. This method uses the following syntax:
[object.]PopupMenu menuname [, flags [,x [, y [, boldcommand ]]]]
For example, the following code displays a menu named mnuFile when the user clicks a form with the right mouse button. You can use the MouseUp or MouseDown event to detect when the user clicks the right mouse button, although the standard is to use the MouseUp event:
Private Sub Form_MouseUp (Button As Integer, Shift As _
Integer, X As Single, Y As Single)
If Button = 2 Then ' Check if right mouse button
' was clicked.
PopupMenu mnuFile ' Display the File menu as a
' pop-up menu.
End If
End Sub
Any code following a call to the PopupMenu method is not run until the user selects an item in the menu or cancels the menu.
Note Only one pop-up menu can be displayed at a time. While a pop-up menu is displayed, calls to the PopupMenu method are ignored. Calls to the PopupMenu method are also ignored whenever a menu control is active.
Often you want a pop-up menu to access options that are not usually available on the menu bar. To create a menu that will not display on the menu bar, make the top-level menu item invisible at design time (make sure the Visible check box in the Menu Editor is not checked). When Visual Basic displays a pop-up menu, the Visible property of the specified top-level menu is ignored.
The Flags Argument You use the flags argument in the PopupMenu method to further define the location and behavior of a pop-up menu. The following table lists the flags available to describe a pop-up menu's location.
Location constants Description vbPopupMenuLeftAlign Default. The specified x location defines the left edge of the pop-up menu. vbPopupMenuCenterAlign The pop-up menu is centered around the specified x location. vbPopupMenuRightAlign The specified x location defines the right edge of the pop-up menu. The following table lists the flags available to describe a pop-up menu's behavior.
Behavior constants Description vbPopupMenuLeftButton Default. The pop-up menu is displayed when the user clicks a menu item with the left mouse button only. vbPopupMenuRightButton The pop-up menu is displayed when the user clicks a menu item with either the right or left mouse button To specify a flag, you combine one constant from each group using the Or operator. The following code displays a pop-up menu with its top border centered on a form when the user clicks a command button. The pop-up menu triggers Click events for menu items that are clicked with either the right or left mouse button.
Private Sub Command1_Click ()
' Dimension X and Y variables.
Dim xloc, yloc
' Set X and Y variables to center of form.
xloc = ScaleWidth / 2
yloc = ScaleHeight / 2
' Display the pop-up menu.
PopupMenu mnuEdit, vbPopupMenuCenterAlign Or _
vbPopupMenuRightButton, xloc, yloc
End Sub
Menus in MDI Applications
In an MDI application, the menus for each child are displayed on the MDI form, rather than on the child forms themselves. When a child form has the focus, that child's menu (if any) replaces the MDI form's menu on the menu bar. If there are no child forms visible, or if the child with the focus does not have a menu, the MDI form's menu is displayed
It is common for MDI applications to use several sets of menus. When the user opens a document, the application displays the menu associated with that type of document. Usually, a different menu is displayed when no child forms are visible. For example, when there are no files open, Microsoft Excel displays only the File and Help menus. When the user opens a file, other menus are displayed (File, Edit, View, Insert, Format, Tools, Data, Window, and so on).
Creating Menus for MDI Applications
You can create menus for your Visual Basic application by adding menu controls to the MDI form and to the child forms. One way to manage the menus in your MDI application is to place the menu controls you want displayed all of the time, even when no child forms are visible, on the MDI form. When you run the application, the MDI form's menu is automatically displayed when there are no child forms visible
The MDI form menu is displayed when no child forms are loaded
Place the menu controls that apply to a child form on the child form. At run time, as long as there is at least one child form visible, these menu titles are displayed in the menu bar of the MDI form.
Some applications support more than one type of document. For example, in Microsoft Access, you can open tables, queries, forms, and other document types. To create an application such as this in Visual Basic, use two child forms. Design one child with menus that perform spreadsheet tasks and the other with menus that perform charting tasks.
At run time, when an instance of a spreadsheet form has the focus, the spreadsheet menu is displayed, and when the user selects a chart, that form's menu is displayed. If all the spreadsheets and charts are closed, the MDI form's menu is displayed. For more information on creating menus, see "Using Menus in Your Application" earlier in this chapter.
Creating a Window Menu
Most MDI applications (for example, Microsoft Word for Windows and Microsoft Excel) incorporate a Window menu. This is a special menu that displays the captions of all open child forms, as shown in Figure 6.15. In addition, some applications place commands on this menu that manipulate the child windows, such as Cascade, Tile, and Arrange Icons.
The Window menu displays the name of each open child form
Any menu control on an MDI form or MDI child form can be used to display the list of open child forms by setting the WindowList property for that menu control to True. At run time, Visual Basic automatically manages and displays the list of captions and displays a check mark next to the one that had the focus most recently. In addition, a separator bar is automatically placed above the list of windows.
To set the WindowList property
- Select the form where you want the menu to appear, and from the Tools menu, choose Menu Editor.
Note The WindowList property applies only to MDI forms and MDI child forms. It has no effect on standard (non-MDI) forms. - In the Menu Editor list box, select the menu where you want the list of open child forms to display.
- Select the WindowList check box.
Arranging Child Forms
As was mentioned earlier, some applications list actions such as Tile, Cascade, and Arrange Icons on a menu, along with the list of open child forms. Use the Arrange method to rearrange child forms in the MDI form. You can display child forms as cascading, as horizontally tiled, or as child form icons arranged along the lower portion of the MDI form. The following example shows the Click event procedures for the Cascade, Tile, and Arrange Icons menu controls.
Private Sub mnuWCascade_Click ()
' Cascade child forms.
frmMDI.Arrange vbCascade
End Sub
Private Sub mnuWTile_Click ()
' Tile child forms (horizontal).
frmMDI.Arrange vbTileHorizontal
End Sub
Private Sub mnuWArrange_Click ()
' Arrange all child form icons.
frmMDI.Arrange vbArrangeIcons
End Sub
Note The intrinsic constants vbCascade, vbTileHorizontal, and vbArrangeIcons are listed in the Visual Basic (VB) object library of the Object Browser.
When you tile or cascade child forms that have a fixed border style, each child form is positioned as if it had a sizable border. This can cause child forms to overlap.
Creating a Toolbar
The toolbar (also called a ribbon or control bar) has become a standard feature in many Windows-based applications. A toolbar provides quick access to the most frequently used menu commands in an application. Creating a toolbar is easy and convenient using the toolbar control, which is available with the Professional and Enterprise editions of Visual Basic. If you are using the Learning Edition of Visual Basic, you can create toolbars manually as described in "Negotiating Menu and Toolbar Appearance" later in this chapter. The following example demonstrates creating a toolbar for an MDI application; the procedure for creating a toolbar on a standard form is basically the same.
To manually create a toolbar
- Place a picture box on the MDI form.
The width of the picture box automatically stretches to fill the width of the MDI form's workspace. The workspace is the area inside a form's borders, not including the title bar, menu bar, or any toolbars, status bars, or scroll bars that may be on the form.
Note You can place only those controls that support the Align property directly on an MDI form (the picture box is the only standard control that supports this property). - Inside the picture box, place any controls you want to display on the toolbar.
Typically, you create buttons for the toolbar using command buttons or image controls. Figure 6.16 shows a toolbar containing image controls.
To add a control inside a picture box, click the control button in the toolbox, and then draw it inside the picture box.
Note When an MDI form contains a picture box, the internal area of the MDI form does not include the area of the picture box. For example, the ScaleHeight property of the MDI form returns the internal height of the MDI form, which does not include the height of the picture box. You can create buttons for the toolbar using image controls
- Set design-time properties.
One advantage of using a toolbar is that you can present the user with a graphical representation of a command. The image control is a good choice as a toolbar button because you can use it to display a bitmap. Set its Picture property at design time to display a bitmap; this provides the user with a visual cue of the command performed when the button is clicked. You can also use ToolTips, which display the name of the toolbar button when a user rests the mouse pointer over a button, by setting the ToolTipText property for the button. - Write code.
Because toolbar buttons are frequently used to provide easy access to other commands, most of the time you call other procedures, such as a corresponding menu command, from within each button's Click event.
Toolbars are used to provide the user with a quick way to access some of the application's commands. For example, the first button on the toolbar in Figure 6.16 is a shortcut for the File New command. There are now three places in the MDI NotePad sample application where the user can request a new file:
- On the MDI form (New on the MDI form File menu)
- On the child form (New on the child form File menu)
- On the toolbar (File New button)
' This module is in a public procedure.
Public Sub FileNew ()
Dim frmNewPad As New frmNotePad
frmNewPad.Show
End Sub
' The user chooses New on the child form File menu.
Private Sub mnuchildFileNew_Click ()
FileNew
End Sub
' The user chooses New on the MDI form File menu.
Private Sub mnumdiFileNew_Click ()
frmNotePad.FileNew
End Sub
' The user clicks the File New button on the toolbar.
Private Sub btnFileNew_Click ()
frmNotePad.FileNew
End Sub
Negotiating Menu and Toolbar Appearance When an object supplied by another application is activated on a form, there are a number of ways that object's menus and toolbars may appear on the container form; therefore, you need to specify how they will be displayed. This process is called user-interface negotiation because Visual Basic and the object you have linked or embedded must negotiate for space in the container form.
Controlling Menu Appearance
You can determine whether a linked or embedded object's menu will appear in the container form by setting a form's NegotiateMenus property. If the child form's NegotiateMenus property is set to True (default) and the container has a menu bar defined, the object's menus are placed on the container's menu bar when the object is activated. If the container has no menu bar, or the NegotiateMenus property is set to False, the object's menus will not appear when it is activated.
Note The NegotiateMenus property does not apply to MDI Forms.
Controlling Toolbar Appearance
The MDI form's NegotiateToolbars property determines whether the linked or embedded object's toolbars will be floating palettes or placed on the parent form. This behavior does not require toolbars to be present on the MDI parent form. If the MDI form's NegotiateToolbars property is True, the object's toolbar appears on the MDI parent form. If NegotiateToolbars is False, the object's toolbar will be a floating palette.
Note The NegotiateToolbars property applies only to MDI forms.
If an MDI form includes a toolbar, it is usually contained in a picture box control on the parent form. The picture box's Negotiate property determines whether the container's toolbar is still displayed or is replaced by the object's toolbar when activated. If Negotiate is True, the object's toolbar is displayed in addition to the container's toolbar. If Negotiate is False, the object's toolbar replaces the container's toolbar.
Note Menu and toolbar negotiation will occur only for insertable objects that support in-place activation. For more information on in-place activation, see "Programming with ActiveX Components."
You can see how these three properties interact by using the following procedure.
To perform menu and toolbar negotiation
- Add a toolbar to an MDI form. This is described in "Creating a Toolbar" earlier in this chapter.
- Place an insertable object on a child form.
- Set the NegotiateMenus, NegotiateToolbars, and Negotiate properties.
- Run the application, and double-click the object.
The easiest way to add a dialog box to your application is to use a predefined dialog, because you don't have to worry about designing, loading, or showing the dialog box. However, your control over its appearance is limited. Predefined dialog boxes are always modal.
The following table lists the functions you can use to add predefined dialog boxes to your Visual Basic application.
Use this function To do this InputBox function Display a command prompt in a dialog box, and return whatever is entered by the user. MsgBox function Display a message in a dialog box, and return a value indicating the command button was clicked by the user. Prompting for Input with InputBox
Use the InputBox function to solicit data from the user. This function displays a modal dialog box that asks the user to enter some data. The text input box
A dialog box using the InputBox function
FileName = InputBox("Enter file to open:", "File Open")
Note Remember that when you use the InputBox function, you have little control over the components of the dialog box. You can change only the text in the title bar, the command prompt displayed to the user, the position of the dialog box on the screen, and whether or not it displays a Help button.
Displaying Information with MsgBox
Use the MsgBox function to get yes or no responses from users, and to display brief messages, such as errors, warnings, or alerts in a dialog box. After reading the message, the user chooses a button to close the dialog box.
An error message dialog box created using the MsgBox function MsgBox "Error encountered while trying to open file, _
please retry.", vbExclamation, "Text Editor"
Using Forms as Custom Dialog Boxes
A custom dialog box is a form you create containing controls — including command buttons, option buttons, and text boxes — that lets the user supply information to the application. You customize the appearance of the form by setting property values. You also write code to display the dialog box at run time.
To create a custom dialog box, you can start with a new form or customize an existing dialog box. Over time, you can build up a collection of dialog boxes that can be used in many applications
To customize an existing dialog box
- From the Project menu, choose Add Form to add an existing form to your project.
- From the File menu, choose Save filename As and enter a new file name. (This prevents you from making changes to the existing version of the form).
- Customize the appearance of the form as needed.
- Customize event procedures in the Code window.
To create a new dialog box
- From the Project menu, choose Add Form.
–or–
Click the Form button on the toolbar to create a new form - Customize the appearance of the form as needed.
- Customize event procedures in the Code window.
Adding a Title A dialog box should always have a title that identifies it. To create a title, set the form's Caption property to the text string that will appear in the title bar. Usually, this is done at design time using the Properties window, but you can also do this from code. For example:
frmAbout.Caption = "About"
Tip If you want to remove the title bar completely, set the form's ControlBox, MinButton, and MaxButton properties to False; set the BorderStyle to a nonsizable setting (0, 1, or 3); and set the Caption equal to an empty string (""). Setting Standard Dialog Box Properties
Generally, the user responds to a dialog box by providing information and then closing the dialog box with an OK or Cancel command button. Because a dialog box is temporary, users usually don't need to move, size, maximize, or minimize it. As a result, the sizable border style, Control menu box, Maximize button, and Minimize button that come with a new form are unnecessary on most dialog boxes.
You can remove these items by setting the BorderStyle, ControlBox, MaxButton, and MinButton properties. For example, an About dialog box might use the following property settings
Property Setting Effect BorderStyle 1 Changes the border style to fixed single, thus preventing the dialog box from being sized at run time. ControlBox False Removes the Control menu box. MaxButton False Removes the Maximize button, thus preventing the dialog box from being maximized at run time. MinButton False Removes the Minimize button, thus preventing the dialog box from being minimized at run time.
Remember that if you remove the Control menu box (ControlBox = False), you must provide the user with another way to exit the dialog box. This is commonly done by adding an OK, Cancel, or Exit command button to the dialog box and adding code in the Click event for the button that hides or unloads the dialog. Performing Multiple Actions on an Object
You often need to perform several different actions on the same object. For example, you might need to set several properties for the same object. One way to do this is to use several statements. Private Sub Form_Load()
Command1.Caption = "OK"
Command1.Visible = True
Command1.Top = 200
Command1.Left = 5000
Command1.Enabled = True
End Sub
Notice that all these statements use the same object variable, Command1. You can make this code easier to write, easier to read, and more efficient to run by using the With...End With statement.
Private Sub Form_Load()
With Command1
.Caption = "OK"
.Visible = True
.Top = 200
.Left = 5000
.Enabled = True
End With
End Sub
You can also nest With statements by placing one With...End With statement inside another With...End With statement.
Using Default Properties
Many objects have default properties. You can use default properties to simplify your code, because you don't have to refer explicitly to the property when setting its value. For an object where Value is the default property, these two statements are equivalent: object = 20
and
object.Value = 20
To see how this works, draw a command button and a text box on a form. Add the following statement to the command button's Click event:
Text1 = "hello"
Run the application and click the command button. Because Text is the default property of the text box, the text box will display the text, "hello."
Using Default Properties with Object Variables
When a reference to an object is stored in an object variable, you can still use the default property. The following code fragment demonstrates this.
Private Sub Command1_Click()
Dim obj As Object
' Place a reference to Text1 in the object
' variable.
Set obj = Text1
' Set the value of the default property (Text).
obj = "hello"
End Sub
In the code above, obj = "hello" is exactly the same as typing obj.Text = "hello".
Using Default Properties with Variants
Accessing default properties is different when an object reference is stored in a variable of type Variant, instead of in an object variable. This is because a Variant can contain data of many different types. For example, you can read the default property of Text1 using a reference in a Variant, but trying to assign the string "goodbye" to the default property doesn't work. Instead, it replaces the object reference with the string, and changes the Variant type.
To see how this works, enter the following code in the Click event of the command button from the previous example:
Private Sub Command1_Click()
Dim vnt As Variant
' Set the default property (Text) to "hello".
Text1 = "hello"
' Place a reference to Text1 in the Variant.
Set vnt = Text1
' Display the default property of Text1, and show
' that the Variant contains an object reference.
MsgBox vnt, , "IsObject? " & IsObject(vnt)
' Attempt to set the default property of Text1.
vnt = "goodbye"
MsgBox vnt, , "IsObject? " & IsObject(vnt)
End Sub
When you run the application and click the command button, you first get a message box displaying the current value of the default property of Text1, "hello," which you can verify by looking at Text1. The caption of the message box confirms that the Variant contains an object reference — that is, a reference to Text1.
When you click the OK button on the message box, "goodbye" is assigned to the Variant, destroying the reference to Text1. Another message box is then displayed, showing the contents of the Variant — which as you can see doesn't match the current value of Text1.Text.
The caption of the message box confirms that the Variant no longer contains an object reference — it now contains the string "goodbye."
Creating Arrays of Objects
You can declare and use arrays of an object type just as you declare and use an array of any data type. These arrays can be fixed-size or dynamic. Arrays of Form Variables
You can declare an array of forms with Private, Dim, ReDim, Static, or Public in the same way you declare an array of any other type. If you declare the array with the New keyword, Visual Basic automatically creates a new instance of the form for each element in the array as you use the elements in the array. Private Sub Command1_Click ()
Dim intX As Integer
Dim frmNew(1 To 5) As New Form1
For intX = 1 To 5
frmNew(intX).Show
frmNew(intX).WindowState = vbMinimized
' To create minimized forms without having them
' first appear briefly at normal size, reverse
' the order of the two lines above.
Next
End Sub
Pressing the command button to execute the code above will create five minimized instances of Form1.
Note If you look at the Task Bar, you'll see Form1 six times. The extra instance of Form1 isn't minimized — it's the one you started with.
Arrays of Control Variables
You can declare an array of controls with Private, Dim, ReDim, Static, or Public in the same way you declare an array of any other type. Unlike form arrays, however, control arrays cannot be declared with the New keyword. For example, you can declare an array to be a specific control type:
ReDim ActiveImages(10) As Image
When you declare an array to be a particular control type, you can assign only controls of that type to the array. In the case of the preceding declaration, for example, you can only assign image controls to the array — but those image controls can come from different forms.
Contrast this with the built-in Controls collection, which can contain many different types of controls — all which must be on the same form.
Alternatively, you can declare an array of generic control variables. For example, you might want to keep track of every control that was dropped onto a particular control, and not allow any control to be dropped more than once. You can do this by maintaining a dynamic array of control variables that contains references to each control that has been dropped:
Private Sub List1_DragDrop(Source As VB.Control, _
X As Single, Y As Single)
Dim intX As Integer
Static intSize As Integer
Static ctlDropped() As Control
For intX = 1 To intSize
' If the dropped control is in the array, it's
' already been dropped here once.
If ctlDropped(intX) Is Source Then
Beep
Exit Sub
End If
Next
' Enlarge the array.
intSize = intSize + 1
ReDim Preserve ctlDropped(intSize)
' Save a reference to the control that was dropped.
Set ctlDropped(intSize) = Source
' Add the name of the control to the list box.
List1.AddItem Source.Name
End Sub
This example uses the Is operator to compare the variables in the control array with the control argument. The Is operator can be used to test the identity of Visual Basic object references: If you compare two different references to the same object, the Is operator returns True.
The example also uses the Set statement to assign the object reference in the Source argument to an element in the array.
Creating Collections of Objects Collections provide a useful way to keep track of objects. Unlike arrays, Collection objects don't have to be re-dimensioned as you add and remove members.
For example, you might want to keep track of every control that was dropped onto a particular control, and not allow any control to be dropped more than once. You can do this by maintaining a Collection that contains references to each control that has been dropped:
Private Sub List1_DragDrop(Source As VB.Control, _
X As Single, Y As Single)
Dim vnt As Variant
Static colDroppedControls As New Collection
For Each vnt In colDroppedControls
' If the dropped control is in the collection,
' it's already been dropped here once.
If vnt Is Source Then
Beep
Exit Sub
End If
Next
' Save a reference to the control that was dropped.
colDroppedControls.Add Source
' Add the name of the control to the list box.
List1.AddItem Source.Name
End Sub
This example uses the Is operator to compare the object references in the colDroppedControls collection with the event argument containing the reference to the dropped control. The Is operator can be used to test the identity of Visual Basic object references: If you compare two different references to the same object, the Is operator returns True.
The example also uses the Add method of the Collection object to place a reference to the dropped control in the collection.
Unlike arrays, Collections are objects themselves. The variable colDroppedControls is declared As New, so that an instance of the Collection class will be created the first time the variable is referred to in code. The variable is also declared Static, so that the Collection object will not be destroyed when the event procedure ends.
The Visual Basic Collection Object
A collection is a way of grouping a set of related items. Collections are used in Visual Basic to keep track of many things, such as the loaded forms in your program (the Forms collection), or all the controls on a form (the Controls collection).
Visual Basic provides the generic Collection class to give you the ability to define your own collections. You can create as many Collection objects — that is, instances of the Collection class — as you need. You can use Collection objects as the basis for your own collection classes and object models, as discussed in "Creating Your Own Collection Classes" and "Object Models" later in this chapter.
For example, collections are a good way to keep track of multiple forms. "Multiple Document Interface (MDI) Applications" in "Creating a User Interface" discusses applications in which the user can open any number of document windows. The following code fragment shows how you might use the Add method of a collection object to keep a list of MDI child windows the user has created. This code assumes that you have a form named mdiDocument, whose MDIChild property is set to True.
' Module-level collection in the parent MDIForm.
Public colDocuments As New Collection
' Code for creating a new MDI child document form.
Private Sub mnuFileNew()
Dim f As New mdiDocument
Static intDocumentNumber As Integer
intDocumentNumber = intDocumentNumber + 1
' The following line creates the form.
f.Caption = "Document" & intDocumentNumber
' Add the object reference to the collection.
colDocuments.Add f
f.Show
End Sub
The colDocuments collection acts like a subset of the built-in Forms collection, containing only instances of the form mdiDocument. The size of the collection is adjusted automatically as each new form is added. You can use For Each ... Next to iterate through the collection. If you want to give the form a key by which it can be retrieved, you can supply a text string as the second parameter of the Add method, as described later in this section.
The New keyword in the declaration for the variable colDocuments causes a Collection object to be created the first time the variable is referred to in code. Because Collection is a class, rather than a data type, you must create an instance of it and keep a reference to that instance (object) in a variable.
Like any other object, a Collection object will be destroyed when the last variable that contains a reference to it is set to Nothing or goes out of scope. All the object references it contains will be released. For this reason, the variable colDocuments is declared in the parent MDIForm, so that it exists throughout the life of the program.
Note If you use a collection to keep track of forms, use the collection's Remove method to delete the object reference from the collection after you unload the form. You cannot reclaim the memory the form was using as long as a reference to the form still exists, and the reference the Collection object is holding is just as good as a reference in an object variable.
Collections in Visual Basic
What is a collection? In "The Visual Basic Collection Object," a collection was defined as a way of grouping related objects. That leaves a lot of room for interpretation; it's more of a concept than a definition.
In fact, as you'll see when you begin comparing collections, there are a lot of differences even among the kinds of collections provided in Visual Basic. For example, the following code causes an error:
Dim col As Collection
Set col = Forms ' Error!
What's happening here? The Forms collection is a collection; the variable col is declared As Collection; why can't you assign a reference to Forms to the variable col?
The reason for this is that the Collection class and the Forms collection are not polymorphic; that is, you can't exchange one for the other, because they were developed from separate code bases. They don't have the same methods, store object references in the same way, or use the same kinds of index values.
This makes the Collection class's name seem like an odd choice, because it really represents only one of many possible collection implementations. This topic explores some of the implementation differences you'll encounter.
Zero-Based and One-Based Collections
A collection is either zero-based or one-based, depending on what its starting index is. As you might guess, the former means that the index of the first item in the collection is zero, and the latter means it's one. Examples of zero-based collections are the Forms and Controls collections. The Collection object is an example of a one-based collection.
Older collections in Visual Basic are more likely to be zero-based, while more recent additions are more likely to be one-based. One-based collections are somewhat more intuitive to use, because the index ranges from one to Count, where Count is the property that returns the number of items in a collection.
The index of a zero-based collection, by contrast, ranges from zero to one less than the Count property.
Index and Key Values
Many collections in Visual Basic allow you to access an item using either a numeric index or a string key, as the Visual Basic Collection object does. (Visual Basic's Collection object allows you to add items without specifying a key, however.)
The Forms collection, by contrast, allows only a numeric index. This is because there's no unique string value associated with a form. For example, you can have multiple forms with the same caption, or multiple loaded forms with the same Name property
Adding and Removing Items
Collections also differ in whether or not you can add items to them, and if so, how those items are added. You can't add a printer to the Printers collection using Visual Basic code, for example.
Because the Collection object is a general-purpose programming tool, it's more flexible than other collections. It has an Add method you can use to put items into the collection, and a Remove method for taking items out.
By contrast, the only way to get a form into the Forms collection is to load the form. If you create a form with the New operator, or by referring to a variable declared As New, it will not be added to the Forms collection until you use the Load statement to load it.
The Forms and Controls collections don't have Remove methods. You add and remove forms and controls from these collections indirectly, by using the Load and Unload statements.
What Has It Got In Its Pocketses?
As noted above, a form is not added to the Forms collection until it's loaded. Thus the most accurate specification of the Forms collection is that it contains all of the currently loaded forms in the program.
Even that's not completely accurate. If your project uses Microsoft Forms (included for compatibility with Microsoft Office), you'll find those forms in a separate collection named UserForms. So the Forms collection contains all of the currently loaded Visual Basic forms in the program.
The contents of the Collection class are very precisely specified: anything that can be stored in a Variant. Thus the Collection object can contain an object or an integer, but not a user-defined type.
Unfortunately, this specification covers a lot of territory — a given instance of the Collection class could store any mongrel assortment of data types, arrays, and objects.
Tip One of the most important reasons for creating your own collection classes, as discussed in "Creating Your Own Collection Classes," is so you can control the contents of your collections — a concept called type safety. Enumerating a Collection
You can use For Each … Next to enumerate the items in a collection, without worrying about whether the collection is zero-based or one-based. Of course, this is hardly a defining characteristic of collections, because Visual Basic allows you to use For Each … Next to enumerate the items in an array.
What makes For Each … Next work is a tiny object called an enumerator. An enumerator keeps track of where you are in a collection, and returns the next item when it's needed.
When you enumerate an array, Visual Basic creates an array enumerator object on the fly. Collections have their own enumerator objects, which are also created as needed.
Enumerators Don't Skip Items
The enumerators of collections in Visual Basic don't skip items. For example, suppose you enumerate a collection containing "A," "B," and "C," and that while doing so you remove "B." Visual Basic collections will not skip over "C" when you do this.
Enumerators May Not Catch Added Items
If you add items to a collection while enumerating it, some enumerators will include the added items, while some will not. The Forms collection, for example, will not enumerate any forms you load while enumerating.
The Collection object will enumerate items you add while enumerating, if you allow them to be added at the end of the collection. Thus the following loop never ends (until you hit CTRL+BREAK, that is):
Dim col As New Collection
Dim vnt As Variant
col.Add "Endless"
col.Add "Endless"
For Each vnt In col
MsgBox vnt
col.Add "Endless"
Next
On the other hand, items you add at the beginning of the collection will not be included in the enumeration:
Dim col As New Collection
Dim vnt As Variant
col.Add "Will be enumerated"
For Each vnt In col
MsgBox vnt
' Add the item at the beginning.
col.Add "Won't be enumerated", Before:=1
Next
Why Enumerators? By emitting a new enumerator each time a For Each … Next begins, a collection allows nested enumerations. For example, suppose you have a reference to a Collection object in the variable mcolStrings, and that the collection contains only strings. The following code prints all the combinations of two different strings:
Dim vnt1 As Variant
Dim vnt2 As Variant
For Each vnt1 In mcolStrings
For Each vnt2 In mcolStrings
If vnt1 <> vnt2 Then
Debug.Print vnt1 & " " & vnt2
End If
Next
Next
Classes: Putting User-Defined Types and Procedures Together
User-defined types are a powerful tool for grouping related items of data. Consider, for example, the user-defined type named udtAccount defined here:
Public Type udtAccount
Number As Long
Type As Byte
CustomerName As String
Balance As Double
End Type
You can declare a variable of type udtAccount, set the values of its fields individually, and then pass the whole record to procedures that print it, save it to a database, perform computations on it, validate its fields, and so on.
Powerful as they are, user-defined types present the programmer with some problems. You may create a Withdrawal procedure that raises an error if a withdrawal exceeds the balance in the account, but there's nothing to prevent the Balance field from being reduced by other code in your program.
In other words, the connection between procedures and user-defined types depends on the discipline, memory, and knowledge of the programmer maintaining the code.
Objects: User-Defined Types with an Attitude
Object-oriented programming solves this problem by combining data and procedures in a single entity, as shown in Figure.
When the user-defined type udtAccount becomes the Account class, its data become private, and the procedures that access them move inside the class and become properties and methods. This is what's meant by the term encapsulation — that is, an object is a unit (a capsule, if you will) containing both code and data.
When you create an Account object from the class, the only way you can access its data is through the properties and methods that make up its interface. The following code fragment shows how the procedures inside the Account class support encapsulation:
' The account balance is hidden from outside code.
Private mdblBalance As Double
' The read-only Balance property allows outside code
' to find out the account balance.
Public Property Get Balance() As Double
Balance = mdblBalance
End Property
' The Withdrawal method changes the account balance,
' but only if an overdraft error doesn't occur.
Public Sub Withdrawal(ByVal Amount As Double)
If Amount > Balance Then
Err.Raise Number:=vbObjectError + 2081, _
Description:="Overdraft"
End If
mdblBalance = mdblBalance - Amount
End Sub
For the moment, don't worry about how you get the procedures inside the class, or about understanding the syntax of property procedures and private variables. The important thing to remember is that you can define an object that encapsulates and validates its own data.
With the Account object, you never have be concerned about whether you've called the right procedures to update the account, because the only procedures you can call are built into the object.
For More Information "Customizing Form Classes" puts property and method creation into a framework you're already familiar with. Later, "Adding Properties and Methods to a Class" will explain the syntax.
You can read about user-defined types in "Creating Your Own Data Types" in "More About Programming."
For details about Sub and Function procedures, see "Introduction to Procedures" in "Programming Fundamentals."
Customizing Form Classes
It may surprise you to learn that you've been creating classes for as long as you've been programming in Visual Basic. It's true: Form1, that familiar denizen of every project you've ever started, is really · a class.
To see this, open a new Standard Exe project. Add a button to Form1, and place the following code in its Click event:
Private Sub Command1.Click()
Dim f As New Form1
f.Show
End Sub
Press F5 to run the project, and click the button. Holy smokes, there's another instance of Form1! Click its button. There's another! Every instance you create looks the same, and has the same behavior, because they're all instances of the Form1 class.
Public Form1 As New Form1
When you select Form1 as your startup object, or type Form1.Show in code, you're referring to this hidden global object variable. Because it's declared As New, an instance of the Form1 class is created the first time you use this predeclared variable in code.
The reason this declaration is hidden is that Visual Basic changes it every time you change the Name property of a form. In this way, the hidden variable always has the same name as the form class.
The second instance of Form1, and all the ones that followed, had an object variable for just as long as it took to call their Show methods. Then that variable went out of scope, and was set to Nothing. But Visual Basic keeps a special collection named Forms, which you can read about in "More About Forms" in "Creating a User Interface." The Forms collection contains a reference to each of the loaded forms in your project, so that you can always find and control them.
Note As you'll learn, this is not true of all classes. For example, the classes you design won't have hidden global variables or global collections to keep track of them — those are special features of form classes. However, you can declare your own global variables, and you can create your own collections — as described in "Creating Your Own Collection Classes."
Properties, Methods, and Events of Form Classes
The first time you added a property to a form class, you probably did it visually, by dropping a command button (or some other control) on Form1. In doing so, you added a read-only Command1 property to the form class. Thereafter, you invoked this property of Form1 whenever you needed to call a method or property of the command button:
Command1.Caption = "Click Me"
When you changed the Name property of any control on a form, Visual Basic quietly changed the name of the read-only property, so they always matched.
If you still have the project open from the earlier exercise, you can see this Command1 property by pressing F2 to open the Object Browser. In the Project/Library box, select Project1. You'll see Form1 in the Classes pane. In the Members pane, scroll down until you find Command1, and select it.
Command1 has a property symbol beside it, and if you look in the description pane, you'll see that it's a WithEvents property. As you'll learn in "Adding Events to a Class," this means that the property (or object variable) has event procedures associated with it. One of those event procedures, Command1_Click(), may have been the first place you ever wrote Visual Basic code.
But Wait, There's More Dropping controls on a form is not the only way to add new members to the form class. You can add your own custom properties, methods, and events, as easily as you create new variables and procedures.
To see this, add the following code to the Declarations section of Form1:
' The Comment property of the Form1 class.
Public Comment As String
Add the following code to the Click event of Form1:
Private Sub Form_Click()
MsgBox Comment, , "My comment is:"
End Sub
Finally, change the code in the Command1_Click() event procedure by adding a line, as follows:
Private Sub Command1.Click()
Dim f As New Form1
f.Comment = InputBox("What's my comment?")
f.Show
End Sub
Press F5 to run the project. Click Command1, and when the input box appears, type in some racy comment and click OK. When the new instance of Form1 appears, click on it to play back its Comment property.
Click on the first instance of Form1, and notice that its Comment property is blank. Because Visual Basic created this instance as the Startup Object, you never got a chance to set its Comment property.
Forms Can Call Each Other's Methods If you were watching closely, you may have noticed that the code you added to the Form1 class didn't set the object's own Comment property — it set the Comment property of the new instance of Form1 it was creating.
This ability of forms to set each other's properties and call each other's methods is a very useful technique. For example, when an MDIForm is opening a new child window, it can initialize the new window by setting its properties and calling its methods.
You can also use this technique to pass information between forms.
Tip You can create custom events for forms. "Adding an Event to a Form" later in this chapter, provides a step by step procedure
Other Kinds of Modules
You add properties, methods, and events to form classes by putting code in their code modules. In the same way, you can add properties, methods, and events to class modules and — if you have the Professional or Enterprise Edition of Visual Basic — to UserControl and UserDocument code modules.
As you read "Adding Properties and Methods to a Class" and "Adding Events to a Class," remember that everything you read applies to form classes as well as to class modules.
Class Module Step by Step This example shows how you can use class modules to define classes, from which you can then create objects. It will also show you how to create properties and methods for the new class, and demonstrate how objects are created and destroyed.
Open a new Standard Exe project, and insert a class module by selecting Add Class Module from the Project menu. Draw four command buttons on the form. The following table lists the property values you need to set for the objects in this example.
Object Property Setting Class module Name Thing Command1 Caption Show the Thing Command2 Caption Reverse the Thing's Name Command3 Caption Create New Thing Command4 Caption Temporary Thing
Note Class modules are saved in files with the extension .cls. In the class module Declarations section, add the following:
Option Explicit
Public Name As String
Private mdtmCreated As Date
The variable Name will be a property of the Thing object, because it's declared Public.
Note Don't confuse this Name property with the Name property of the class module, which the table above instructed you to set. (The Name property of the class module gives the Thing class its name.) Why would you give the Thing class a Name property? A better question might be, why not? You may want to give the Thing class a Name property because Things should have names! Remember that there's nothing special about the property and method names Visual Basic uses. You can use those same property and method names for your classes. The variable mdtmCreated is a private data member that is used to store the value of the read-only Created property. The Created property returns the date and time a Thing object was created. To implement the Created property, add the following Property Get to the Declarations section of the class module:
Property Get Created() As Date
Created = mdtmCreated
End Property
Note If you added the property procedure using the Add Procedure dialog box, on the Tools menu, be sure to delete the Property Let declaration that is automatically added by this dialog. Property Let is only required for read-write properties, as explained in "Putting Property Procedures to Work for You." The Thing object has one method, ReverseName, which simply reverses the order of the letters in the Name property. It doesn't return a value, so it's implemented as a Sub procedure. Add the following Sub procedure to the class module.
Public Sub ReverseName()
Dim intCt As Integer
Dim strNew As String
For intCt = 1 To Len(Name)
strNew = Mid$(Name, intCt, 1) & strNew
Next
Name = strNew
End Sub
Class modules have two events, Initialize and Terminate. In the Object drop down of the class module, select Class. The Procedure drop down will show the events. Place the following code in the event procedures:
Private Sub Class_Initialize()
' Set date/time of object creation, to be returned
' by the read-only Created property.
mdtmCreated = Now
' Display object properties.
MsgBox "Name: " & Name & vbCrLf & "Created: " _
& Created, , "Thing Initialize"
End Sub
Private Sub Class_Terminate()
' Display object properties.
MsgBox "Name: " & Name & vbCrLf & "Created: " _
& Created, , "Thing Terminate"
End Sub
Usually, the Initialize event procedure contains any code that needs to be executed at the moment the object is created, such as providing the time stamp for the Created property. The Terminate event contains any code you need to execute in order to clean up after the object when it is being destroyed.
In this example, the two events are being used primarily to give you a visual indication that a Thing object is being created or destroyed.
Using the Thing Object
Add this declaration to the Declarations section of the form module: Option Explicit
Private mth As Thing
The variable mth will hold a reference to a Thing object, which will be created in the form's Load event. Put the following code in the Form_Load event procedure, and in the Click event procedures for the four buttons.
Private Sub Form_Load()
Set mth = New Thing
mth.Name = InputBox("Enter a name for the Thing")
End Sub
' Button "Show the Thing"
Private Sub Command1_Click()
MsgBox "Name: " & mth.Name & vbCrLf _
& "Created: " & mth.Created, , "Form Thing"
End Sub
' Button "Reverse the Thing's Name"
Private Sub Command2_Click()
mth.ReverseName
' Click "Show the Thing"
Command1.Value = True
End Sub
' Button "Create New Thing"
Private Sub Command3_Click()
Set mth = New Thing
mth.Name = InputBox( _
"Enter a name for the new Thing")
End Sub
' Button "Temporary Thing".
Private Sub Command4_Click()
Dim thTemp As New Thing
thTemp.Name = InputBox( _
"Enter a name for the Temporary Thing")
End Sub
Running the Project
Press F5 to run the project. Looking at the code in the Form_Load event procedure, you can see that the New operator is used to create a Thing object. A reference to this Thing is assigned to the variable mth.
You will see the InputBox asking you for a name for the Thing. When you type a name and press ENTER, the return value is assigned to the Name property of the Thing object.
Show the Form Thing
You can verify that the Name property has been assigned by pressing the first button, "Show the Thing," which displays a message box with all the properties of the Thing object.
Reverse the Thing's Name Press the second button, "Reverse the Thing's Name." This button calls the ReverseName method to turn the Thing object's name around, and then clicks the first button to display the updated property values.
Create New Thing Click the "Create New Thing" button to destroy the existing Thing object and create a new one. (Or, as it turns out, to create a new Thing and then destroy the old one.)
The New operator causes a new Thing to be created, so you'll see the MsgBox displayed by the new Thing's Initialize event. When you click OK, a reference to the new Thing is placed in the form-level variable mth.
This wipes out the reference to the old Thing. Because there are no more references to it, it's destroyed, and you'll see its Terminate event message box. When you click OK, the InputBox statement requests a name for the new Thing.
Note If you want to destroy the old Thing before creating the new one, you can add the line of code Set mth = Nothing at the beginning of the event procedure. Temporary Thing
The fourth button demonstrates another aspect of object lifetime. When you press it, you'll be prompted for a name for the temporary Thing.
But wait — there isn't a temporary Thing object yet. You haven't seen its Initialize message box. How can you assign it a name?
Because the variable thTemp was declared As New, a Thing object will be created the moment one of its properties or methods is invoked. This will happen when the return value of the InputBox is assigned to the Name property. Type a name and click OK on the InputBox.
You'll now see the Thing Initialize message box, which shows you that the Name property is still blank. When you click OK to dismiss the message box, the value from the InputBox statement is finally assigned to the Name property. That's a lot of activity for one line of code.
Of course, as soon as you've done that, the Click event procedure ends, and the variable thTemp goes out of scope. The object reference for the temporary Thing is released, so you'll see the Thing Terminate message box. Notice that it contains the name you supplied.
Each time you click this button, another temporary Thing will be created, named, and destroyed.
Closing the Program Close the program by clicking the form's close button. Do not use the End button on the toolbar. When the program closes, Form1 is destroyed. The variable mth goes out of scope, and Visual Basic cleans up the reference to the Thing. There are no remaining references to the Thing, so it's destroyed, and its Terminate event message box is displayed.
Run the program again, and this time end it using the End button on the toolbar. Notice that the Terminate message box for the Thing object is not displayed.
It's important to remember that ending your program with the End button, or with an End statement in your code, halts the program immediately, without executing the Terminate events of any objects. It's always better to shut down your program by unloading all the forms.
You may find it useful to run the example by pressing F8 to step through the code one line at a time. This is a good way to understand the order of events for object creation and destruction.
Important In an actual application, the Initialize and Terminate events should not contain message boxes, or any other code that allows Windows messages to be processed. In general, it's better to use Debug.Print statements when debugging object lifetimes.
Debugging Class Modules
programs. This is because an error in a property or method of a class module always acts like a handled error. (That is, there's always a procedure on the call stack that can handle the error — namely the procedure that called the class module's property or method.)
Visual Basic compensates for this difference by providing the error-trapping option Break in Class Module, in addition to the older options Break on Unhandled Errors and Break on All Errors.
Note You can set the Default Error Trapping State on the General tab of the Options dialog box, available from the Tools menu. The option you select affects the current session, and becomes the default for all subsequent instances of Visual Basic. To change the setting only for the current session, without affecting the default, select Toggle from the Code window context menu (which is available by right-clicking on the Code window).
For example, suppose the class module Class1 contains the following code:
Public Sub Oops()
Dim intOops As Integer
intOops = intOops / 0
End Sub
Now suppose a procedure in another class module, form, or standard module calls the member Oops:
Private Sub Command1_Click()
Dim c1 As New Class1
c1.Oops
End Sub
If the error trapping option is set to Break on Unhandled Errors, execution will not stop on the zero divide. Instead, the error will be raised in the calling procedure, Command1_Click. Execution will stop on the call to the Oops method.
You could use Break on All Errors to stop in the zero divide, but Break on All Errors is a very inconvenient option for most purposes. It stops on every error, even errors for which you've written error handling code.
Break in Class Module is a compromise setting:
Execution will not stop on class module code for which you've written an error handler.
Execution only stops on an error that's unhandled in the class module, and therefore would be returned to the caller of the method.
When the Visual Basic development environment is started, it defaults to Break in Class Module.
If there are no class modules involved, Break in Class Module is exactly the same as Break on Unhandled Errors.
Tip When you hit a break point using Break in Class Module or Break on All Errors, you can step or run past the error — into your error handling code or into the code that called procedure in which the error occurred — by pressing ALT+F8 or ALT+F5.
Life Cycle of Visual Basic Forms
Because they're visible to the user, forms and controls have a different life cycle than other objects. For example, a form will not close just because you've released all your references to it. Visual Basic maintains a global collection of all forms in your project, and only removes a form from that collection when you unload the form.
In similar fashion, Visual Basic maintains a collection of controls on each form. You can load and unload controls from control arrays, but simply releasing all references to a control is not sufficient to destroy it.
States a Visual Basic Form Passes Through
A Visual Basic form normally passes through four states in its lifetime:
Created, but not loaded
Loaded, but not shown
Shown.
Memory and resources completely reclaimed.
There's a fifth state a form can get into under certain circumstances: Unloaded and unreferenced while a control is still referenced.
This topic describes these states, and the transitions between them.
Created, But Not Loaded
The beginning of this state is marked by the Initialize event. Code you place in the Form_Initialize event procedure is therefore the first code that gets executed when a form is created.
In this state, the form exists as an object, but it has no window. None of its controls exist yet. A form always passes through this state, although its stay there may be brief.
For example, if you execute Form1.Show, the form will be created, and Form_Initialize will execute; as soon as Form_Initialize is complete, the form will be loaded, which is the next state.
The same thing happens if you specify a form as your Startup Object, on the General tab of the Project Properties dialog box (which is available from the Project menu). A form specified as the Startup Object is created as soon as the project starts, and is then immediately loaded and shown.
Note You can cause your form to load from within Form_Initialize, by calling its Show method or by invoking its built-in properties and methods, as described below.
Remaining Created, But Not Loaded
By contrast, the following code creates an instance of Form1 without advancing the form to the loaded state:
Dim frm As Form1
Set frm = New Form1
Once Form_Initialize has ended, the only procedures you can execute without forcing the form to load are Sub, Function, and Property procedures you've added to the form's code window. For example, you might add the following method to Form1:
Public Sub ANewMethod()
Debug.Print "Executing ANewMethod"
End Sub
You could call this method using the variable frm (that is, frm.ANewMethod) without forcing the form on to the next state. In similar fashion, you could call ANewMethod in order to create the form:
Dim frm As New Form1
frm.ANewMethod
Because frm is declared As New, the form is not created until the first time the variable is used in code — in this case, when ANewMethod is invoked. After the code above is executed, the form remains created, but not loaded.
Note Executing Form1.ANewMethod, without declaring a form variable, has the same effect as the example above. As explained in "Customizing Form Classes," Visual Basic creates a hidden global variable for each form class. This variable has the same name as the class; it's as though Visual Basic had declared Public Form1 As New Form1.
You can execute as many custom properties and methods as you like without forcing the form to load. However, the moment you access one of the form's built-in properties, or any control on the form, the form enters the next state.
Note You may find it helpful to think of a form as having two parts, a code part and a visual part. Before the form is loaded, only the code part is in memory. You can call as many procedures as you like in the code part without loading the visual part of the form.
The Only State All Forms Pass Through
Created, But Not Loaded is the only state all forms pass through. If the variable frm in the examples above is set to Nothing, as shown here, the form will be destroyed before entering the next state:
Dim frm As New Form1
frm.ANewMethod
Set frm = Nothing ' Form is destroyed.
A form used in this fashion is no better than a class module, so the vast majority of forms pass on to the next state.
Loaded, But Not Shown
The event that marks the beginning of this state is the familiar Load event. Code you place in the Form_Load event procedure is executed as soon as the form enters the loaded state.
When the Form_Load event procedure begins, the controls on the form have all been created and loaded, and the form has a window — complete with window handle (hWnd) and device context (hDC) — although that window has not yet been shown.
Any form that becomes visible must first be loaded.
Many forms pass automatically from the Created, But Not Loaded state into the Loaded, but Not Shown state. A form will be loaded automatically if:
The form has been specified as the Startup Object, on the General tab of the Project Properties dialog box.
The Show method is the first property or method of the form to be invoked, as for example Form1.Show.
The first property or method of the form to be invoked is one of the form's built-in members, as for example the Move method.
Note This case includes any controls on the form, because each control defines a property of the form; that is, in order to access the Caption property of Command1, you must go through the form's Command1 property: Command1.Caption.
The Load statement is used to load the form, without first using New or As New to create the form, as described earlier.
In the first two cases listed above, the form will continue directly on to the visible state, as soon as Form_Load completes. In the last two cases, the form will remain loaded, but not shown.
It has long been common coding practice in Visual Basic to load a form but never show it. This might be done for several reasons:
To use the Timer control to generate timed events.
To use controls for their functionality, rather than their user interface — for example, for serial communications or access to the file system.
To execute DDE transactions.
Note With the Professional or Enterprise edition, you can create ActiveX components (formerly called OLE servers), which are often better at providing code-only functionality than controls are. See Creating ActiveX Components in the Component Tools Guide.
Always Coming Home
Forms return from the visible state to the loaded state whenever they're hidden. Returning to the loaded state does not re-execute the Load event, however. Form_Load is executed only once in a form's life.
Shown
Once a form becomes visible, the user can interact with it. Thereafter, the form may be hidden and shown as many times as you like before finally being unloaded.
Unloaded and Unreferenced, But a Control Is Still Referenced
To get into this odd state, you have to unload and free the form while keeping a reference to one of its controls. If this sounds like a silly thing to do, rest assured that it is.
Dim frm As New Form1
Dim obj As Object
frm.Show vbModal
' When the modal form is dismissed, save a
' reference to one of its controls.
Set obj = frm.Command1
Unload frm
Set frm = Nothing
The form has been unloaded, and all references to it released. However, you still have a reference to one of its controls, and this will keep the code part of the form from releasing the memory it's using. If you invoke any of the properties or methods of this control, the form will be reloaded:
obj.Caption = "Back to life"
The values in module-level variables will still be preserved, but the property values of all the controls will be set back to their defaults, as if the form were being loaded for the first time. Form_Load will execute.
Note In some previous versions of Visual Basic, the form did not completely re-initialize, and Form_Load did not execute again.
Note Not all forms behave as Visual Basic forms do. For example, the Microsoft Forms provided in Microsoft Office don't have Load and Unload events; when these forms receive their Initialize events, all their controls exist and are ready to use.
Class Modules vs. Standard Modules
Classes differ from standard modules in the way their data is stored. There's never more than one copy of a standard module's data. This means that when one part of your program changes a public variable in a standard module, and another part of your program subsequently reads that variable, it will get the same value.
Class module data, on the other hand, exists separately for each instance of the class (that is, for each object created from the class).
By the same token, data in a standard module has program scope — that is, it exists for the life of your program — while class module data for each instance of a class exists only for the lifetime of the object; it's created when the object is created, and destroyed when the object is destroyed.
Finally, variables declared Public in a standard module are visible from anywhere in your project, whereas Public variables in a class module can only be accessed if you have an object variable containing a reference to a particular instance of a class.
All of the above are also true for public procedures in standard modules and class modules. This is illustrated by the following example. You can run this code by opening a new Standard Exe project and using the Project menu to add a module and a class module.
Place the following code in Class1:
' The following is a property of Class1 objects.
Public Comment As String
' The following is a method of Class1 objects.
Public Sub ShowComment()
MsgBox Comment, , gstrVisibleEverywhere
End Sub
Place the following code in Module1:
' Code in the standard module is global.
Public gstrVisibleEverywhere As String
Public Sub CallableAnywhere(ByVal c1 As Class1)
' The following line changes a global variable
' (property) of an instance of Class1. Only the
' particular object passed to this procedure is
' affected.
c1.Comment = "Touched by a global function."
End Sub
Put two command buttons on Form1, and add the following code to Form1:
Private mc1First As Class1
Private mc1Second As Class1
Private Sub Form_Load()
' Create two instances of Class1.
Set mc1First = New Class1
Set mc1Second = New Class1
gstrVisibleEverywhere = "Global string data"
End Sub
Private Sub Command1_Click()
Call CallableAnywhere(mc1First)
mc1First.ShowComment
End Sub
Private Sub Command2_Click()
mc1Second.ShowComment
End Sub
Press F5 to run the project. When Form1 is loaded, it creates two instances of Class1, each having its own data. Form1 also sets the value of the global variable gstrVisibleEverywhere.
Press Command1, which calls the global procedure and passes a reference to the first Class1 object. The global procedure sets the Comment property, and Command1 then calls the ShowComment method to display the object's data.
As Figure shows, the resulting message box demonstrates that the global procedure CallableAnywhere set the Comment property of the object that was passed to it, and that the global string is visible from within Class1.
Figure Message box from the first Class1 object
Important Avoid making the code in your classes dependent on global data — that is, public variables in standard modules. Many instances of a class can exist simultaneously, and all of these objects share the global data in your program.
Using global variables in class module code also violates the object-oriented programming concept of encapsulation, because objects created from such a class do not contain all their data.
Static Class Data
There may be occasions when you want a single data item to be shared among all objects created from a class module. This is sometimes referred to as static class data.
You cannot implement true static class data in a Visual Basic class module. However, you can simulate it by using Property procedures to set and return the value of a Public data member in a standard module, as in the following code fragment:
' Read-only property returning the application name.
Property Get CommonString() As String
' The variable gstrVisibleEverywhere is stored in a
' standard module, and declared Public.
CommonString = gstrVisibleEverywhere
End Property
Note You cannot use the Static keyword for module-level variables in a class module. The Static keyword can only be used within procedures.
It's possible to simulate static class data that's not read-only by providing a corresponding Property Let procedure — or Property Set for a property that contains an object reference — to assign a new value to the standard module data member. Using global variables in this fashion violates the concept of encapsulation, however, and is not recommended.
For example, the variable gstrVisibleEverywhere can be set from anywhere in your project, even from code that doesn't belong to the class that has the CommonString property. This can lead to subtle errors in your program.
Adding Properties to a Class
The easiest way to define properties for a class is by adding public variables to the class module. For example, you could very easily create an Account class by declaring two public variables in a class module named Account:
Public Balance As Double
Public Name As String
This is pretty easy. It's just as easy to create private data for a class; simply declare a variable Private, and it will be accessible only from code within the class module:
Private mstrMothersMaidenName As String
Private mintWithdrawalsMonthToDate As Integer
Data Hiding
The ability to protect part of an object's data, while exposing the rest as properties, is called data hiding. This is one aspect of the object-oriented principle of encapsulation, as explained in "Classes: Putting User-Defined Types and Procedures Together."
Data hiding means that you can make changes in the implementation of a class — for example, increasing the Account class's private variable mintWithdrawalsMonthToDate from an Integer to a Long — without affecting existing code that uses the Account object.
Data hiding also allows you to define properties that are read-only. For example, you could use a Property Get procedure to return the value of the private variable containing the number of withdrawals in a month, while only incrementing the variable from within the Account object's code. Which brings us to property procedures.
Property Procedures
Data hiding wouldn't be much use if the only way you could create properties was by declaring public variables. How much good would it do you to give the Account class a Type property, if any code that had a reference to an Account object could blithely set the account type to any value at all? Property procedures allow you to execute code when a property value is set or retrieved. For example, you might want to implement the Type property of the Account object as a pair of Property procedures:
Public Enum AccountTypes
atSavings = 1
atChecking
atLineOfCredit
End Enum
' Private data storage for the Type property.
Private mbytType As AccountTypes
Public Property Get Type() As AccountTypes
Type = mbytType
End Property
Public Property Let Type(ByVal NewType As AccountTypes)
Select Case NewType
Case atChecking, atSavings, atLineOfCredit
' No need to do anything if NewType is valid.
Case Else
Err.Raise Number:=vbObjectError + 32112, _
Description:="Invalid account type"
End Select
If mbytType > NewType Then
Err.Raise Number:=vbObjectError + 32113, _
Description:="Cannot downgrade account type"
Else
mbytType = NewType
End If
End Property
Now suppose you have a variable named acct that contains a reference to an Account object. When the code x = acct.Type is executed, the Property Get procedure is invoked to return the value stored in the class module's private data member mbytType.
When the code acct.Type = atChecking is executed, the Property Let is invoked. If the Account object is brand new, mbytType will be zero, and any valid account type can be assigned. If the current account type is atSavings, the account will be upgraded.
However, if the current account type is atLineOfCredit, the Property Let will raise an error, preventing the downgrade. Likewise, if the code acct.Type = 0 is executed, the Select statement in the Property Let will detect the invalid account type and raise an error.
In short, property procedures allow an object to protect and validate its own data.
Property Procedures vs. Public Variables
Property procedures are clearly such a powerful means for enabling encapsulation that you may be wondering if you should even bother with public variables. The answer, as always in programming, is "Of course — sometimes." Here are some ground rules: Use property procedures when:
- The property is read-only, or cannot be changed once it has been set.
- The property has a well-defined set of values that need to be validated.
- Values outside a certain range — for example, negative numbers — are valid for the property's data type, but cause program errors if the property is allowed to assume such values.
- Setting the property causes some perceptible change in the object's state, as for example a Visible property.
- Setting the property causes changes to other internal variables or to the values of other properties.
- The property is of a self-validating type. For example, an error or automatic data conversion will occur if a value other than True or False is assigned to a Boolean variable.
- Any value in the range supported by the data type is valid. This will be true of many properties of type Single or Double.
- The property is a String data type, and there's no constraint on the size or value of the string.
Putting Property Procedures to Work for You
Visual Basic provides three kinds of property procedures, as described in the following table.
Procedure
Purpose Property Get Returns the value of a property Property Let Sets the value of a property Property Set Sets the value of an object property (that is, a property that contains a reference to an object).
As you can see from the table, each of these property procedures has a particular role to play in defining a property. The typical property will be made up of a pair of property procedures: A Property Get to retrieve the property value, and a Property Let or Property Set to assign a new value. These roles can overlap in some cases. The reason there are two kinds of property procedures for assigning a value is that Visual Basic has a special syntax for assigning object references to object variables:
Dim wdg As Widget
Set wdg = New Widget
The rule is simple: Visual Basic calls Property Set if the Set statement is used, and Property Let if it is not.
Tip To keep Property Let and Property Set straight, hearken back to the Basics of yore, when instead of x = 4 you had to type Let x = 4 (syntax supported by Visual Basic to this very day). Visual Basic always calls the property procedure that corresponds to the type of assignment — Property Let for Let x = 4, and Property Set for Set c1 = New Class1 (that is, object properties).
Read-Write Properties
The following code fragment shows a typical read-write property:
' Private storage for property value.
Private mintNumberOfTeeth As Integer
Public Property Get NumberOfTeeth() As Integer
NumberOfTeeth = mintNumberOfTeeth
End Property
Public Property Let NumberOfTeeth(ByVal NewValue _
As Integer)
' (Code to validate property value omitted.)
mintNumberOfTeeth = NewValue
End Property
The name of the private variable that stores the property value is made up of a scope prefix (m) that identifies it as a module-level variable; a type prefix (int); and a name (NumberOfTeeth). Using the same name as the property serves as a reminder that the variable and the property are related.
As you've no doubt noticed, here and in earlier examples, the names of the property procedures that make up a read-write property must be the same.
Note Property procedures are public by default, so if you omit the Public keyword, they will still be public. If for some reason you want a property to be private (that is, accessible only from within the object), you must declare it with the Private keyword. It's good practice to use the Public keyword, even though it isn't required, because it makes your intentions clear.
Property Procedures at Work and Play
It's instructive to step through some property procedure code. Open a new Standard Exe project and add a class module, using the Project menu. Copy the code for the NumberOfTeeth property, shown above, into Class1.
Switch to Form1, and add the following code to the Load event:
Private Sub Form_Load()
Dim c1 As Class1
Set c1 = New Class1
' Assign a new property value.
c1.NumberOfTeeth = 42
' Display the property value.
MsgBox c1.NumberOfTeeth
End Sub
Press F8 to step through the code one line at a time. Notice that when the property value is assigned, you step into the Property Let, and when it's retrieved, you step into the Property Get. You may find it useful to duplicate this exercise with other combinations of property procedures.
Arguments of Paired Property Procedures Must Match
The property procedure examples you've seen so far have been simple, as they will be for most properties. However, property procedures can have multiple arguments — and even optional arguments. Multiple arguments are useful for properties that act like arrays, as discussed below.
When you use multiple arguments, the arguments of a pair of property procedures must match. The following table demonstrates the requirements for arguments in property procedure
Procedure Declaration syntax Property Get Property Get propertyname(1,..., n) As type Property Let Property Let propertyname(1,..., n, n+1) Property Set Property Set propertyname(1,..., n, n+1) The first argument through the second-to-last argument (1,..., n) must share the same names and data types in all Property procedures with the same name. As with other procedure types, all of the required parameters in this list must precede the first optional parameter.
You've probably noticed that a Property Get procedure declaration takes one less argument than the related Property Let or Property Set. The data type of the Property Get procedure must be the same as the data type of the last argument (n+1) in the related Property Let or Property Set.
For example, consider this Property Let declaration, for a property that acts like a two-dimensional array:
Public Property Let Things(ByVal X As Integer, _
ByVal Y As Integer, ByVal Thing As Variant)
' (Code to assign array element omitted.)
End Property
The Property Get declaration must use arguments with the same name and data type as the arguments in the Property Let procedure:
Public Property Get Things(ByVal X As Integer, _
ByVal Y As Integer) As Variant
' (Code for retrieval from array omitted.)
End Property
The data type of the final argument in a Property Set declaration must be either an object type or a Variant.
Matching Up the Arguments
The reason for these argument matching rules is illustrated in Figure , which shows how Visual Basic matches up the parts of the assignment statement with the arguments of a Property Let.
Figure Calling a Property Let procedure
The most common use for property procedures with multiple arguments is to create property arrays
Read-Only Properties
To create a read-only property, simply omit the Property Let or (for object properties) the Property Set.
Object Properties
If you're creating a read-write object property, you use a Property Get and a Property Set, as here:
Private mwdgWidget As Widget
Public Property Get Widget() As Widget
' The Set statement must be used to return an
' object reference.
Set Widget = mwdgWidget
End Property
Public Property Set Widget(ByVal NewWidget As Widget)
Set mwdgWidget = NewWidget
End Property
Variant Properties
Read-write properties of the Variant data type are the most complicated. They use all three property procedure types, as shown here:
Private mvntAnything As Variant
Public Property Get Anything() As Variant
' The Set statement is used only when the Anything
' property contains an object reference.
If IsObject(mvntAnything) Then
Set Anything = mvntAnything
Else
Anything = mvntAnything
End If
End Property
Public Property Let Anything(ByVal NewValue As Variant)
' (Validation code omitted.)
mvntAnything = NewWidget
End Property
Public Property Set Anything(ByVal NewValue As Variant)
' (Validation code omitted.)
Set mvntAnything = NewWidget
End Property
The Property Set and Property Let are straightforward, as they're always called in the correct circumstances. However, the Property Get must handle both of the following cases:
strSomeString = objvar1.Anything
Set objvar2 = objvar1.Anything
In the first case, the Anything property contains a string, which is being assigned to a String variable. In the second, Anything contains an object reference, which is being assigned to an object variable.
The Property Get can be coded to handle these cases, by using the IsObject function to test the private Variant before returning the value.
Of course, if the first line of code is called when Anything contains an object reference, an error will occur, but that's not Property Get's problem — that's a problem with using Variant properties.
Write-Once Properties
There are many possible combinations of property procedures. All of them are valid, but some are relatively uncommon, like write-only properties (only a Property Let, no Property Get). And some depend on factors other than the kinds of property procedures you combine.
For example, when you organize the objects in your program by creating an object model, as described in "Object Models" later in this chapter, you may want an object to be able to refer back to the object that contains it. You can do this by implementing a Parent property.
You need to set this Parent property when the object is created, but thereafter you may want to prevent it from being changed — accidentally or on purpose. The following example shows how the Account object might implement a Parent property that points to the Department object that contains the account.
' Private data storage for Parent property.
Private mdeptParent As Department
Property Get Parent() As Department
' Use the Set statement for object references.
Set Parent = mdeptParent
End Property
' The property value can only be set once.
Public Property Set Parent(ByVal NewParent _
As Department)
If deptParent Is Nothing Then
' Assign the initial value.
Set mdeptParent = NewParent
Else
Err.Raise Number:=vbObjectError + 32144, _
Description:="Parent property is read-only"
End If
End Property
When you access the parent of an Account object, for example by coding strX = acctNew.Parent.Name to get the department name, the Property Get is invoked to return the reference to the parent object.
The Property Set in this example is coded so that the Parent property can be set only once. For example, when the Department object creates a new account, it might execute the code Set acctNew.Parent = Me to set the property. Thereafter the property is read-only.
Adding Methods to a Class The methods of a class are just the public Sub or Function procedures you've declared. Since Sub and Function procedures are public by default, you don't even have to explicitly specify the Public keyword to create a method.
For example, to create a Withdrawal method for the Account class, you could add this Public Function procedure to the class module:
Public Function WithDrawal(ByVal Amount As Currency, _
ByVal TransactionCode As Byte) As Double
' (Code to perform the withdrawal and return the
' new balance, or to raise an Overdraft error.)
End Function
Tip Although you don't have to type the Public keyword, doing so is good programming practice, because it makes your intent clear to people maintaining your code later.
Declaring Methods as Public Subs
Returning the new balance is optional, since you could easily call the Balance property of the Account object after calling the Withdrawal method. You could thus code Withdrawal as a Public Sub procedure.
Tip If you find yourself calling Balance almost every time you call Withdrawal, returning the new balance will be slightly more efficient. This is because, as noted in "Adding Properties to Class Modules," any property access, even reading a public variable, means a function call — an explicit or implicit Property Get.
Important The following names cannot be used as property or method names, because they belong to the underlying IUnknown and IDispatch interfaces: QueryInterface, AddRef, Release, GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, and Invoke. These names will cause a compilation error.
Protecting Implementation Details
Even more important, you can also change the code inside the public Sub or Function procedure that implements a method, without affecting code that uses the method. As long as you don't change the data types of the procedure's arguments, or the type of data returned by a Function procedure, the interface is unchanged.
Hiding the details of an object's implementation behind the interface is another facet of encapsulation. Encapsulation allows you to enhance the performance of methods, or completely change the way a method is implemented, without having to change code that uses the method.
Note The guidelines for naming interface elements — discussed in "Naming Properties, Methods, and Events" — apply not only to property and method names, but to the names of parameters in the Sub and Function procedures that define your methods. These parameter names are visible when you view the methods in the Object Browser, and can be used as named parameters (that is, parametername:=value) when the methods are invoked. Is It a Property or a Method?
In general, a property is data about an object, while a method is an action the object can be asked to perform. Some things are obviously properties, like Color and Name, and some are obviously methods, like Move and Show.
As with any facet of human endeavor, however, there's a gray area in which an argument can be made either way.
For example, why is the Item method of the Visual Basic Collection class a method and not an indexed property? Aren't the items in the collection just data? The Item method of a hypothetical Widgets collection class could be implemented either way, as shown here:
' Private storage for the objects in the Widgets
' collection (same for both implementations).
Private mcol As New Collection
Public Property Get Item(Index As Variant) As Widget
Set Item = mcol.Item(Index)
End Function
- or -
Public Function Item(Index As Variant) As Widget
Set Item = mcol.Item(Index)
End Function
There's not a whole lot of difference between these two implementations. Both are read-only, so both depend on the Add method of the Widgets class to get Widget objects into the collection. Both delegate everything to a Collection object — even their errors are generated by the Collection!
The Syntax Argument
Set Widgets.Item(4) = wdgMyNewWidget
If so, implement the member as a read-write property, using Property Get and Property Set, because methods don't support this syntax.
Note In most collection implementations you encounter, this syntax is not allowed. Implementing a Property Set for a collection is not as easy as it looks.
The Property Window Argument
You can also suppose for a moment that your object is like a control. Can you imagine the member showing up in the Property window, or on a property page? If that doesn't make sense, don't implement the member as a property.
The Sensible Error Argument
If you forget that you made Item a read-only property and try to assign a value to it, you'll most likely find it easier to understand the error message Visual Basic raises for a Property Get — "Can't assign to read-only property" — than the error message it raises for a Function procedure — "Function call on left-hand side of assignment must return Variant or Object."
The Argument of Last Resort As a last resort, flip a coin. If none of the other arguments in this topic seem compelling, it probably doesn't make much difference.
Making a Property or Method the Default
You can give objects created from your classes default properties, like the default properties of objects provided by Visual Basic. The best candidate for default member is the one you use most often.
To set a property or method as the default
- On the Tools menu, select Procedure Attributes to open the Procedure Attributes dialog box.
- Click Advanced to expand the Procedure Attributes dialog box.
- In the Name box, select the property or method that is currently the default for the class. If the class does not currently have a default member, skip to step 5.
Note You can use the Object Browser to find out what the current default member of a class is. When you select the class in the Classes list, you can scroll through the members in the Members list; the default member will be marked with a small blue globe beside its icon. - In the Procedure ID box, select None to remove the default status of the property or method.
- In the Name box, select the property or method you want to be the new default.
- In the Procedure ID box, select (Default), then click OK.
You can also open the Procedure Attributes dialog box from the Object Browser. This is convenient when you're changing the default member of a class, because it allows you to locate the existing default member quickly.
To change a default property using the Object Browser
- Press F2 to open the Object Browser.
- In the Classes list, select the class whose default you want to change.
- In the Members list, right-click the member with the small blue globe beside its icon to open the context menu. Click Properties to show the Property Attributes dialog box.
- Click Advanced to expand the Procedure Attributes dialog box.
- In the Procedure ID box, select None to remove the default status of the property or method, then click OK.
- In the Members list, right-click the member you want to be the new default to open the context menu. Click Properties to show the Property Attributes dialog box.
- Click Advanced to expand the Procedure Attributes dialog box.
- In the Procedure ID box, select (Default), then click OK.
Fixing Defaults You Have Accidentally Made Private or Friend
The Procedure Attributes dialog box only allows you to select public properties and methods as the default for a class. If you make a public property or method the default for a class, and later change the declaration to Private or Friend, the property or method may continue to behave as if it were still declared Public.
To correct this problem, you must make the property or method Public again, because the Procedure Attributes dialog box will not show procedures declared Private and Friend. Once you have changed the declaration to Public, you can use the Procedure Attributes dialog to remove the Default attribute. You can then change the declaration back to Friend or Private
Friend Properties and Methods.
In addition to declaring properties and methods Public and Private, you can declare them Friend. Friend members look just like Public members to other objects in your project. That is, they appear to be part of a class's interface. They are not.
In the ActiveX components you can create with the Professional and Enterprise editions of Visual Basic, Friend members play an important role. Because they're not part of an object's interface, they can't be accessed by programs that use the component's objects. They're visible to all the other objects within the component, however, so they allow safe internal communication within the component.
Important Because Friend members aren't part of an object's public interface, they can't be accessed late bound — that is, through variables declared As Object. To use Friend members, you must declare variables with early binding — that is, As classname.
Standard Exe projects can't be ActiveX components, because their class modules can't be Public, and thus can't be used by other applications. All communication between objects in a Standard Exe project is therefore private, and there's no need for Friend members.
However, Friend members have one particularly useful feature. Because they're not part of an ActiveX interface, they can be used to pass user-defined types between objects without exposing them publicly. For example, suppose you have the following user-defined type in a standard module:
Public Type udtDemo intA As Integer
lngB As Long
strC As String
End Type
You can define the following private variable and Friend members in Class1:
Private mDemo As udtDemo
Friend Property Get Demo() As udtDemo
Demo = mDemo
End Property
' Note that udtDemo must be passed by reference.
Friend Property Let Demo(NewDemo As udtDemo)
mDemo = NewDemo
End Property
Friend Sub SetDemoParts(ByVal A As Integer, _
ByVal B As Long, ByVal C As String)
mDemo.intA = A
mDemo.lngB = B
mDemo.strC = C
End Sub
Public Sub ShowDemo()
MsgBox mDemo.intA & vbCrLf _
& mDemo.lngB & vbCrLf & mDemo.strC
End Sub
Note When you pass user-defined types as Sub, Function, or property procedure arguments, you must pass them by reference. (ByRef is the default for procedure arguments.) You can then write the following code to use Class1:
Private Sub Command1_Click()
Dim c1A As New Class1
Dim c1B As New Class1
c1A.SetDemoParts 42, 1138, "Howdy"
c1B.Demo = c1A.Demo
c1B.ShowDemo
End Sub
The message box will display 42, 1138, and "Howdy."
Note Because Friend procedures are not part of a class's interface, they are not included when you use the Implements statement to implement multiple interfaces, as described in "Polymorphism."
Adding Events to a Class
Okay, let's say you've created a dinosaur simulation, complete with Stegosaur, Triceratops, and Tyrannosaur classes. As the final touch, you want the Tyrannosaur to roar, and when it does you want every other dinosaur in your simulation to sit up and take notice.
If the Tyrannosaur class had a Roar event, you could handle that event in all your other dinosaur classes. This topic discusses the declaration and handling of events in your class modules.
Note Kids, don't try this at home, at least with more than a few dinosaurs. Connecting every dinosaur with every other dinosaur using events could make your dinosaurs so slow that mammal objects would take over the simulation. Declaring and Raising Events
Assume for the moment that you have a Widget class. Your Widget class has a method that can take a long time to execute, and you'd like your application to be able to put up some kind of completion indicator.
Of course, you could make the Widget object show a percent-complete dialog box, but then you'd be stuck with that dialog box in every project in which you used the Widget class. A good principle of object design is to let the application that uses an object handle the user interface — unless the whole purpose of the object is to manage a form or dialog box.
The Widget's purpose is to perform other tasks, so it's reasonable to give it a PercentDone event, and to let the procedure that calls the Widget's methods handle that event. The PercentDone event can also provide a mechanism for canceling the task.
You can start building the code example for this topic by opening a Standard Exe project, and adding two buttons and a label to Form1. On the Project menu, select Add Class Module to add a class module to the project. Name the objects as shown in the following table.
Object Property Setting Class module Name Widget First Button Caption Start Task Second Button Caption Cancel Label Name
Caption lblPercentDone
"0"
The Widget Class
You declare an event in the Declarations section of a class module, using the Event keyword. An event can have ByVal and ByRef arguments, as the Widget's PercentDone event demonstrates:
Option Explicit
Public Event PercentDone(ByVal Percent As Single, _
ByRef Cancel As Boolean)
When the calling object receives a PercentDone event, the Percent argument contains the percentage of the task that's complete. The ByRef Cancel argument can be set to True to cancel the method that raised the event.
Note You can declare event arguments just as you do arguments of procedures, with the following exceptions: Events cannot have named arguments, optional arguments, or ParamArray arguments. Events do not have return values. Raising the PercentDone Event
The PercentDone event is raised by the LongTask method of the Widget class. The LongTask method takes two arguments: the length of time the method will pretend to be doing work, and the minimum time interval before LongTask pauses to raise the PercentDone event.
Public Sub LongTask(ByVal Duration As Single, _
ByVal MinimumInterval As Single)
Dim sngThreshold As Single
Dim sngStart As Single
Dim blnCancel As Boolean
' The Timer function returns the fractional number
' of seconds since Midnight, as a Single.
sngStart = Timer
sngThreshold = MinimumInterval
Do While Timer < (sngStart + Duration)
' In a real application, some unit of work would
' be done here each time through the loop.
If Timer > (sngStart + sngThreshold) Then
RaiseEvent PercentDone( _
sngThreshold / Duration, blnCancel)
' Check to see if the operation was canceled.
If blnCancel Then Exit Sub
sngThreshold = sngThreshold + MinimumInterval
End If
Loop
End Sub
Every MinimumInterval seconds, the PercentDone event is raised. When the event returns, LongTask checks to see if the Cancel argument was set to True.
Note For simplicity, LongTask assumes you know in advance how long the task will take. This is almost never the case. Dividing tasks into chunks of even size can be difficult, and often what matters most to users is simply the amount of time that passes before they get an indication that something is happening.
Handling an Object's Events
An object that raises events is called an event source. To handle the events raised by an event source, you can declare a variable of the object's class using the WithEvents keyword.An object that raises events is called an event source. To handle the events raised by an event source, you can declare a variable of the object's class using the WithEvents keyword.
This topic continues the Widget object example begun in "Declaring and Raising Events." To handle the PercentDone event of a Widget, place the following code in the Declarations section of Form1:
Option Explicit
Private WithEvents mWidget As Widget
Private mblnCancel As Boolean
The WithEvents keyword specifies that the variable mWidget will be used to handle an object's events. You specify the kind of object by supplying the name of the class from which the object will be created.
The variable mWidget is declared in the Declarations section of Form1 because WithEvents variables must be module-level variables. This is true regardless of the type of module you place them in.
The variable mblnCancel will be used to cancel the LongTask method.
Limitations on WithEvents Variables
You should be aware of the following limitations on the use of WithEvents variables:
- A WithEvents variable cannot be a generic object variable. That is, you cannot declare it As Object — you must specify the class name when you declare the variable.
- You cannot declare a WithEvents variable As New. The event source object must be explicitly created and assigned to the WithEvents variable.
- You cannot declare WithEvents variables in a standard module. You can declare them only in class modules, form modules, and other modules that define classes.
- You cannot create arrays of WithEvents variables.
As soon as you declare a variable WithEvents, the variable name appears in the left-hand drop down of the module's code window. When you select mWidget, the Widget class's events will appear in the right-hand drop down, as shown in Figure
Figure An event associated with a WithEvents variable
Selecting an event will display the corresponding event procedure, with the prefix mWidget_. All the event procedures associated with a WithEvents variable will have the variable name as a prefix. Add the following code to the mWidget_PercentDone event procedure.
Private Sub mWidget_PercentDone(ByVal Percent As _
Single, Cancel As Boolean)
lblPercentDone.Caption = CInt(100 * Percent) & "%"
DoEvents
If mblnCancel Then Cancel = True
End Sub
Whenever the PercentDone event is raised, the event procedure displays the percent complete in a Label control. The DoEvents statement allows the label to repaint, and also gives the user the opportunity to click the Cancel button. Add the following code for the Click event of the button whose caption is Cancel. Private Sub Command2_Click()
mblnCancel = True
End Sub
If the user clicks the Cancel button while LongTask is running, the Command2_Click event will be executed as soon as the DoEvents statement allows event processing to occur. The module-level variable mblnCancel is set to True, and the mWidget_PercentDone event then tests it and sets the ByRef Cancel argument to True. Connecting a WithEvents Variable to an Object
Form1 is all set up to handle a Widget object's events. All that remains is to find a Widget somewhere.
When you declare a variable WithEvents at design time, there is no object associated with it. A WithEvents variable is just like any other object variable. You have to create an object and assign a reference to the object to the WithEvents variable. Add the following code to the Form_Load event procedure to create the Widget.
Private Sub Form_Load()
Set mWidget = New Widget
End Sub
When the code above is executed, Visual Basic creates a Widget and connects its events to the event procedures associated with mWidget. From that point on, whenever the Widget raises its PercentDone event, the mWidget_PercentDone event procedure will be executed.
To call the LongTask method, add the following code to the Click event of the button whose caption is Start Task.
' Start Task button.
Private Sub Command1_Click()
mblnCancel = False
lblPercentDone.Caption = "0%"
lblPercentDone.Refresh
Call mWidget.LongTask(14.4, 0.66)
If Not mblnCancel Then lblPercentDone.Caption = 100
End Sub
Before the LongTask method is called, the label that displays the percent complete must be initialized, and the module-level Boolean flag for canceling the method must be set to False.
LongTask is called with a task duration of 14.4 seconds. The PercentDone event is to be raised once every two-thirds of a second. Each time the event is raised, the mWidget_PercentDone event procedure will be executed.
When LongTask is done, mblnCancel is tested to see if LongTask ended normally, or if it stopped because mblnCancel was set to True. The percent complete is updated only for the former case.
Running the Program
Press F5 to put the project in Run mode. Click the Start Task button. Each time the PercentDone event is raised, the label is updated with the percentage of the task that's complete. Click the Cancel button to stop the task. Notice that the appearance of the Cancel button doesn't change immediately when you click it. The Click event can't happen until the DoEvents statement allows event processing.
You may find it instructive to run the program with F8, and step through the code a line at a time. You can clearly see how execution enters LongTask, and then re-enters Form1 briefly each time the PercentDone event is raised.
What would happen if, while execution was back in Form1's code, the LongTask method was called again? Confusion, chaos, and eventually (if it happened every time the event was raised) a stack overflow.
Handling Events for a Different Widget You can cause the variable mWidget to handle events for a different Widget object by assigning a reference to the new Widget to mWidget. In fact, you can make the code in Command1 do this every time you click the button, by adding two lines of code:
Set mWidget = New Widget '<- New line.
Call mWidget.LongTask(14.4, 0.66)
Set mWidget = Nothing '<- New line.
The code above creates a new Widget each time the button is pressed. As soon as the LongTask method completes, the reference to the Widget is released by setting mWidget to Nothing, and the Widget is destroyed.
A WithEvents variable can only contain one object reference at a time, so if you assign a different Widget object to mWidget, the previous Widget object's events will no longer be handled. If mWidget is the only object variable containing a reference to the old Widget, the object will be destroyed.
Note You can declare as many WithEvents variables as you need, but arrays of WithEvents variables are not supported.
Terminating Event Handling for a WithEvents Variable
As long as there is a Widget object assigned to the variable mWidget, the event procedures associated with mWidget will be called whenever the Widget raises an event. To terminate event handling, you can set mWidget to Nothing, as shown in the following code fragment.
' Terminate event handling for mWidget.
Set mWidget = Nothing
When a WithEvents variable is set to Nothing, Visual Basic disconnects the object's events from the event procedures associated with the variable.
Important A WithEvents variable contains an object reference, just like any other object variable. This object reference counts toward keeping the object alive. When you are setting all references to an object to Nothing in order to destroy it, don't forget the variables you declared WithEvents.
Comparing WithEvents to Control Events on Forms
You've probably noticed some similarities between the way you use WithEvents variables and the way you handle the events raised by controls on a form. In both cases, when you select the event in the right-hand drop down of a code window, you get an event procedure containing the correct arguments for the event.
In fact, the mechanism is exactly the same. A control is treated as a property of the form class, and the name of that property is the value you assigned to the control's Name property in the Properties window.
It's as if there's a Public module-level variable with the same name as the control, and all of the control's event procedure names begin with that variable name, just as they would with a WithEvents variable.
You can easily see this by declaring the variable mWidget Public instead of Private. The moment you do this, mWidget will show up in the Object Browser as a property of Form1, just like the controls on the form.
The difference between the two cases is that Visual Basic automatically creates instances of all the controls on a form when the form is created, whereas you have to create your own instances of classes whose events you want to handle, and assign references to those objects to WithEvents variables.
Adding an Event to a Form
The following step by step procedure shows how you can create custom events for forms. To try this exercise, open a new Standard Exe project and do the following:
To add an event to Form1
- On the Project menu, select Add Class Module to add a class module to the project. Place the following code in the Declarations section of Class1:
Public Property Get Form1() As Form1
Set Form1 = mForm1
End Property Public Property Set Form1(ByVal NewForm1 As Form1)
Set mForm1 = NewForm1
End Property
If you're using Procedure View, the property procedures can't be viewed at the same time. Click the Full Module View button at the bottom left corner of the code window to switch to Full Module View. You can return to Procedure View by clicking the Procedure View button next to it. (Hover the mouse over the buttons to see which is which.)
- Add the following code to the Declarations section of Form1:
Event Gong
Private mc1 As Class1 Now that Class1 has been created, it's possible to create a variable of type Class1. This procedure switches between Form1 and Class1 several times, because a step in one module requires first adding code to the other.
- Go back to Class1 and add the following code to the Declarations section.
Private WithEvents mForm1 As Form1 As discussed in "Adding Events to a Class," the WithEvents keyword means this instance of Form1 is associated with events. Note that this step wasn't possible until the Gong event had been created.
- In the left-hand (Object) drop down on Class1's Code window, select mForm1 to get the event procedure for the Gong event. Add the following code to the event procedure:
Private Sub mForm1_Gong()
MsgBox "Gong!"
End Sub - Go back to Form1. In the Object drop down, select Form. In the right-hand (Procedure) drop down, select Load. Add the following code to the event procedure:
Private Sub Form_Load()
Set mc1 = New Class1
Set mc1.Form1 = Me
End Sub The first line creates a Class1 object, and the second assigns to its Form1 property (created in step 1) a reference to Form1 (that is, Me — when you're in Form1's Code window, Me refers to Form1; when you're in Class1's Code window, Me refers to Class1).
- Put three text boxes on Form1. Use the Object and Procedure drop downs to select the Change event procedure for each control in turn, and place the same line of code in each:
Private Sub Text1_Change()
RaiseEvent Gong
End Sub Each time the contents of a text box change, the form's Gong event will be raised.
- Press F5 to run the project. Each time you type a character in one of the text boxes, the message box rings a bell. It's very annoying, but it shows how you can add an event to a form, and thus get notifications from several controls.
To add an event to a class and then use the event, you must:
- In the Declarations section of the class module that defines the class, use the Event statement to declare the event with whatever arguments you want it to have. Events are always Public.
Note Events cannot have named arguments, optional arguments, or ParamArray arguments. Events do not have return values. - At appropriate places in the class module's code, use the RaiseEvent statement to raise the event, supplying the necessary arguments.
- In the Declarations section of the module that will handle the event, add a variable of the class type, using the WithEvents keyword. This must be a module-level variable.
- In the left-hand drop down of the code window, select the variable you declared WithEvents.
- In the right-hand drop down, select the event you wish to handle. (You can declare multiple events for a class.)
- Add code to the event procedure, using the supplied arguments.
If you’ve read the preceding material on creating classes, you know by now that a class is an object that encapsulates data and code, and that the properties of a class are the data that describe an object. You also know that you can use property procedures or public properties to expose the data represented by those properties.
So far, so good — all of the examples thus far have dealt with transient data, that is, data that is created and consumed at run time. For many programs, this may be all that you need, but what if you need to store data between sessions, or utilize data that already exists outside of your program? In order to work with external sources of data, you need to make your class data-aware.
Data-aware classes can be divided into two categories — data consumers and data sources. Class modules have two design-time properties, DataBindingBehavior and DataSourceBehavior, that determine how a class will interact with external data. The BindingCollection object is used to bind data-aware classes to controls or to each other.
Data Sources
A data source is a class that provides data from an external source to be consumed by other objects. A Data control is in reality an instance of a class that is a data source, but classes that have been set up to act as data sources can be much more powerful than a Data control. Unlike the Data control, a data-aware class doesn’t have to have a visual representation, and it isn’t limited to a particular data interface such as Data Access Objects (DAO) or Remote Data Objects (RDO). In fact, a data-aware class can act as a data source for any type of data, including traditional ODBC sources, ActiveX Data Objects (ADO), or any OLE DB provider. The DataSourceBehavior property determines whether or not a class can act as a data source. By setting the DataSourceBehavior to 1 (vbDataSource), your class can act as a source of data for other objects.
Data Consumers
Simply put, a data consumer is a class that can be bound to an external source of data, much as a TextBox control can be bound to a Data control. In earlier versions of Visual Basic, controls were the only objects that could be bound to a data source. Data-aware classes set up as data consumers allow you to bind any object to any object created from a class that has been set up as a data source.
The DataBindingBehavior property allows a class to bind to external data. By setting this property to 1 (vbSimpleBound), an object created from your class will be bound to a single data field in an external data source. By setting the DataBindingBehavior to 2 (vbComplexBound), your class will be bound to a row of data in an external data source. Think of it this way — if your objects were controls, a TextBox control would be simple bound, whereas a grid control would be complex bound.
The BindingCollection Object Just as you would bind a control to a database through a Data control, data-aware classes need a central object to bind them together. That object is the BindingCollection object. Just as it sounds, the BindingCollection is a collection of bindings between a data source and one or more data consumers.
In order to use the BindingCollection object you must first add a reference to the Microsoft Data Binding Collection by selecting it in the References dialog, available from the Project menu. As with any object, you’ll need to create an instance of the BindingCollection object at run time.
The DataSource property of the BindingCollection object is used to specify the object that will provide the data. This object must be a class or UserControl with its DataSourceBehavior property set to vbDataSource.
Once the BindingCollection has been instantiated and its DataSource set, you can use the Add method to define the binding relationships. The Add method takes three required arguments: the name of the consumer object, the property of that object to be bound to the source, and the field from the source that will be bound to the property. You can add multiple bindings to the BindingCollection by repeating the Add method; you can use the Remove method to delete a binding.
Creating a Data Source In this section, we’ll walk step-by-step through the process of creating a data-aware class that acts as a data source. This example will bind a TextBox control to our data source class in order to display the data. The next section, "Creating a Data Consumer," demonstrates how to bind our data source class to a data consumer class.
Creating the Source Class The first step in creating a source class is to define a new class and give it the properties and methods necessary to provide data:
- Open a new Standard EXE project, and insert a class module by selecting Add Class Module from the Project menu.
- In the Properties window, set the properties of the class as follows:
Property Setting Name MySource DataSourceBehavior vbDataSource
When DataSourceBehavior is set to vbDataSource, a new Sub procedure GetDataMember is added to the class module. You can see this by selecting Class from the Object list in the code editor, then selecting the Event list. - Select References from the Project menu, and add a reference to the Microsoft ActiveX Data Objects 2.0 Library.
- Add the following to the Declarations section of the class module:
Option Explicit
Private rs As ADODB.Recordset This declares an object variable for the ADO Recordset object.
- Add the following code to the class module’s Initialize event procedure:
Private Sub Class_Initialize()
Dim strPath As String, strName As String
Dim i As Integer ' Create an instance of the Recordset.
Set rs = New ADODB.Recordset
' Set the properties of the Recordset.
With rs
.Fields.Append "DirID", adInteger
.Fields.Append "Directory", adVarChar, 255
.CursorType = adOpenStatic
.LockType = adLockOptimistic
.Open
End With
' Loop through the directories and populate
' the Recordset.
strPath = "C:\"
strName = Dir(strPath, vbDirectory)
i = 0
Do While strName <> ""
If strName <> "." And strName <> ".." Then
If (GetAttr(strPath & strName) And _
vbDirectory) = vbDirectory Then
i = i + 1
With rs
.AddNew
.Fields.Item("DirID") = i
.Fields.Item("Directory") = strName
.Update
End With
End If
End If
strName = Dir
Loop
' Return to the first record.
rs.MoveFirst
End Sub
In this example we’re creating a ADO Recordset object on the fly and populating it with a list of directories. Alternatively, you could use an existing recordset by assigning to the Connect property of the ADO Recordset in the Initialize event.
- Select Class from the Object list in the Code Editor, then select GetDataMember from the Event list. Add the following code to the GetDataMember Sub procedure:
Private Sub Class_GetDataMember(DataMember As String, Data As Object)
' Assign the Recordset to the Data object.
Set Data = rs
End Sub The GetDataMember procedure sets the source of the data for the class. Your data source class can provide multiple data sources by adding a Select Case statement to the GetDataMember procedure and passing in a source name in the DataMember argument.
- Add a new Sub procedure to provide a public method to loop through the Recordset:
Public Sub Cycle()
' Cycle through the Recordset.
rs.MoveNext
If rs.EOF = True Then
rs.MoveFirst
End If
End Sub
Using the Source Class
Now that the source class is defined, we can do something useful with it. In this example we’ll bind it to a TextBox control so that we can see its output; we’ll also use a CommandButton to execute our Cycle method.
- Select Form1 and add a TextBox control and a CommandButton control to the form.
- In the Properties window, set the properties of the TextBox as follows
Property Setting Name txtConsumer Text (blank) - In the Properties window, set the properties of the CommandButton as follows:
Property Setting Name cmdCycle Caption Cycle - Select References from the Project menu, and add a reference to the Microsoft Data Binding Collection.
The DataBinding object provided by the Data Binding Collection is the "glue" that binds a data source to a data consumer. - Add the following to the Declarations section of the class module:
Option Explicit
Private objSource As MySource
Private objBindingCollection As BindingCollection We need to declare our source class (MySource) and the BindingCollection object using early binding.
- Add the following code to the Form_Load event procedure:
Private Sub Form_Load()
Set objSource = New MySource
Set objBindingCollection = New BindingCollection ' Assign the source class to the Binding
' Collection’s DataSource property.
Set objBindingCollection.DataSource = objSource
' Add a binding.
ObjBindingCollection.Add txtConsumer, "Text", "Directory"
In the Load event we create instances of the source class and the BindingCollection object, then we assign the source object to the DataSource property of the BindingCollection. Finally, we add a binding by specifying the name of the consumer (txtConsumer), the Property of the consumer to be bound (the Text property), and the Field property of the source object that we are binding to (Directory).
- Add the following code to the cmdCycle Click event procedure:
Private cmdCycle_Click()
' Call the Cycle method of the data source.
ObjSource.Cycle
End Sub This will execute the Cycle method of our source class.
- Press F5 to run the project
As you click the Cycle button, directory names from the recordset created in our source class will appear in the TextBox. Congratulations — you’ve just bound a control to a data source class without using a Data control! - Save the project. When prompted for filenames, use the following names.
Save the source class as "MySource.cls". Save the form as "Dataform.frm".
Save the project as "Dataware.vbp".
These files will be used later in "Creating a Data Consumer."
Polymorphism
Polymorphism and Performance
Polymorphism is important for performance reasons. To see this, consider the following function:
Public Sub GetFood(ByVal Critter As Object, _
ByVal Food As Object)
Dim dblDistance As Double
' Code to calculate distance to food (omitted).
Critter.Move dblDistance ' Late bound
Critter.Bite Food ' Late bound
End Sub
The Move and Bite methods are late bound to Critter. Late binding happens when Visual Basic can't determine at compile time what kind of object a variable will contain. In this example, the Critter argument is declared As Object, so at run time it could contain a reference to any kind of object — like a Car or a Rock.
Because it can't tell what the object will be, Visual Basic compiles some extra code to ask the object if it supports the method you've called. If the object supports the method, this extra code invokes it; if not, the extra code raises an error. Every method or property call incurs this additional overhead.
By contrast, interfaces allow early binding. When Visual Basic knows at compile time what interface is being called, it can check the type library to see if that interface supports the method. Visual Basic can then compile in a direct jump to the method, using a virtual function table (vtable). This is many times faster than late binding.
Now suppose the Move and Bite methods belong to an Animal interface, and that all animal classes implement this interface. The Critter argument can now be declared As Animal, and the Move and Bite methods will be early bound:
Public Sub GetFood(ByVal Critter As Animal, _
ByVal Food As Object)
Dim dblDistance As Double
' Code to calculate distance to food (omitted).
Critter.Move dblDistance ' Early bound (vtable).
Critter.Bite Food ' Early bound (vtable).
End Sub
Creating and Implementing an Interface
As explained in "How Visual Basic Provides Polymorphism," an interface is a set of properties and methods. In the following code example, you'll create an Animal interface and implement it in two classes, Flea and Tyrannosaur.
You can create the Animal interface by adding a class module to your project, naming it Animal, and inserting the following code:
Public Sub Move(ByVal Distance As Double)
End Sub
Public Sub Bite(ByVal What As Object)
End Sub
Notice that there's no code in these methods. Animal is an abstract class, containing no implementation code. An abstract class isn't meant for creating objects — its purpose is to provide the template for an interface you add to other classes. (Although, as it turns out, sometimes it's useful to implement the interface of a class that isn't abstract; this is discussed later in this topic.)
Note Properly speaking, an abstract class is one from which you can't create objects. You can always create objects from Visual Basic classes, even if they contain no code; thus they are not truly abstract. Now you can add two more class modules, naming one of them Flea and the other Tyrannosaur. To implement the Animal interface in the Flea class, you use the Implements statement:
Option Explicit
Implements Animal
As soon as you've added this line of code, you can click the left-hand (Object) drop down in the code window. One of the entries will be Animal. When you select it, the right-hand (Procedure) drop down will show the methods of the Animal interface.
Select each method in turn, to create empty procedure templates for all the methods. The templates will have the correct arguments and data types, as defined in the Animal class. Each procedure name will have the prefix Animal_ to identify the interface
Important An interface is like a contract. By implementing the interface, a class agrees to respond when any property or method of the interface is invoked. Therefore, you must implement all the properties and methods of an interface.
You can now add the following code to the Flea class:
Private Sub Animal_Move(ByVal Distance As Double)
' (Code to jump some number of inches omitted.)
Debug.Print "Flea moved"
End Sub
Private Sub Animal_Bite(ByVal What As Object)
' (Code to suck blood omitted.)
Debug.Print "Flea bit a " & TypeName(What)
End Sub
You may be wondering why the procedures are declared Private. If they were Public, the procedures Animal_Jump and Animal_Bite would be part of the Flea interface, and we'd be stuck in the same bind we were in originally, declaring the Critter argument As Object so it could contain either a Flea or a Tyrannosaur.
Multiple Interfaces The Flea class now has two interfaces: The Animal interface you've just implemented, which has two members, and the default Flea interface, which has no members. Later in this example you'll add a member to one of the default interfaces.
You can implement the Animal interface similarly for the Tyrannosaur class:
Option Explicit
Implements Animal
Private Sub Animal_Move(ByVal Distance As Double)
' (Code to pounce some number of yards omitted.)
Debug.Print "Tyrannosaur moved"
End Sub
Private Sub Animal_Bite(ByVal What As Object)
' (Code to take a pound of flesh omitted.)
Debug.Print "Tyrannosaur bit a " & TypeName(What)
End Sub
Exercising the Tyrannosaur and the Flea
Add the following code to the Load event of Form1:
Private Sub Form_Load()
Dim fl As Flea
Dim ty As Tyrannosaur
Dim anim As Animal
Set fl = New Flea
Set ty = New Tyrannosaur
' First give the Flea a shot.
Set anim = fl
Call anim.Bite(ty) 'Flea bites dinosaur.
' Now the Tyrannosaur gets a turn.
Set anim = ty
Call anim.Bite(fl) 'Dinosaur bites flea.
End Sub
Press F8 to step through the code. Notice the messages in the Immediate window. When the variable anim contains a reference to the Flea, the Flea's implementation of Bite is invoked, and likewise for the Tyrannosaur.
The variable anim can contain a reference to any object that implements the Animal interface. In fact, it can only contain references to such objects. If you attempt to assign a Form or PictureBox object to anim, an error will occur.
The Bite method is early bound when you call it through anim, because Visual Basic knows at compile time that whatever object is assigned to anim will have a Bite method.
Passing Tyrannosaurs and Fleas to Procedures Remember the GetFood procedure from "How Visual Basic Provides Polymorphism?" You can add the second version of the GetFood procedure — the one that illustrates polymorphism — to Form1, and replace the code in the Load event with the following:
Private Sub Form_Load()
Dim fl As Flea
Dim ty As Tyrannosaur
Set fl = New Flea
Set ty = New Tyrannosaur
'Flea dines on dinosaur.
Call GetFood(fl, ty)
' And vice versa.
Call GetFood(ty, fl)
End Sub
Stepping through this code shows how an object reference that you pass to an argument of another interface type is converted into a reference to the second interface (in this case, Animal). What happens is that Visual Basic queries the object to find out whether it supports the second interface. If the object does, it returns a reference to the interface, and Visual Basic places that reference in the argument variable. If the object does not support the second interface, an error occurs. Implementing Methods That Return Values
Suppose the Move method returned a value. After all, you know how far you want an Animal to move, but an individual specimen might not be able to move that far. It might be old and decrepit, or there might be a wall in the way. The return value of the Move method could be used to tell you how far the Animal actually moved.
Public Function Move(ByVal Distance As Double) _
As Double
End Function
When you implement this method in the Tyrannosaur class, you assign the return value to the procedure name, just as you would for any other Function procedure:
Private Function Animal_Move(ByVal Distance _
As Double) As Double
Dim dblDistanceMoved As Double
' Code to calculate how far to pounce (based on
' age, state of health, and obstacles) is omitted.
' This example assumes that the result has been
' placed in the variable dblDistanceMoved.
Debug.Print "Tyrannosaur moved"; dblDistanceMoved
Animal_Move = dblDistanceMoved
End Function
To assign the return value, use the full procedure name, including the interface prefix.
Implementing Properties
This topic continues the code example begun in "Creating and Implementing an Interface," adding properties to the Animal interface that was implemented in the Flea and Tyrannosaur classes. You may find it helpful to read that topic before beginning this one.
Suppose we give the Animal class an Age property, by adding a Public variable to the Declarations section:
Option Explicit
Public Age As Double
The Procedure drop downs in the code modules for the Tyrannosaur and Flea classes now contain property procedures for implementing the Age property, as shown in Figure
Figure:Implementing property procedures
This illustrates a point made in "Adding Properties to a Class" earlier in this chapter. Using a public variable to implement a property is strictly a convenience for the programmer. Behind the scenes, Visual Basic implements the property as a pair of property procedures.
You must implement both procedures. The property procedures are easily implemented by storing the value in a private data member, as shown here:
Private mdblAge As Double
Private Property Get Animal_Age() As Double
Animal_Age = mdblAge
End Property
Private Property Let Animal_Age(ByVal RHS As Double)
mdblAge = RHS
End Property
The private data member is an implementation detail, so you have to add it yourself.
Note When Implements provides the template for a Property Set or Property Let, it has no way of determining the name of the last argument, so it substitutes the name RHS, as shown in the code example above. There's no data validation on a property implemented as a public data member, but that doesn't mean you can't add validation code to the Property Let for Animal_Age. For example, you might want to restrict the values to ages appropriate for a Tyrannosaur or a Flea, respectively.
In fact, this shows the independence of interface and implementation. As long as the interface matches the description in the type library, the implementation can be anything.
Before you go on to the next step, remove the implementation of the read-write Age property from both class modules.
Implementing a Read-Only Property
Of course, allowing the age of an animal to be set arbitrarily is bad object design. The object should know its own age, and provide it to the user as a read-only property. Remove the public variable Age from the Animal class, and add the template for a read-only age property, like this:
Public Property Get Age() As Double
End Property
Now the Procedure drop downs in the code windows for the Tyrannosaur and Flea classes contain only a single entry, Age [PropertyGet]. You might implement this for the Tyrannosaur as follows:
Private mdblBirth As Double
Private Property Get Animal_Age() As Double
Animal_Age = Now - mdblBirth
End Property
The code above returns the age of the Tyrannosaur in days. You could set mdblBirth in the Initialize event of the Tyrannosaur class, as here:
Private Sub Class_Initialize()
mdblBirth = Now
End Sub
And of course you could return the property value in more commonly used units, such as dog years.
Time Out for a Brief Discussion of Objects and Interfaces
This topic completes the code example begun in "Creating and Implementing an Interface," and continued in "Implementing Properties." You may find it helpful to read those topics before beginning this one.
The Tyrannosaur and Flea code example seems to play fast and loose with interfaces and objects. References to objects are assigned to one object variable, and references to interfaces to another.
In fact, all of the references are object references. A reference to an interface is also a reference to the object that implements the interface. Furthermore, an object may have multiple interfaces, but it's still the same object underneath.
In Visual Basic, each class has a default interface that has the same name as the class. Well, almost the same. By convention, an underscore is prefixed to the class name. The underscore indicates that this interface is hidden in the type library.
Thus the Tyrannosaur class has a default interface called _Tyrannosaur. Because Tyrannosaur also implements Animal, the class has a second interface named Animal.
However, underneath it all, the object is still a Tyrannosaur. Place a command button on Form1, and add the following code:
Private Sub Command1_Click()
Dim ty As Tyrannosaur
Dim anim As Animal
Set ty = New Tyrannosaur
Set anim = ty
MsgBox TypeName(anim)
End Sub
You might expect the message box to display "Animal," but in fact it displays "Tyrannosaur."
Querying for Interfaces
When you assign a Tyrannosaur object to variable of type Animal, Visual Basic asks the Tyrannosaur object if it supports the Animal interface. (The method used for this is called QueryInterface, or QI for short; you may sometimes hear QI used as a verb.) If the answer is no, an error occurs.
If the answer is yes, the object is assigned to the variable. Only the methods and properties of the Animal interface can be accessed through this variable.
Generic Object Variables and Interfaces What happens if you assign the object reference to a generic object variable, as in the following code?
Private Sub Command1_Click()
Dim ty As Tyrannosaur
Dim anim As Animal
Dim obj As Object
Set ty = New Tyrannosaur
Set anim = ty
Set obj = anim
MsgBox TypeName(obj)
End Sub
The result is again Tyrannosaur. Now, what interface do you get when you call properties and methods through the variable obj? Add the following method to the Tyrannosaur class:
Public Sub Growl()
Debug.Print "Rrrrrr"
End Sub
The Growl method belongs to the Tyrannosaur object's default interface. In the code for the command button's Click event, replace the MsgBox statement with the following two lines of code:
obj.Move 42
obj.Growl
When you run the project and click the button, execution stops on the Growl method, with the error "Object does not support this property or method." Clearly, the interface is still Animal.
This is something to bear in mind when using variables of type Object with objects that have multiple interfaces. The interface the variable will access is the last interface assigned. For example:
Private Sub Command1_Click()
Dim ty As Tyrannosaur
Dim anim As Animal
Dim obj As Object
Set ty = New Tyrannosaur
Set anim = ty
Set obj = anim
obj.Move 42 ' Succeeds
obj.Growl ' Fails
Set obj = ty
obj.Move 42 ' Fails
obj.Growl ' Succeeds
End Sub
Fortunately, there's very little reason to use the slower, late-bound Object data type with objects that have multiple interfaces. One of the main reasons for using multiple interfaces is to gain the advantage of early binding through polymorphism.
Other Sources of Interfaces Visual Basic class modules are not your only source of interfaces to implement. You can implement any interface contained in a type library, as long as that interface supports Automation.
If you have the Professional or Enterprise Edition of Visual Basic, you can create your own type libraries of abstract classes. These type libraries can be used in many projects, as described in "General Principles of Component Design" in Creating ActiveX Components in the Component Tools Guide.
The Professional and Enterprise editions also include the MkTypLib (Make Type Library) utility in the Tools directory. If you've used this utility with Microsoft Visual C++, you may find it a more congenial way to create interfaces.
Using Interfaces in Your Project To use an interface in your project, click References on the Project menu to open the References dialog box. If the type library is registered, it will appear in the list of references, and you can check it. If the type library is not in the list, you can use the Browse button to locate it.
Once you have a reference to a type library, you can use Implements to implement any Automation interfaces the type library contains.
The Many (Inter)Faces of Code Reuse There are two main forms of code reuse — binary and source. Binary code reuse is accomplished by creating and using an object, while source code reuse is achieved by inheritance, which isn't supported by Visual Basic. (Source code reuse can also be achieved by copying and modifying the source code, but this technique is nothing new, and has many well-known problems.)
Visual Basic has been a pioneer of binary code reuse — controls being the classic example. You reuse the code in a control by placing an instance of the control on your form. This is known as a containment relationship or a has-a relationship; that is, the form contains or has a CommandButton.
Delegating to an Implemented Object Implements provides a powerful new means of code reuse. You can implement an abstract class (as discussed in "Creating and Implementing an Interface"), or you can implement the interface of a fully functional class. You can create the inner object (that is, the implemented object) in the Initialize event of the outer object (that is, the one that implements the inner object's interface).
As noted in "Creating and Implementing an Interface," an interface is like a contract — you must implement all the members of the inner object's interface in the outer object's class module. However, you can be very selective in the way you delegate to the properties and methods of the inner object. In one method you might delegate directly to the inner object, passing the arguments unchanged, while in another method you might execute some code of your own before calling the inner object — and in a third method you might execute only your own code, ignoring the inner object altogether!
For example, suppose you have a OneManBand class and a Cacophony class, both of which generate sounds. You'd like to add the functionality of the Cacophony class to the OneManBand class, and reuse some of the implementation of the Cacophony class's methods.
' OneManBand implements the Cacophony interface.
Implements Cacophony
' Object variable to keep the reference in.
Private mcac As Cacophony
Private Sub Class_Initialize()
' Create the object.
Set mcac = New Cacophony
End Sub
You can now go to the Object drop down and select Cacophony, and then get procedure templates for the methods of the Cacophony interface. To implement these methods, you can delegate to the Cacophony object. For example, the Beep method might look like this:
Private Sub Cacophony_Beep(ByVal Frequency As Double, _
ByVal Duration As Double)
' Delegate to the inner Cacophony object.
Call mcac.Beep(Frequency, Duration)
End Sub
The implementation above is very simple. The outer object (OneManBand) delegates directly to the inner (Cacophony), reusing the Cacophony object's Beep method without any changes. This is a good thing, but it's only the beginning.
The Implements statement is a very powerful tool for code reuse, because it gives you enormous flexibility. You might decide to alter the effects of the OneManBand class's Beep method, by inserting your own code before (or after) the call to the inner Cacophony object:
Private Sub Cacophony_Beep(ByVal Frequency As Double, _
ByVal Duration As Double)
' Bump everything up an octave.
Frequency = Frequency * 2
' Based on another property of the OneManBand
' class, Staccato, cut the duration of each beep.
If Staccato Then Duration = Duration * 7 / 8
Call mcac.Beep(Frequency, Duration)
' You can even call other methods of OneManBand.
If Staccato Then Pause(Duration * 1 / 8)
End Sub
For some of the methods, your implementation may delegate directly to the inner Cacophony object, while for others you may interpose your own code before and after delegating — or even omit delegation altogether, using entirely your own code to implement a method.
Because the OneManBand class implements the Cacophony interface, you can use it with any musical application that calls that interface. Your implementation details are hidden from the calling application, but the resulting sounds are all your own.
Note COM provides another mechanism for binary code reuse, called aggregation. In aggregation, an entire interface is reused, without any changes, and the implementation is provided by an instance of the class being aggregated. Visual Basic does not support this form of code reuse.
Doesn't This Get Tedious? Writing delegation code can indeed become tedious, especially if most of the outer object's properties and methods simply delegate directly to the corresponding properties and methods of the inner object.
If you have the Professional or Enterprise Edition of Visual Basic, you can use the Visual Basic Extensibility model to create your own delegation wizard to automate the task, similar to the Class Wizard that's included in the Professional and Enterprise editions.
Object Models
The simplest way to keep track of objects is to declare an object variable for each object you plan to create. Of course, this places a limit on the number of objects you can create.
You can keep multiple object references in an array or a collection, as discussed in "Creating Arrays of Objects" and "Creating Collections of Objects" earlier in this chapter.
In the beginning, you'll probably locate object variables, arrays, and collections in forms or standard modules, as you do with ordinary variables. As you add more classes, though, you'll probably discover that the objects you're using have clear relationships to each other.
Object Models Express Containment Relationships Object models give structure to an object-based program. By defining the relationships between the objects you use in your program, an object model organizes your objects in a way that makes programming easier.
Typically, an object model expresses the fact that some objects are "bigger," or more important than others — these objects can be thought of as containing other objects, or as being made up of other objects.
For example, you might create a SmallBusiness object as the core of your program. You might want the SmallBusiness object to have other types of objects associated with it, such as Employee objects and Customer objects. You would probably also want it to contain a Product object. An object model for this program is shown in Figure
Figure :An object model
You can define four class modules, named SmallBusiness, Employee, Customer, and Product, and give them each appropriate properties and methods, but how do you make the connections between objects? You have two tools for this purpose: Object properties and the Collection object. The following code fragment shows one way to implement the hierarchy in Figure 9.11.
' Code for the Declarations section of the
' SmallBusiness class module.
Public Name As String
Public Product As New Product
Public Employees As New Collection
Public Customers As New Collection
The first time you refer to the Product property, the object will be created, because it was declared As New. For example, the following code might create and set the name and price of the SmallBusiness object's Product object.
' Code for a standard module.
Public sbMain As New SmallBusiness
Sub Main
sbMain.Name = "Velociraptor Enterprises, Inc."
' The first time the Product variable is used in
' code, the Product object is created.
sbMain.Product.Name = "Inflatable Velociraptor"
sbMain.Product.Price = 1.98
.
. ' Code to initialize and show main form.
.
End Sub
Note Implementing an object property with public variables is sloppy. You could inadvertently destroy the Product object by setting the property to Nothing somewhere in your code. It's better to create object properties as read-only properties, as shown in the following code fragment.
' Code for a more robust object property. Storage for
' the property is private, so it can't be set to
' Nothing from outside the object.
Private mProduct As New Product
Property Get Product() As Product
' The first time this property is called, mProduct
' contains Nothing, so Visual Basic will create a
' Product object.
Set Product = mProduct
End If
One-to-Many Object Relationships Object properties work well when the relationship between objects is one-to-one. It frequently happens, however, that an object of one type contains a number of objects of another type. In the SmallBusiness object model, the Employees property is implemented as a Collection object, so that the SmallBusiness object can contain multiple Employee objects. The following code fragment shows how new Employee objects might be added to this collection.
Public Function NewEmployee(Name, Salary, HireDate, _
ID) As Employee
Dim empNew As New Employee
empNew.Name = Name ' Implicit object creation.
empNew.Salary = Salary
empNew.HireDate = HireDate
' Add to the collection, using the ID as a key.
sbMain.Employees.Add empNew, CStr(ID)
' Return a reference to the new Employee.
Set NewEmployee = empNew
End Function
The NewEmployee function can be called as many times as necessary to create employees for the business represented by the SmallBusiness object. The existing employees can be listed at any time by iterating over the Employees collection.
Note Once again, this is not a very robust implementation. Better practice is to create your own collection classes, and expose them as read-only properties. This is discussed in "Creating Your Own Collection Classes." Tip The Class Builder utility, included in the Professional and Enterprise editions of Visual Basic, can generate much of the code you need to implement an object model. Class Builder creates robust object properties and collection classes, and allows you to rearrange your model easily.
Parent Properties
When you have a reference to an object, you can get to the objects it contains by using its object properties and collections. It's also very useful to be able to navigate up the hierarchy, to get to the object that contains the object you have a reference to.
Navigating upward is usually done with Parent properties. The Parent property returns a reference to the object's container. For a discussion of object model navigation, see "Navigating Object Models" in "Programming with Components."
You can find an example of a Parent property in "Adding Properties to Classes" earlier in this chapter.
Tip When you assign a Parent property to an object in a collection, don't use a reference to the Collection object. The real parent of the object is the object that contains the collection. If the Parent property points to the collection, you'll have to use two levels of indirection to get to the real parent — that is, obj.Parent.Parent instead of obj.Parent. Parent Properties, Circular References, and Object Teardown
One of the biggest problems with Parent properties is that they create circular references. The "larger" object has a reference to the object it contains, and the contained object has a reference through its Parent property, creating a loop as shown in Figure
What's wrong with this picture? The way you get rid of objects when you're done with them is to release all references to them. Assuming the reference to the SmallBusiness object is in a variable named sbMain, as earlier in this topic, you might write the following code:
Set sbMain = Nothing
Unfortunately, there's still a reference to the SmallBusiness object — in fact, there may be many references, because each Employee object's Parent property will hold a reference to the SmallBusiness object.
Since the SmallBusiness object's Employees collection holds a reference to each Employee object, none of the objects ever get destroyed.
TearDown Methods One solution is to give the SmallBusiness object a TearDown method. This could set all of the SmallBusiness object's object properties to Nothing, and also set all the Collection objects (Employees, Customers) to Nothing.
When a Collection object is destroyed, Visual Basic sets all the object references it was holding to Nothing. If there are no other references to the Employee and Customer objects that were contained in the Employees and Customers collections, they'll be destroyed.
Of course, if the Employee object is made up of finer objects, it will have the same circular reference problem its parent does. In that case, you'll have to give the Employee class a TearDown method. Instead of just setting the Employees Collection object to Nothing, the SmallBusiness object will first have to iterate through the collection, calling the TearDown method of each Employee object.
It's Not Over Yet Even then, not all the objects may be destroyed. If there are variables anywhere in your program that still contain references to the SmallBusiness object, or to any of the objects it contains, those objects won't be destroyed. Part of the cleanup for your program must be to ensure that all object variables everywhere are set to Nothing.
To test whether this is happening, you may want to add some debugging code to your objects. For example, you can add the following code to a standard module:
' Global debug collection
Public gcolDebug As New Collection
' Global function to give each object a unique ID.
Public Function DebugSerial() As Long
Static lngSerial As Long
lngSerial = lngSerial + 1
DebugSerial = lngSerial
End Function
In each class module, you can put code similar to the following. Each class provides its own name where "Product" appears.
' Storage for the debug ID.
Private mlngDebugID As Long
Property Get DebugID() As Long
DebugID = mlngDebugID
End Property
Private Sub Class_Initialize()
mlngDebugID = DebugSerial
' Add a string entry to the global collection.
gcolDebug.Add "Product Initialize; DebugID=" _
& DebugID, CStr(DebugID)
End Sub
Private Sub Class_Terminate()
' Remove the string entry, so you know the object
' isn't around any more.
gcolDebug.Remove CStr(DebugID)
End Sub
As each object is created, it places a string in the global collection; as it's destroyed it removes the string. At any time, you iterate over the global collection to see what objects haven't been destroyed.
Public Collection Example: The House of Straw
To create the example, open a new project and insert two class modules. Draw five command buttons, a list box, two text boxes, and two labels on the form, as shown in Figure
The following table lists the property values you need to set for this example.
Object Property Setting Class module Name Employee Class module Name SmallBusiness Form Caption Employees Collection First command button Caption
Name Add
cmdAddEmployee Second command button Caption
Name Delete
cmdDeleteEmployee Third command button Caption
Name Refresh List
cmdListEmployees Fourth command button Caption
Name Trouble
cmdTrouble Fifth command button Caption
Name Close
cmdClose First label control Caption Name Second label control Caption Salary First text box Name
Text txtName
(blank) Second text box Name
Text txtSalary
(blank) List Box Name lstEmployees In the Employee class module, add the following declarations and property procedures:
Option Explicit
' Properties of the Employee class.
Public Name As String
Public Salary As Long
' Private data for the write-once ID property.
Private mstrID As String
Property Get ID() As String
ID = mstrID
End Property
' The first time the ID property is set, the static
' Boolean is also set. Subsequent calls do nothing.
' (It would be better to raise an error, instead.)
Property Let ID(strNew As String)
Static blnAlreadySet As Boolean
If Not blnAlreadySet Then
blnAlreadySet = True
mstrID = strNew
End If
End Property
The ID property is the key for retrieving or deleting an Employee object from the collection, so it must be set once and never changed. This is accomplished with a Static Boolean variable that is set to True the first time the property is set. The property can always be read, because there is a Property Get.
In the SmallBusiness class module, add the following declaration. The collection object will be created the first time the Employees variable is referred to in code.
Option Explicit
Public Employees As New Collection
The Form Does All the Work All of the remaining code goes into the form module. Add the following declaration in the Declarations section.
Option Explicit
Public sbMain As New SmallBusiness
The code in the cmdEmployeeAdd_Click event adds a member to the collection.
Private Sub cmdEmployeeAdd_Click()
Dim empNew As New Employee
Static intEmpNum As Integer
' Using With makes your code faster and more
' concise (.ID vs. empNew.ID).
With empNew
' Generate a unique ID for the new employee.
intEmpNum = intEmpNum + 1
.ID = "E" & Format$(intEmpNum, "00000")
.Name = txtName.Text
.Salary = CDbl(txtSalary.Text)
' Add the Employee object reference to the
' collection, using the ID property as the key.
sbMain.Employees.Add empNew, .ID
End With
txtName.Text = ""
txtSalary.Text = ""
' Click the Refresh List button.
cmdListEmployees.Value = True
End Sub
The code in the cmdListEmployees_Click event procedure uses a For Each ... Next statement to add all the employee information to the ListBox control.
Private Sub cmdListEmployees_Click()
Dim emp As Employee
lstEmployees.Clear
For Each emp In sbMain.Employees
lstEmployees.AddItem emp.ID & ", " & emp.Name _
& ", " & emp.Salary
Next
End Sub
The cmdEmployeeDelete_Click event uses the Collection object's Remove method to delete the collection member currently selected in the ListBox control.
Private Sub cmdEmployeeDelete_Click()
' Check to make sure there's an employee selected.
If lstEmployees.ListIndex > -1 Then
' The first six characters are the ID.
sbMain.Employees.Remove _
Left(lstEmployees.Text, 6)
End If
' Click the Refresh List button.
cmdListEmployees.Value = True
End Sub
Add the following code to the Trouble button.
Private Sub cmdTrouble_Click()
' Say what!?
sbMain.Employees.Add Me
End Sub
The cmdClose_Click event closes the application. When you close projects that use objects, do so by unloading all the forms, to ensure that any Terminate event procedures in your class modules will get executed. By contrast, using the End statement stops a program abruptly, without executing Terminate events.
Private Sub cmdClose_Click()
Unload Me
End Sub
To add employees in the example, run the application, enter values in the two text boxes, and then choose the Add button. Add a few employees, and then experiment with the delete and list buttons.
Robust as a Straw House This simple implementation is not very robust. Because the Employees property is just a public Collection object, you could inadvertently access it from anywhere in your program. Furthermore, the Add method of the Collection object doesn't do any type checking. For example, the code in the Trouble button's Click event blithely inserts an object reference to the form into the collection of employees.
Click the Trouble button, and notice that no error occurs. Now click the Refresh List button. When the For Each ... Next loop encounters the unexpected object type, it causes error 13, Type mismatch.
This is an example of the kind of error you're exposed to when you build an object model with public Collection objects. Objects can be added from anywhere in your project, and there's no guarantee that they'll be properly initialized. If a programmer clones the code to add an employee, and the original code is later changed, the resulting errors can be very difficult to track down.
The Project Explorer
As you create, add, or remove editable files from a project, Visual Basic reflects your changes in the Project Explorer window, which contains a current list of the files in the project.
The Project Explorer window
The Project File
Each time you save a project, Visual Basic updates the project file (.vbp). A project file contains the same list of files that appears in the Project Explorer window, as well as references to the ActiveX controls and insertable objects that are used in the project.
You can open an existing project file by double-clicking its icon, by choosing the Open Project command from the File menu, or by dragging the file and dropping it on the Project Explorer window.
The Structure of a Visual Basic Project
Form Modules
Form modules (.frm file name extension) can contain textual descriptions of the form and its controls, including their property settings. They can also contain form-level declarations of constants, variables, and external procedures; event procedures; and general procedures. Class Modules
Class modules (.cls file name extension) are similar to form modules, except that they have no visible user interface. You can use class modules to create your own objects, including code for methods and properties. Standard Modules
Standard modules (.bas file name extension) can contain public or module-level declarations of types, constants, variables, external procedures, and public procedures. Resource Files
Resource files (.res file name extension) contain bitmaps, text strings, and other data that you can change without having to re-edit your code. For example, if you plan to localize your application in a foreign language, you can keep all of the user-interface text strings and bitmaps in a resource file, which you can then localize instead of the entire application. A project can contain no more than one resource file. ActiveX Documents
ActiveX documents (.dob) are similar to forms, but are displayable in an Internet browser such as Internet Explorer. The Professional and Enterprise editions of Visual Basic are capable of creating ActiveX documents. User Control and Property Page Modules
User Control (.ctl) and Property Page (.pag) modules are also similar to forms, but are used to create ActiveX controls and their associated property pages for displaying design-time properties. The Professional and Enterprise editions of Visual Basic are capable of creating ActiveX controls. Components
In addition to files and modules, several other types of components can be added to the project. ActiveX Controls
ActiveX controls (.ocx file name extension) are optional controls which can be added to the toolbox and used on forms. When you install Visual Basic, the files containing the controls included with Visual Basic are copied to a common directory (the \Windows\System subdirectory under Windows 95 or later). Additional ActiveX controls are available from a wide variety of sources. You can also create your own controls using the Professional or Enterprise editions of Visual Basic. Insertable Objects
Insertable objects, such as a Microsoft Excel Worksheet object, are components you can use as building blocks to build integrated solutions. An integrated solution can contain data in different formats, such as spreadsheets, bitmaps, and text, which were all created by different applications.
Creating, Opening, and Saving Projects
New Project
Closes the current project, prompting you to save any files that have changed. You can select a type of project from the New Project dialog. Visual Basic then creates a new project with a single new file.
Open Project
Closes the current project, prompting you to save any changes. Visual Basic then opens an existing project, including the forms, modules, and ActiveX controls listed in its project (.vbp) file.
Save Project
Updates the project file of the current project and all of its form, standard, and class modules.
Save Project As
Updates the project file of the current project, saving the project file under a file name that you specify. Visual Basic also prompts you to save any forms or modules that have changed.
Adding, Removing, and Saving Files
To add a file to a project
- Select Project, Add filetype (where filetype is the type of file).
- Select an existing file or a new file type, and choose Open.
When you add a file to a project, you are simply including a reference to the existing file in the project; you are not adding a copy of the file. Therefore, if you make changes to a file and save it, your changes will affect any project that includes the file. To change a file without affecting other projects, select the file in the Project Explorer, choose Save filename As from the File menu, and then save the file under a new file name.
To remove a file from a project
- Select the file in the Project Explorer.
- From the Project menu, choose Remove filename.
- The file will be removed from the project but not from the disk.
The set of controls available in the toolbox can be customized for each project. Any given control must be in the toolbox before you can add it to a form in the project. The basic set of standard controls that always appear in the toolbox is described in "Forms, Controls, and Menus."
To add a control to a project's toolbox
- From the Project menu, choose Components.
- To add a control (.ocx file name extension) or an insertable object to the toolbox, select the check box to the left of the control name.
To view controls with .ocx file name extensions, select the Controls tab. To view insertable objects, such as a Microsoft Excel Chart, select the Insertable Objects tab. - Choose OK to close the Components dialog box. All of the ActiveX controls that you selected will now appear in the toolbox.
The Components dialog box
To add ActiveX controls to the Components dialog box, choose the Browse button, and search other directories for files with a .ocx file name extension. When you add an ActiveX control to the list of available controls, Visual Basic automatically selects the check box.
Note Each ActiveX control is accompanied by a file with an .oca extension. This file stores cached type library information and other data specific to the control. The .oca files are typically stored in the same directory as the ActiveX controls and are recreated as needed (file sizes and dates may change).
Removing Controls from a Project
- From the Project menu, choose Components.
The Components dialog box is displayed. - Clear the check box next to each control you want to remove.
The control icons will be removed from the toolbox.
Making and Running an Executable File
To make an executable file in Visual Basic
- From the File menu, choose Make projectname .exe where projectname is the application name for the project.
- Type a file name, or browse through the directories and select an existing file name to overwrite an existing executable with a newer version.
- By clicking the Options button, you can also specify a number of version-specific details about the executable file in the Project Properties dialog box.
- If you want to modify the version number of the project, set the appropriate Major, Minor, and Revision numbers. Selecting Auto Increment will automatically step the Revision number each time you run the Make projectname .exe command for this project.
- To specify a new name for the application, under Application, type a new name in the Title box. If you want to specify a new icon, choose one from the list.
- You can also enter version-specific commentary on a variety of issues under the Version Information box (comments, company name, trademark and copyright information, and so on) by selecting a topic from the list box and entering information in the text box.
- Choose OK to close the Project Properties dialog box, and then choose OK in the Make appname .exe dialog box to compile and link the executable file. You can run the executable file like any other Windows-based application: double-click the icon for the executable file.
Note Building an executable file from the command line in a DOS session can be useful when you want to compile a project programmatically. In a batch file, type:
Programming Fundamentals
This chapter introduces the essential components of the Visual Basic language. After creating the interface for your application using forms and controls, you will need to write the code that defines the application's behavior. As with any modern programming language, Visual Basic supports a number of common programming constructs and language elements.
Visual Basic is an object-based programming language. The mere mention of objects may cause undue anxiety in many programmers. Don't worry: whether you realize it or not, you've been dealing with objects most of your life. Once you understand a few basic concepts, objects actually help to make programming easier than ever before.
If you've programmed in other languages, much of the material covered in this chapter will seem familiar. While most of the constructs are similar to other languages, the event-driven nature of Visual Basic introduces some subtle differences. Try and approach this material with an open mind; once you understand the differences you can use them to your advantage.
If you're new to programming, the material in this chapter will serve as an introduction to the basic building blocks for writing code. Once you understand the basics, you will be able to create powerful applications using Visual Basic.
The Structure of a Visual Basic Application An application is really nothing more than a set of instructions directing the computer to perform a task or tasks. The structure of an application is the way in which the instructions are organized; that is, where the instructions are stored and the order in which instructions are executed.
Simple applications such as the classic "hello world" example have a simple structure; organization isn't very important with a single line of code. As applications become more complex, the need for organization or structure becomes obvious. Imagine the chaos that would result if your application's code was allowed to execute in random order. In addition to controlling the execution of an application, the structure is important to the programmer: how easily can you find specific instructions within your application?
Because a Visual Basic application is based on objects, the structure of its code closely models its physical representation on screen. By definition, objects contain data and code. The form that you see on screen is a representation of the properties that define its appearance and intrinsic behavior. For each form in an application, there is a related form module (with file name extension .frm) that contains its code
A form and its related form module
Each form module contains event procedures — sections of code where you place the instructions that will execute in response to specific events. Forms can contain controls. For each control on a form, there is a corresponding set of event procedures in the form module. In addition to event procedures, form modules can contain general procedures that are executed in response to a call from any event procedure.
Code that isn't related to a specific form or control can be placed in a different type of module, a standard module (.BAS). A procedure that might be used in response to events in several different objects should be placed in a standard module, rather than duplicating the code in the event procedures for each object.
A class module (.CLS) is used to create objects that can be called from procedures within your application. Whereas a standard module contains only code, a class module contains both code and data — you can think of it as a control without a physical representation.
While "Managing Projects" describes which components you can add to an application, this chapter explains how to write code in the various components that make up an application. By default, your project contains a single form module. You can add additional form, class, and standard modules, as needed. Class modules are discussed in "Programming with Objects."
Before You Start Coding Perhaps the most important (and often overlooked) part of creating an application in Visual Basic is the design phase. While it's obvious that you need to design a user interface for your application, it may not be as obvious that you need to design the structure of the code. The way you structure your application can make a difference in its performance as well as in the maintainability and usability of your code.
The code in a Visual Basic application is organized in a hierarchical fashion. A typical application consists of one or more modules: a form module for each form in the application, optional standard modules for shared code, and optional class modules. Each module contains one or more procedures that contain the code: event procedures, Sub or Function procedures, and Property procedures.
Determining which procedures belong in which module depends somewhat on the type of application that you are creating. Because Visual Basic is based on objects, it helps to think of your application in terms of the objects that it represents. The design of the sample application for this chapter, Vcr.vbp, is based on the objects that comprise a video cassette recorder and a television. The VCR application consists of two form modules, a standard module, and two class modules. You can use the Object Browser to examine the structure of the project
The structure of the VCR project is shown in the Object Browser
The main form for the VCR application (frmVCR) is a visual representation of a combination VCR and television screen . It is composed of several objects that model those found in the real world version. A group of Command buttons (cmdPlay, cmdRecord, and so on) mimic the buttons used to operate a VCR. The software VCR also contains a clock (lblTime), a channel indicator (lblChannel), function indicators (shpPlay, shpRecord, and so on), and a "picture tube" (picTV). The event procedures for all of these objects are contained in the Vcr.frm form module.
Using the Code Editor
The Visual Basic Code Editor is a window where you write most of your code. It is like a highly specialized word processor with a number of features that make writing Visual Basic code a lot easier.
The Code Editor window
Each section of code can contain several different procedures, accessed using the Procedure Listbox. The procedure list for a form module contains a separate section for each event procedure for the form or control. For example, the procedure list for a Label control includes sections for the Change, Click, and DblClick events, among others. Class modules list only the event procedures for the class itself — Initialize and Terminate. Standard modules don't list any event procedures, because a standard module doesn't support events.
The procedure list for a general section of a module contains a single selection — the Declarations section, where you place module-level variable, constant, and DLL declarations. As you add Sub or Function procedures to a module, those procedures are added in the Procedure Listbox below the Declarations section.
Two different views of your code are available in the Code Editor window. You can choose to view a single procedure at a time, or to view all of the procedures in the module with each procedure separated from the next by a line (as shown in Figure 5.4). To switch between the two views, use the View Selection buttons in the lower left-hand corner of the editor window.
Auto Quick Info
Bookmarks
Bookmarks can be used to mark lines of code in the Code Editor so that you can easily return to them later. Commands to toggle bookmarks on or off as well as to navigate existing bookmarks are available from the Edit, Bookmarks menu item, or from the Edit toolbar
Code Basics Combining Statements on One Line
There is usually one Visual Basic statement to a line, and there is no statement terminator. However, you can place two or more statements on a line if you use a colon (:) to separate them:
Text1.Text = "Hello" : Red = 255 : Text1.BackColor = _
Red
In order to make your code more readable, however, it's better to place each statement on a separate line.
Adding Comments to Your Code
As you read through the examples in this guide, you'll often come across the comment symbol ('). This symbol tells Visual Basic to ignore the words that follow it. Such words are remarks placed in the code for the benefit of the developer, and other programmers who might examine the code later. For example:
' This is a comment beginning at the left edge of the
' screen.
Text1.Text = "Hi!" ' Place friendly greeting in text
' box.
Comments can follow a statement on the same line or can occupy an entire line. Both are illustrated in the preceding code. Remember that comments can't follow a line-continuation character on the same line.
Variables
In Visual Basic, you use variables to temporarily store values during the execution of an application. Variables have a name (the word you use to refer to the value the variable contains) and a data type (which determines the kind of data the variable can store).
You can think of a variable as a placeholder in memory for an unknown value. For example, imagine you are creating a program for a fruit stand to track the sales of apples. You don't know the price of an apple or the quantity sold until the sale actually occurs. You can use two variables to hold the unknown values — let's name them ApplePrice and ApplesSold. Each time the program is run, the user supplies the values for the two variables. To calculate the total sales and display it in a Textbox named txtSales, your code would look like this:
txtSales.txt = ApplePrice * ApplesSold
The expression returns a different total each time, depending on what values the user provides. The variables allow you to make a calculation without having to know in advance what the actual inputs are.
In this example, the data type of ApplePrice is Currency; the data type of ApplesSold is an integer. Variables can represent many other values as well: text values, dates, various numeric types, even objects.
Declaring Variables To declare a variable is to tell the program about it in advance. You declare a variable with the Dim statement, supplying a name for the variable:
Dim variablename [As type]
Variables declared with the Dim statement within a procedure exist only as long as the procedure is executing. When the procedure finishes, the value of the variable disappears. In addition, the value of a variable in a procedure is local to that procedure — that is, you can't access a variable in one procedure from another procedure. These characteristics allow you to use the same variable names in different procedures without worrying about conflicts or accidental changes.
A variable name:
- Must begin with a letter.
- Can't contain an embedded period or embedded type-declaration character.
- Must not exceed 255 characters.
- Must be unique within the same scope, which is the range from which the variable can be referenced — a procedure, a form, and so on.
- The optional As type clause in the Dim statement allows you to define the data type or object type of the variable you are declaring. Data types define the type of information the variable stores. Some examples of data types include String, Integer, and Currency. Variables can also contain objects from Visual Basic or other applications. Examples of Visual Basic object types, or classes, include Object, Form1, and TextBox.
You don't have to declare a variable before using it. For example, you could write a function where you don't need to declare TempVal before using it:
Function SafeSqr(num)
TempVal = Abs(num)
SafeSqr = Sqr(TempVal)
End Function
Visual Basic automatically creates a variable with that name, which you can use as if you had explicitly declared it. While this is convenient, it can lead to subtle errors in your code if you misspell a variable name. For example, suppose that this was the function you wrote:
Function SafeSqr(num)
TempVal = Abs(num)
SafeSqr = Sqr(TemVal)
End Function
At first glance, this looks the same. But because the TempVal variable was misspelled on the next-to-last line, this function will always return zero. When Visual Basic encounters a new name, it can't determine whether you actually meant to implicitly declare a new variable or you just misspelled an existing variable name, so it creates a new variable with that name.
Explicit Declaration
To avoid the problem of misnaming variables, you can stipulate that Visual Basic always warn you whenever it encounters a name not declared explicitly as a variable.
Scoping Variables
Depending on how it is declared, a variable is scoped as either a procedure-level (local) or module-level variable.
Scope Private Public Procedure-level Variables are private to the procedure in which they appear. Not applicable. You cannot declare public variables within a procedure. Module-level Variables are private to the module in which they appear Variables are available to all modules Variables Used Within a Procedure
Procedure-level variables are recognized only in the procedure in which they're declared. These are also known as local variables. You declare them with the Dim or Static keywords. For example:
Dim intTemp As Integer
–or–
Static intPermanent As Integer
Values in local variables declared with Static exist the entire time your application is running while variables declared with Dim exist only as long as the procedure is executing.
Local variables are a good choice for any kind of temporary calculation. For example, you can create a dozen different procedures containing a variable called intTemp. As long as each intTemp is declared as a local variable, each procedure recognizes only its own version of intTemp. Any one procedure can alter the value in its local intTemp without affecting intTemp variables in other procedures.
Variables Used Within a Module
By default, a module-level variable is available to all the procedures in that module, but not to code in other modules. You create module-level variables by declaring them with the Private keyword in the Declarations section at the top of the module. For example:
Private intTemp As Integer
At the module level, there is no difference between Private and Dim, but Private is preferred because it readily contrasts with Public and makes your code easier to understand.
Variables Used by All Modules
To make a module-level variable available to other modules, use the Public keyword to declare the variable. The values in public variables are available to all procedures in your application. Like all module-level variables, public variables are declared in the Declarations section at the top of the module. For example: Public intTemp As Integer
Note You can't declare public variables within a procedure, only within the Declarations section of a module.
Static Variables
In addition to scope, variables have a lifetime, the period of time during which they retain their value. The values in module-level and public variables are preserved for the lifetime of your application. However, local variables declared with Dim exist only while the procedure in which they are declared is executing. Usually, when a procedure is finished executing, the values of its local variables are not preserved and the memory used by the local variables is reclaimed. The next time the procedure is executed, all its local variables are reinitialized.
However, you can preserve the value of a local variable by making the variable static. Use the Static keyword to declare one or more variables inside a procedure, exactly as you would with the Dim statement:
Static Depth
For example, the following function calculates a running total by adding a new value to the total of previous values stored in the static variable Accumulate:
Function RunningTotal(num)
Static ApplesSold
ApplesSold = ApplesSold + num
RunningTotal = ApplesSold
End Function
If ApplesSold was declared with Dim instead of Static, the previous accumulated values would not be preserved across calls to the function, and the function would simply return the same value with which it was called.
You can produce the same result by declaring ApplesSold in the Declarations section of the module, making it a module-level variable. Once you change the scope of a variable this way, however, the procedure no longer has exclusive access to it. Because other procedures can access and change the value of the variable, the running totals might be unreliable and the code would be more difficult to maintain.
Declaring All Local Variables as Static
To make all local variables in a procedure static, place the Static keyword at the beginning of a procedure heading. For example: Static Function RunningTotal(num)
This makes all the local variables in the procedure static regardless of whether they are declared with Static, Dim, Private, or declared implicitly. You can place Static in front of any Sub or Function procedure heading, including event procedures and those declared as Private.
Constants
Often you'll find that your code contains constant values that reappear over and over. Or you may find that the code depends on certain numbers that are difficult to remember — numbers that, in and of themselves, have no obvious meaning.
In these cases, you can greatly improve the readability of your code — and make it easier to maintain — by using constants. A constant is a meaningful name that takes the place of a number or string that does not change. Although a constant somewhat resembles a variable, you can't modify a constant or assign a new value to it as you can to a variable. There are two sources for constants:
- Intrinsic or system-defined constants are provided by applications and controls. Visual Basic constants are listed in the Visual Basic (VB) and Visual Basic for applications (VBA) object libraries in the Object Browser. Other applications that provide object libraries, such as Microsoft Excel and Microsoft Project, also provide a list of constants you can use with their objects, methods, and properties. Constants are also defined in the object library for each ActiveX control. For details on using the Object Browser, see "Programming with Objects."
- Symbolic or user-defined constants are declared using the Const statement. User-defined constants are described in the next section, "Creating Your Own Constants."
The prefixes are intended to prevent accidental collisions in cases where constants have identical names and represent different values. Even with prefixes, it's still possible that two object libraries may contain identical constants representing different values. Which constant is referenced in this case depends on which object library has the higher priority. For information on changing the priority of object libraries, see the "References Dialog Box."
To be absolutely sure you avoid constant name collisions, you can qualify references to constants with the following syntax:
[libname.][modulename.]constname
Libname is usually the class name of the control or library. Modulename is the name of the module that defines the constant. Constname is the name of the constant. Each of these elements is defined in the object library, and can be viewed in the Object Browser.
Creating Your Own Constants The syntax for declaring a constant is:
[Public|Private] Const constantname[As type] = expression
The argument constantname is a valid symbolic name (the rules are the same as those for creating variable names), and expression is composed of numeric or string constants and operators; however, you can't use function calls in expression.
A Const statement can represent a mathematical or date/time quantity:
Const conPi = 3.14159265358979
Public Const conMaxPlanets As Integer = 9
Const conReleaseDate = #1/1/95#
The Const statement can also be used to define string constants:
Public Const conVersion = "07.10.A"
Const conCodeName = "Enigma"
You can place more than one constant declaration on a single line if you separate them with commas:
Public Const conPi = 3.14, conMaxPlanets = 9, _
conWorldPop = 6E+09
The expression on the right side of the equal sign ( = ) is often a number or literal string, but it can also be an expression that results in a number or string (although that expression can't contain calls to functions). You can even define constants in terms of previously defined constants:
Const conPi2 = conPi * 2
Once you define constants, you can place them in your code to make it more readable. For example:
Static SolarSystem(1 To conMaxPlanets)
If numPeople > conWorldPop Then Exit Sub
Scoping User-Defined Constants A Const statement has scope like a variable declaration, and the same rules apply:
- To create a constant that exists only within a procedure, declare it within that procedure
- To create a constant available to all procedures within a module, but not to any code outside that module, declare it in the Declarations section of the module.
- To create a constant available throughout the application, declare the constant in the Declarations section of a standard module, and place the Public keyword before Const. Public constants cannot be declared in a form or class module.
Variables are placeholders used to store values; they have names and data types. The data type of a variable determines how the bits representing those values are stored in the computer's memory. When you declare a variable, you can also supply a data type for it. All variables have a data type that determines what kind of data they can store.
By default, if you don't supply a data type, the variable is given the Variant data type. The Variant data type is like a chameleon — it can represent many different data types in different situations. You don't have to convert between these types of data when assigning them to a Variant variable: Visual Basic automatically performs any necessary conversion.
If you know that a variable will always store data of a particular type, however, Visual Basic can handle that data more efficiently if you declare a variable of that type. For example, a variable to store a person's name is best represented as a string data type, because a name is always composed of characters.
Data types apply to other things besides variables. When you assign a value to a property, that value has a data type; arguments to functions also have data types. In fact, just about anything in Visual Basic that involves data also involves data types.
You can also declare arrays of any of the fundamental types.
For More Information For more information, see the section, "Arrays," later in this chapter. Selecting data types to improve your application's performance is discussed in "Designing for Performance and Compatibility."
Declaring Variables with Data Types
Before using a non-Variant variable, you must use the Private, Public, Dim or Static statement to declare it As type. For example, the following statements declare an Integer, Double, String, and Currency type, respectively:
Private I As Integer
Dim Amt As Double
Static YourName As String
Public BillsPaid As Currency
A Declaration statement can combine multiple declarations, as in these statements:
Private I As Integer, Amt As Double
Private YourName As String, BillsPaid As Currency
Private Test, Amount, J As Integer
Note If you do not supply a data type, the variable is given the default type. In the preceding example, the variables Test and Amount are of the Variant data type. This may surprise you if your experience with other programming languages leads you to expect all variables in the same declaration statement to have the same specified type (in this case, Integer).
Numeric Data Types
Visual Basic supplies several numeric data types — Integer, Long (long integer), Single (single-precision floating point), Double (double-precision floating point), and Currency. Using a numeric data type generally uses less storage space than a variant.
If you know that a variable will always store whole numbers (such as 12) rather than numbers with a fractional amount (such as 3.57), declare it as an Integer or Long type. Operations are faster with integers, and these types consume less memory than other data types. They are especially useful as the counter variables in For...Next loops.
The Byte Data Type If the variable contains binary data, declare it as an array of the Byte data type. (Arrays are discussed in "Arrays" later in this chapter). Using Byte variables to store binary data preserves it during format conversions. When String variables are converted between ANSI and Unicode formats, any binary data in the variable is corrupted. Visual Basic may automatically convert between ANSI and Unicode when:
- Reading from files
- Writing to files
- Calling DLLs
- Calling methods and properties on objects
All numeric variables can be assigned to each other and to variables of the Variant type. Visual Basic rounds off rather than truncates the fractional part of a floating-point number before assigning it to an integer.
The String Data Type If you have a variable that will always contain a string and never a numeric value, you can declare it to be of type String:
Private S As String
You can then assign strings to this variable and manipulate it using string functions:
S = "Database"
S = Left(S, 4)
By default, a string variable or argument is a variable-length string; the string grows or shrinks as you assign new data to it. You can also declare strings that have a fixed length. You specify a fixed-length string with this syntax:
String * size
For example, to declare a string that is always 50 characters long, use code like this:
Dim EmpName As String * 50
If you assign a string of fewer than 50 characters, EmpName is padded with enough trailing spaces to total 50 characters. If you assign a string that is too long for the fixed-length string, Visual Basic simply truncates the characters.
Because fixed-length strings are padded with trailing spaces, you may find the Trim and RTrim functions, which remove the spaces, useful when working with them.
Fixed-length strings in standard modules can be declared as Public or Private. In forms and class modules, fixed-length strings must be declared Private.
Exchanging Strings and Numbers
You can assign a string to a numeric variable if the string represents a numeric value. It's also possible to assign a numeric value to a string variable. For example, place a command button, text box, and list box on a form. Enter the following code in the command button's Click event. Run the application, and click the command button.
Private Sub Command1_Click()
Dim intX As Integer
Dim strY As String
strY = "100.23"
intX = strY ' Passes the string to a numeric
' variable.
List1.AddItem Cos(strY) ' Adds cosine of number in
' the string to the listbox.
strY = Cos(strY) ' Passes cosine to the
' string variable.
Text1.Text = strY ' String variable prints in
' the text box.
End Sub
Visual Basic will automatically coerce the variables to the appropriate data type. You should use caution when exchanging strings and numbers; passing a non-numeric value in the string will cause a run-time error to occur.
The Boolean Data Type If you have a variable that will contain simple true/false, yes/no, or on/off information, you can declare it to be of type Boolean. The default value of Boolean is False. In the following example, blnRunning is a Boolean variable which stores a simple yes/no setting.
Dim blnRunning As Boolean
' Check to see if the tape is running.
If Recorder.Direction = 1 Then
blnRunning = True
End if
The Date Data Type Date and time values can be contained both in the specific Date data type and in Variant variables. The same general characteristics apply to dates in both types.
The Object Data Type
Object variables are stored as 32-bit (4-byte) addresses that refer to objects within an application or within some other application. A variable declared as Object is one that can subsequently be assigned (using the Set statement) to refer to any actual object recognized by the application.
Dim objDb As Object
Set objDb = OpenDatabase("c:\Vb5\Biblio.mdb")
When declaring object variables, try to use specific classes (such as TextBox instead of Control or, in the preceding case, Database instead of Object) rather than the generic Object. Visual Basic can resolve references to the properties and methods of objects with specific types before you run an application. This allows the application to perform faster at run time. Specific classes are listed in the Object Browser.
When working with other applications' objects, instead of using a Variant or the generic Object, declare objects as they are listed in the Classes list in the Object Browser. This ensures that Visual Basic recognizes the specific type of object you're referencing, allowing the reference to be resolved at run time.
Converting Data Types Visual Basic provides several conversion functions you can use to convert values into a specific data type. To convert a value to Currency, for example, you use the CCur function:
PayPerWeek = CCur(hours * hourlyPay)
Conversion function Converts an expression to
Cbool Boolean Cbyte Byte Ccur Currency Cdate Date CDbl Double Cint Integer CLng Long CSng Single CStr String Cvar Variant CVErr Error
The Variant Data Type
Variant variable is capable of storing all system-defined types of data. You don't have to convert between these types of data if you assign them to a Variant variable; Visual Basic automatically performs any necessary conversion. For example:
Dim SomeValue ' Variant by default.
SomeValue = "17" ' SomeValue contains "17" (a two-
' character string).
SomeValue = SomeValue - 15 ' SomeValue now contains
' the numeric value 2.
SomeValue = "U" & SomeValue ' SomeValue now contains
' "U2" (a two- character string).
While you can perform operations on Variant variables without much concern for the kind of data they contain, there are some traps you must avoid.
- f you perform arithmetic operations or functions on a Variant, the Variant must contain something that is a number. For details, see the section, "Numeric Values Stored in Variants," in "Advanced Variant Topics."
- If you are concatenating strings, use the & operator instead of the + operator. For details, see the section, "Strings Stored in Variants," in "Advanced Variant Topics."
The Empty Value
Sometimes you need to know if a value has ever been assigned to a created variable. A Variant variable has the Empty value before it is assigned a value. The Empty value is a special value different from 0, a zero-length string (""), or the Null value. You can test for the Empty value with the IsEmpty function: If IsEmpty(Z) Then Z = 0
When a Variant contains the Empty value, you can use it in expressions; it is treated as either 0 or a zero-length string, depending on the expression.
The Empty value disappears as soon as any value (including 0, a zero-length string, or Null) is assigned to a Variant variable. You can set a Variant variable back to Empty by assigning the keyword Empty to the Variant.
The Null Value The Variant data type can contain another special value: Null. Null is commonly used in database applications to indicate unknown or missing data. Because of the way it is used in databases, Null has some unique characteristics:
- Expressions involving Null always result in Null. Thus, Null is said to "propagate" through expressions; if any part of the expression evaluates to Null, the entire expression evaluates to Null.
- Passing Null, a Variant containing Null, or an expression that evaluates to Null as an argument to most functions causes the function to return Null.
- Null values propagate through intrinsic functions that return Variant data types.
Z = Null
You can use the IsNull function to test if a Variant variable contains Null:
If IsNull(X) And IsNull(Y) Then
Z = Null
Else
Z = 0
End If
If you assign Null to a variable of any type other than Variant, a trappable error occurs. Assigning Null to a Variant variable doesn't cause an error, and Null will propagate through expressions involving Variant variables (though Null does not propagate through certain functions). You can return Null from any Function procedure with a Variant return value.
Variables are not set to Null unless you explicitly assign Null to them, so if you don't use Null in your application, you don't have to write code that tests for and handles it.
The Error Value In a Variant, Error is a special value used to indicate that an error condition has occurred in a procedure. However, unlike for other kinds of errors, normal application-level error handling does not occur. This allows you, or the application itself, to take some alternative based on the error value. Error values are created by converting real numbers to error values using the CVErr function.
Arrays
If you have programmed in other languages, you're probably familiar with the concept of arrays. Arrays allow you to refer to a series of variables by the same name and to use a number (an index) to tell them apart. This helps you create smaller and simpler code in many situations, because you can set up loops that deal efficiently with any number of cases by using the index number. Arrays have both upper and lower bounds, and the elements of the array are contiguous within those bounds. Because Visual Basic allocates space for each index number, avoid declaring an array larger than necessary.
Note The arrays discussed in this section are arrays of variables, declared in code. They are different from the control arrays you specify by setting the Index property of controls at design time. Arrays of variables are always contiguous; unlike control arrays, you cannot load and unload elements from the middle of the array.
All the elements in an array have the same data type. Of course, when the data type is Variant, the individual elements may contain different kinds of data (objects, strings, numbers, and so on). You can declare an array of any of the fundamental data types, including user-defined types (described in the section, "Creating Your Own Data Types," in "More About Programming") and object variables (described in "Programming with Objects").
Declaring Fixed-Size Arrays There are three ways to declare a fixed-size array, depending on the scope you want the array to have:
- To create a public array, use the Public statement in the Declarations section of a module to declare the array.
- To create a module-level array, use the Private statement in the Declarations section of a module to declare the array.
- To create a local array, use the Private statement in a procedure to declare the array.
When declaring an array, follow the array name by the upper bound in parentheses. The upper bound cannot exceed the range of a Long data type (-2,147,483,648 to 2,147,483,647). For example, these array declarations can appear in the Declarations section of a module:
Dim Counters(14) As Integer ' 15 elements.
Dim Sums(20) As Double ' 21 elements.
To create a public array, you simply use Public in place of Dim:
Public Counters(14) As Integer
Public Sums(20) As Double
The same declarations within a procedure use Dim:
Dim Counters(14) As Integer
Dim Sums(20) As Double
The first declaration creates an array with 15 elements, with index numbers running from 0 to 14. The second creates an array with 21 elements, with index numbers running from 0 to 20. The default lower bound is 0.
To specify a lower bound, provide it explicitly (as a Long data type) using the To keyword:
Dim Counters(1 To 15) As Integer
Dim Sums(100 To 120) As String
In the preceding declarations, the index numbers of Counters range from 1 to 15, and the index numbers of Sums range from 100 to 120.
Arrays that Contain Other Arrays
It's possible to create a Variant array, and populate it with other arrays of different data types. The following code creates two arrays, one containing integers and the other strings. It then declares a third Variant array and populates it with the integer and string arrays.
Private Sub Command1_Click()
Dim intX As Integer ' Declare counter variable.
' Declare and populate an integer array.
Dim countersA(5) As Integer
For intX = 0 To 4
countersA(intX) = 5
Next intX
' Declare and populate a string array.
Dim countersB(5) As String
For intX = 0 To 4
countersB(intX) = "hello"
Next intX
Dim arrX(2) As Variant ' Declare a new two-member
' array.
arrX(1) = countersA() ' Populate the array with
' other arrays.
arrX(2) = countersB()
MsgBox arrX(1)(2) ' Display a member of each
' array.
MsgBox arrX(2)(3)
End Sub
Multidimensional Arrays
Sometimes you need to keep track of related information in an array. For example, to keep track of each pixel on your computer screen, you need to refer to its X and Y coordinates. This can be done using a multidimensional array to store the values.
With Visual Basic, you can declare arrays of multiple dimensions. For example, the following statement declares a two-dimensional 10-by-10 array within a procedure:
Static MatrixA(9, 9) As Double
Either or both dimensions can be declared with explicit lower bounds:
Static MatrixA(1 To 10, 1 To 10) As Double
You can extend this to more than two dimensions. For example:
Dim MultiD(3, 1 To 10, 1 To 15)
This declaration creates an array that has three dimensions with sizes 4 by 10 by 15. The total number of elements is the product of these three dimensions, or 600.
Note When you start adding dimensions to an array, the total storage needed by the array increases dramatically, so use multidimensional arrays with care. Be especially careful with Variant arrays, because they are larger than other data types.
Using Loops to Manipulate Arrays You can efficiently process a multidimensional array by using nested For loops. For example, these statements initialize every element in MatrixA to a value based on its location in the array:
Dim I As Integer, J As Integer
Static MatrixA(1 To 10, 1 To 10) As Double
For I = 1 To 10
For J = 1 To 10
MatrixA(I, J) = I * 10 + J
Next J
Next I
Dynamic Arrays
Sometimes you may not know exactly how large to make an array. You may want to have the capability of changing the size of the array at run time.
A dynamic array can be resized at any time. Dynamic arrays are among the most flexible and convenient features in Visual Basic, and they help you to manage memory efficiently. For example, you can use a large array for a short time and then free up memory to the system when you're no longer using the array.
The alternative is to declare an array with the largest possible size and then ignore array elements you don't need. However, this approach, if overused, might cause the operating environment to run low on memory.
To create a dynamic array
- Declare the array with a Public statement (if you want the array to be public) or Dim statement at the module level (if you want the array to be module level), or a Static or Dim statement in a procedure (if you want the array to be local). You declare the array as dynamic by giving it an empty dimension list.
Dim DynArray() - Allocate the actual number of elements with a ReDim statement.
ReDim DynArray(X + 1)
The ReDim statement supports the same syntax used for fixed arrays. Each ReDim can change the number of elements, as well as the lower and upper bounds, for each dimension. However, the number of dimensions in the array cannot change.
ReDim DynArray(4 to 12)
For example, the dynamic array Matrix1 is created by first declaring it at the module level:
Dim Matrix1() As Integer
A procedure then allocates space for the array:
Sub CalcValuesNow ()
.
.
.
ReDim Matrix1(19, 29)
End Sub
The ReDim statement shown here allocates a matrix of 20 by 30 integers (at a total size of 600 elements). Alternatively, the bounds of a dynamic array can be set using variables:
ReDim Matrix1(X, Y)
Note You can assign strings to resizable arrays of bytes. An array of bytes can also be assigned to a variable-length string. Be aware that the number of bytes in a string varies among platforms. On Unicode platforms the same string contains twice as many bytes as it does on a non-Unicode platform.
Function Procedures Visual Basic includes built-in, or intrinsic functions, like Sqr, Cos or Chr. In addition, you can use the Function statement to write your own Function procedures.
The syntax for a Function procedure is:
[Private|Public][Static]Function procedurename (arguments) [As type]
statements
End Function
Like a Sub procedure, a Function procedure is a separate procedure that can take arguments, perform a series of statements, and change the value of its arguments. Unlike a Sub procedure, a Function procedure can return a value to the calling procedure. There are three differences between Sub and Function procedures:
- Generally, you call a function by including the function procedure name and arguments on the right side of a larger statement or expression (returnvalue = function()).
- Function procedures have data types, just as variables do. This determines the type of the return value. (In the absence of an As clause, the type is the default Variant type.)
- You return a value by assigning it to the procedurename itself. When the Function procedure returns a value, this value can then become part of a larger expression.
Function Hypotenuse (A As Integer, B As Integer) _
As String
Hypotenuse = Sqr(A ^ 2 + B ^ 2)
End Function
You call a Function procedure the same way you call any of the built-in functions in Visual Basic:
Label1.Caption = Hypotenuse(CInt(Text1.Text), _
CInt(Text2.Text))
strX = Hypotenuse(Width, Height)
Working with Procedures Creating New Procedures
To create a new general procedure
- Type a procedure heading in the Code window and press ENTER. The procedure heading can be as simple as Sub or Function followed by a name. For example, you can enter either of the following:
Sub UpdateForm ()
Function GetCoord ()
Selecting Existing Procedures
To view a procedure in the current module
To view an existing general procedure, select "(General)" from the Object box in the Code window, and then select the procedure in the Procedure box.
–or–
To view an event procedure, select the appropriate object from the Object box in the Code window, and then select the event in the Procedure box.
To view a procedure in another module
- From the View menu, choose Object Browser.
- Select the project from the Project/Library box.
- Select the module from the Classes list, and the procedure from the Members of list.
- Choose View Definition.
Calling Sub Procedures
Sub procedure differs from a Function procedure in that a Sub procedure cannot be called by using its name within an expression. A call to a Sub is a stand-alone statement. Also, a Sub does not return a value in its name as does a function. However, like a Function, a Sub can modify the values of any variables passed to it.
There are two ways to call a Sub procedure:
' Both of these statements call a Sub named MyProc.
Call MyProc (FirstArgument, SecondArgument)
MyProc FirstArgument, SecondArgument
Note that when you use the Call syntax, arguments must be enclosed in parentheses. If you omit the Call keyword, you must also omit the parentheses around the arguments.
Calling Function Procedures
Usually, you call a function procedure you've written yourself the same way you call an intrinsic Visual Basic function like Abs; that is, by using its name in an expression:
' All of the following statements would call a function
' named ToDec.
Print 10 * ToDec
X = ToDec
If ToDec = 10 Then Debug.Print "Out of Range"
X = AnotherFunction(10 * ToDec)
It's also possible to call a function just like you would call a Sub procedure. The following statements both call the same function:
Call Year(Now)
Year Now
When you call a function this way, Visual Basic throws away the return value.
Calling Procedures in Other Modules Public procedures in other modules can be called from anywhere in the project. You might need to specify the module that contains the procedure you're calling. The techniques for doing this vary, depending on whether the procedure is located in a form, class, or standard module.
Procedures in Forms All calls from outside the form module must point to the form module containing the procedure. If a procedure named SomeSub is in a form module called Form1, then you can call the procedure in Form1 by using this statement:
Call Form1.SomeSub(arguments)
Procedures in Class Modules Like calling a procedure in a form, calling a procedure in a class module requires that the call to the procedure be qualified with a variable that points to an instance of the class. For example, DemoClass is an instance of a class named Class1:
Dim DemoClass as New Class1
DemoClass.SomeSub
However, unlike a form, the class name cannot be used as the qualifier when referencing an instance of the class. The instance of the class must be first be declared as an object variable (in this case, DemoClass) and referenced by the variable name.
Procedures in Standard Modules If a procedure name is unique, you don't need to include the module name in the call. A call from inside or outside the module will refer to that unique procedure. A procedure is unique if it appears only in one place.
If two or more modules contain a procedure with the same name, you may need to qualify it with the module name. A call to a common procedure from the same module runs the procedure in that module. For example, with a procedure named CommonName in Module1 and Module2, a call to CommonName from Module2 will run the CommonName procedure in Module2, not the CommonName procedure in Module1.
A call to a common procedure name from another module must specify the intended module. For example, if you want to call the CommonName procedure in Module2 from Module1, use:
Module2.CommonName(arguments)
Passing Arguments to Procedures Usually the code in a procedure needs some information about the state of the program to do its job. This information consists of variables passed to the procedure when it is called. When a variable is passed to a procedure, it is called an argument.
Argument Data Types The arguments for procedures you write have the Variant data type by default. However, you can declare other data types for arguments. For example, the following function accepts a string and an integer:
Function WhatsForLunch(WeekDay As String, Hour _
As Integer) As String
' Returns a lunch menu based on the day and time.
If WeekDay = "Friday" then
WhatsForLunch = "Fish"
Else
WhatsForLunch = "Chicken"
End If
If Hour > 4 Then WhatsForLunch = "Too late"
End Function
Passing Arguments By Value Only a copy of a variable is passed when an argument is passed by value. If the procedure changes the value, the change affects only the copy and not the variable itself. Use the ByVal keyword to indicate an argument passed by value.
For example:
Sub PostAccounts(ByVal intAcctNum as Integer)
.
. ' Place statements here.
.
End Sub
Passing Arguments By Reference Passing arguments by reference gives the procedure access to the actual variable contents in its memory address location. As a result, the variable's value can be permanently changed by the procedure to which it is passed. Passing by reference is the default in Visual Basic.
If you specify a data type for an argument passed by reference, you must pass a value of that type for the argument. You can work around this by passing an expression, rather than a data type, for an argument. Visual Basic evaluates an expression and passes it as the required type if it can.
The simplest way to turn a variable into an expression is to enclose it in parentheses. For example, to pass a variable declared as an integer to a procedure expecting a string as an argument, you would do the following:
Sub CallingProcedure()
Dim intX As Integer
intX = 12 * 3
Foo(intX)
End Sub
Sub Foo(Bar As String)
MsgBox Bar 'The value of Bar is the string "36".
End Sub
Using Optional Arguments
You can specify arguments to a procedure as optional by placing the Optional keyword in the argument list. If you specify an optional argument, all subsequent arguments in the argument list must also be optional and declared with the Optional keyword. The two pieces of sample code below assume there is a form with a command button and list box.
For example, this code provides all optional arguments:
Dim strName As String
Dim strAddress As String
Sub ListText(Optional x As String, Optional y _
As String)
List1.AddItem x
List1.AddItem y
End Sub
Private Sub Command1_Click()
strName = "yourname"
strAddress = 12345 ' Both arguments are provided.
Call ListText(strName, strAddress)
End Sub
This code, however, does not provide all optional arguments:
Dim strName As String
Dim varAddress As Variant
Sub ListText(x As String, Optional y As Variant)
List1.AddItem x
If Not IsMissing(y) Then
List1.AddItem y
End If
End Sub
Private Sub Command1_Click()
strName = "yourname" ' Second argument is not
' provided.
Call ListText(strName)
End Sub
In the case where an optional argument is not provided, the argument is actually assigned as a variant with the value of Empty. The example above shows how to test for missing optional arguments using the IsMissing function.
Providing a Default for an Optional Argument It's also possible to specify a default value for an optional argument. The following example returns a default value if the optional argument isn't passed to the function procedure:
Sub ListText(x As String, Optional y As _
Integer = 12345)
List1.AddItem x
List1.AddItem y
End Sub
Private Sub Command1_Click()
strName = "yourname" ' Second argument is not
' provided.
Call ListText(strName) ' Adds "yourname" and
' "12345".
End Sub
Using an Indefinite Number of Arguments Generally, the number of arguments in the procedure call must be the same as in the procedure specification. Using the ParamArray keyword allows you to specify that a procedure will accept an arbitrary number of arguments. This allows you to write functions like Sum:
Dim x As Integer
Dim y As Integer
Dim intSum As Integer
Sub Sum(ParamArray intNums())
For Each x In intNums
y = y + x
Next x
intSum = y
End Sub
Private Sub Command1_Click()
Sum 1, 3, 5, 7, 8
List1.AddItem intSum
End Sub
Creating Simpler Statements with Named Arguments For many built-in functions, statements, and methods, Visual Basic provides the option of using named arguments as a shortcut for typing argument values. With named arguments, you can provide any or all of the arguments, in any order, by assigning a value to the named argument. You do this by typing the argument name plus a colon followed by an equal sign and the value ( MyArgument:= "SomeValue") and placing that assignment in any sequence delimited by commas. Notice that the arguments in the following example are in the reverse order of the expected arguments:
Function ListText(strName As String, Optional strAddress As String)
List1.AddItem strName
List2.AddItem strAddress
End Sub
Private Sub Command1_Click()
ListText strAddress:="12345", strName:="Your Name"
End Sub
This is especially useful if your procedures have several optional arguments that you do not always need to specify.
Determining Support for Named Arguments
To determine which functions, statements, and methods support named arguments, use the AutoQuickInfo feature in the Code window, check the Object Browser, or see the Language Reference. Consider the following when working with named arguments:
- Named arguments are not supported by methods on objects in the Visual Basic (VB) object library. They are supported by all language keywords in the Visual Basic for applications (VBA) object library.
- In syntax, named arguments are shown as bold and italic. All other arguments are shown in italic only.
Visual Basic procedures can test conditions and then, depending on the results of that test, perform different operations. The decision structures that Visual Basic supports include:
- If...Then
- If...Then...Else
- Select Case
Use an If...Then structure to execute one or more statements conditionally. You can use either a single-line syntax or a multiple-line block syntax:
If condition Then statement
If condition Then
statements
End If
The condition is usually a comparison, but it can be any expression that evaluates to a numeric value. Visual Basic interprets this value as True or False; a zero numeric value is False, and any nonzero numeric value is considered True. If condition is True, Visual Basic executes all the statements following the Then keyword. You can use either single-line or multiple-line syntax to execute just one statement conditionally (these two examples are equivalent):
If anyDate < Now Then anyDate = Now
If anyDate < Now Then
anyDate = Now
End If
Notice that the single-line form of If...Then does not use an End If statement. If you want to execute more than one line of code when condition is True, you must use the multiple-line block If...Then...End If syntax.
If anyDate < Now Then
anyDate = Now
Timer1.Enabled = False ' Disable timer control.
End If
If...Then...Else
Use an If...Then...Else block to define several blocks of statements, one of which will execute:
If condition1 Then
[statementblock-1]
[ElseIf condition2 Then
[statementblock-2]] ...
[Else
[statementblock-n]]
End If
Visual Basic first tests condition1. If it's False, Visual Basic proceeds to test condition2, and so on, until it finds a True condition. When it finds a True condition, Visual Basic executes the corresponding statement block and then executes the code following the End If. As an option, you can include an Else statement block, which Visual Basic executes if none of the conditions are True.
If...Then…ElseIf is really just a special case of If...Then...Else. Notice that you can have any number of ElseIf clauses, or none at all. You can include an Else clause regardless of whether you have ElseIf clauses.
For example, your application could perform different actions depending on which control in a menu control array was clicked:
Private Sub mnuCut_Click (Index As Integer)
If Index = 0 Then ' Cut command.
CopyActiveControl ' Call general procedures.
ClearActiveControl
ElseIf Index = 1 Then ' Copy command.
CopyActiveControl
ElseIf Index = 2 Then ' Clear command.
ClearActiveControl
Else ' Paste command.
PasteActiveControl
End If
End Sub
Notice that you can always add more ElseIf parts to your If...Then structure. However, this syntax can get tedious to write when each ElseIf compares the same expression to a different value. For this situation, you can use a Select Case decision structure.
Select Case
Visual Basic provides the Select Case structure as an alternative to If...Then...Else for selectively executing one block of statements from among multiple blocks of statements. A Select Case statement provides capability similar to the If...Then...Else statement, but it makes code more readable when there are several choices.
A Select Case structure works with a single test expression that is evaluated once, at the top of the structure. Visual Basic then compares the result of this expression with the values for each Case in the structure. If there is a match, it executes the block of statements associated with that Case:
Select Case testexpression
[Case expressionlist1
[statementblock-1]]
[Case expressionlist2
[statementblock-2]]
.
.
.
[Case Else
[statementblock-n]]
End Select
Each expressionlist is a list of one or more values. If there is more than one value in a single list, the values are separated by commas. Each statementblock contains zero or more statements. If more than one Case matches the test expression, only the statement block associated with the first matching Case will execute. Visual Basic executes statements in the Case Else clause (which is optional) if none of the values in the expression lists matches the test expression.
For example, suppose you added another command to the Edit menu in the If...Then...Else example. You could add another ElseIf clause, or you could write the function with Select Case:
Private Sub mnuCut_Click (Index As Integer)
Select Case Index
Case 0 ' Cut command.
CopyActiveControl ' Call general procedures.
ClearActiveControl
Case 1 ' Copy command.
CopyActiveControl
Case 2 ' Clear command.
ClearActiveControl
Case 3 ' Paste command.
PasteActiveControl
Case Else
frmFind.Show ' Show Find dialog box.
End Select
End Sub
Notice that the Select Case structure evaluates an expression once at the top of the structure. In contrast, the If...Then...Else structure can evaluate a different expression for each ElseIf statement. You can replace an If...Then...Else structure with a Select Case structure only if the If statement and each ElseIf statement evaluates the same expression.
Loop Structures
Loop structures allow you to execute one or more lines of code repetitively. The loop structures that Visual Basic supports include:
- Do...Loop
- For...Next
- For Each...Next
Use a Do loop to execute a block of statements an indefinite number of times. There are several variations of the Do...Loop statement, but each evaluates a numeric condition to determine whether to continue execution. As with If...Then, the condition must be a value or expression that evaluates to False (zero) or to True (nonzero).
In the following Do...Loop, the statements execute as long as the condition is True:
Do While condition
statements
Loop
When Visual Basic executes this Do loop, it first tests condition. If condition is False (zero), it skips past all the statements. If it's True (nonzero), Visual Basic executes the statements and then goes back to the Do While statement and tests the condition again.
Consequently, the loop can execute any number of times, as long as condition is nonzero or True. The statements never execute if condition is initially False. For example, this procedure counts the occurrences of a target string within another string by looping as long as the target string is found:
Function CountStrings (longstring, target)
Dim position, count
position = 1
Do While InStr(position, longstring, target)
position = InStr(position, longstring, target)_
+ 1
count = count + 1
Loop
CountStrings = count
End Function
If the target string doesn't occur in the other string, then InStr returns 0, and the loop doesn't execute.
Another variation of the Do...Loop statement executes the statements first and then tests condition after each execution. This variation guarantees at least one execution of statements:
Do
statements
Loop While condition
Two other variations are analogous to the previous two, except that they loop as long as condition is False rather than True.
For...Next
Do loops work well when you don't know how many times you need to execute the statements in the loop. When you know you must execute the statements a specific number of times, however, a For·Next loop is a better choice. Unlike a Do loop, a For loop uses a variable called a counter that increases or decreases in value during each repetition of the loop. The syntax is:
For counter = start To end [Step increment]
statements
Next [counter]
The arguments counter, start, end, and increment are all numeric.
Note The increment argument can be either positive or negative. If increment is positive, start must be less than or equal to end or the statements in the loop will not execute. If increment is negative, start must be greater than or equal to end for the body of the loop to execute. If Step isn't set, then increment defaults to 1.
In executing the For loop, Visual Basic
- Sets counter equal to start.
- Tests to see if counter is greater than end. If so, Visual Basic exits the loop.
(If increment is negative, Visual Basic tests to see if counter is less than end.) - Executes the statements
- Increments counter by 1 or by increment, if it's specified.
- Repeats steps 2 through 4.
Private Sub Form_Click ()
Dim I As Integer
For i = 0 To Screen.FontCount
Print Screen.Fonts(i)
Next
End Sub
In the VCR sample application, the HighlightButton procedure uses a For...Next loop to step through the controls collection of the VCR form and show the appropriate Shape control:
Sub HighlightButton(MyControl As Variant)
Dim i As Integer
For i = 0 To frmVCR.Controls.Count - 1
If TypeOf frmVCR.Controls(i) Is Shape Then
If frmVCR.Controls(i).Name = MyControl Then
frmVCR.Controls(i).Visible = True
Else
frmVCR.Controls(i).Visible = False
End If
End If
Next
End Sub
For Each...Next
For Each...Next loop is similar to a For...Next loop, but it repeats a group of statements for each element in a collection of objects or in an array instead of repeating the statements a specified number of times. This is especially helpful if you don't know how many elements are in a collection.
Here is the syntax for the For Each...Next loop:
For Each element In group
statements
Next element
For example, the following Sub procedure opens Biblio.mdb and adds the name of each table to a list box.
Sub ListTableDefs()
Dim objDb As Database
Dim MyTableDef as TableDef
Set objDb = OpenDatabase("c:\vb\biblio.mdb", _
True, False)
For Each MyTableDef In objDb.TableDefs()
List1.AddItem MyTableDef.Name
Next MyTableDef
End Sub
Keep the following restrictions in mind when using For Each...Next:
Nested Control Structures
You can place control structures inside other control structures (such as an If...Then block within a For...Next loop). A control structure placed inside another control structure is said to be nested.
Control structures in Visual Basic can be nested to as many levels as you want. It's common practice to make nested decision structures and loop structures more readable by indenting the body of the decision structure or loop.
For example, this procedure prints all the font names that are common to both the Printer and Screen:
Private Sub Form_Click()
Dim SFont, PFont
For Each SFont In Screen.Fonts()
For Each PFont In Printer.Fonts()
If SFont = PFont Then
Print SFont
End If
Next PFont
Next SFont
End Sub
Notice that the first Next closes the inner For loop and the last For closes the outer For loop. Likewise, in nested If statements, the End If statements automatically apply to the nearest prior If statement. Nested Do...Loop structures work in a similar fashion, with the innermost Loop statement matching the innermost Do statement.
Exiting a Control Structure
The Exit statement allows you to exit directly from a For loop, Do loop, Sub procedure, or Function procedure. The syntax for the Exit statement is simple: Exit For can appear as many times as needed inside a For loop, and Exit Do can appear as many times as needed inside a Do loop:
For counter = start To end [Step increment]
[statementblock]
[Exit For]
[statementblock]
Next [counter[, counter] [,...]]
Do [{While | Until} condition]
[statementblock]
[Exit Do]
[statementblock]
Loop
The Exit Do statement works with all versions of the Do loop syntax.
Exit For and Exit Do are useful because sometimes it's appropriate to quit a loop immediately, without performing any further iterations or statements within the loop. For example, in the previous example that printed the fonts common to both the Screen and Printer, the code continues to compare Printer fonts against a given Screen font even when a match has already been found with an earlier Printer font. A more efficient version of the function would exit the loop as soon as a match is found:
Private Sub Form_Click()
Dim SFont, PFont
For Each SFont In Screen.Fonts()
For Each PFont In Printer.Fonts()
If SFont = PFont Then
Print Sfont
Exit For ' Exit inner loop.
End If
Next PFont
Next SFont
End Sub As this example illustrates, an Exit statement almost always appears inside an If statement or Select Case statement nested inside the loop.
Exiting a Sub or Function Procedure
You can also exit a procedure from within a control structure. The syntax of Exit Sub and Exit Function is similar to that of Exit For and Exit Do in the previous section, "Exiting a Control Structure." Exit Sub can appear as many times as needed, anywhere within the body of a Sub procedure. Exit Function can appear as many times as needed, anywhere within the body of a Function procedure.
Exit Sub and Exit Function are useful when the procedure has done everything it needs to do and can return immediately. For example, if you want to change the previous example so it prints only the first common Printer and Screen font it finds, you would use Exit Sub:
Private Sub Form_Click()
Dim SFont, PFont
For Each SFont In Screen.Fonts()
For Each PFont In Printer.Fonts()
If SFont = PFont Then
Print Sfont
Exit Sub ' Exit the procedure.
End If
Next PFont
Next SFont
End Sub
What is an Object?
An object is a combination of code and data that can be treated as a unit. An object can be a piece of an application, like a control or a form. An entire application can also be an object. The following table describes examples of the types of objects you can use in Visual Basic.
Example
Description Command button Controls on a form, such as command buttons and frames, are objects. Form Each form in a Visual Basic project is a separate object. Database Databases are objects, and contain other objects, like fields and indexes Chart A chart in Microsoft Excel is an object
All objects are created as identical copies of their class. Once they exist as individual objects, their properties can be changed. For example, if you draw three command buttons on a form, each command button object is an instance of the CommandButton class. Each object shares a common set of characteristics and capabilities (properties, methods, and events), defined by the class. However, each has its own name, can be separately enabled and disabled, can be placed in a different location on the form, and so on. For simplicity, most of the material outside of this chapter won't make many references to an object's class. Just remember that the term "list box control," for example, means "an instance of the ListBox class."
Controls collection
For example, the following code scrolls through the Controls collection and lists each member's name in a list box.
Dim MyControl as Control
For Each MyControl In Form1.Controls
' For each control, add its name to a list box.
List1.AddItem MyControl.Name
Next MyControl