Jeff Webb, Mike McKelvy, Ronald Martinsen, Taylor Maxwell, Michael Regelski September 1995 Special Edition Using Visual Basic 4 - Chapter 32 1-56529-998-1 Computer Programming computer programming Visual Basic OLE database applications ODBC VB VBA API This book is an all-in-one reference that provides extensive coverage of every topic and technique for creating optimized and customized applications with Visual Basic.

Chapter 32

Advanced Code Techniques


There is no single way to solve a technical programming problem. Many factors go into choosing a programming solution for a problem. Visual Basic properties, methods, and events define what you can and cannot do. You must find ways to work around bugs when they get in the way of providing a solution. The Windows environment itself presents you with challenges of capabilities and limitations.

When writing code to solve a client’s problem, you usually must do so as quickly as possible. Foreknowledge of efficient coding techniques and common technical pitfalls can make writing code a faster process. Being aware of potential resources that provide such knowledge can prepare you to develop such techniques and avoid such pitfalls before difficulties even arise.

In this chapter, you learn how to do the following:

The MSBASIC Forum on CompuServe Information Services (CIS)

The CompuServe online service provides a wonderful resource for finding solutions to programming problems. Join the MSBASIC forum and talk to other Visual Basic programmers who might have solutions to your current problems. You can obtain a membership copy from most computer software stores, or call 1-800-848-8990 to order a copy directly.

Minimizing CompuServe Charges

CompuServe charges can add up as you increasingly use this valuable resource. One way to reduce your online charges is to use an auto-navigator. An auto-navigator reduces your online time by first obtaining the headers of all the new messages; you can then choose which ones you want to read. Then the auto-navigator retrieves your selected messages to enable you to read them offline rather than online—and thus save money. An example of such an auto-navigator is Navcis, which is available in the DVORAK forum, but many other good ones are available. You should try some to discover one that best fits your needs.

Understanding Configuration Options

Visual Basic supports the two most common methods for storing application information: .INI files and the Windows Registry. Support for .INI files exists for compatibility with earlier versions of Windows and Visual Basic. The Windows Registry is the current accepted location for placing application information.

Application .INI Files

Every application should have its own .INI file. This file provides a way to save settings between application sessions. Many Windows applications still store settings in the WIN.INI Windows configuration file. This creates problems when the WIN.INI file grows beyond the 64K limit. Under these circumstances, Windows no longer runs on that machine until someone reduces the .INI file’s size. Remember to use an application .INI file rather than WIN.INI.

Visual Basic provides no internal functions for creating and maintaining application .INI files. To work with .INI files, you must work directly with the Windows API. Three basic API functions allow access to application .INI files: GetPrivateProfileString, GetPrivateProfileInt, and WritePrivateProfileString. GetPrivateProfileString provides a method for obtaining text or values from an indicated .INI file. The API calls return these settings as a string. GetPrivateProfileInt returns values stored in the indicated .INI file. WritePrivateProfileString saves indicated strings to the indicated .INI file. Chapter 33, “Accessing the Windows API,” contains the declarations and syntax for using these routines.

The Windows Registry

Windows 95 and NT place their system configuration information in the Registry database. This is a change from the older .INI system found in Windows 3.x systems. OLE applications like Microsoft Office broke this trend by using the Registry database, but such applications are the exception rather than the rule. Any entries in the old WIN.INI and SYSTEM.INI files are there simply for compatibility reasons. The Registry keeps information in binary files. An application uses the Registry API functions to access data in the Registry.

Visual Basic provides no internal functions for creating and maintaining the Windows Registry. To work with the Windows Registry, you must work directly with the Windows 32 API. Seven API functions allow access to the Windows Registry: RegOpenKeyEx, RegQueryValueEx, RegCloseKey, RegCreateKeyEx, RegSetValueEx, RegDeleteKey, and RegDeleteValue. RegOpenKeyEx enables you to open a Registry key so that you can work with its contents. Use RegQueryValueEx to obtain the settings that belong to a key in the Registry. RegCreateKeyEx creates new Registry keys and RegSetValueEx assigns settings to a key. RegDeleteKey removes keys from the Registry, and RegDeleteValue removes a value from a key. Chapter 33 contains the declarations and syntax for using these routines.

Optimizing the Application Startup Process

An application’s startup process gives the user a first impression of the entire application. If you design an application that has a long startup, you give the user the impression that your entire application is slow. Unfortunately, at program startup, it is important for an application must make calculations and check settings. Therefore, you are faced with a challenge: to design applications that don’t seem slow yet still handle configuration issues at startup. You can use several techniques to avoid this dilemma.

Understanding Pseudo Constants

The following two lines of code define the constants LINE_FEED and TAB_STOP:

' initialize global strings
LINE_FEED = Chr$(10)
TAB_STOP = Chr$(9)

At least these variables seem to be formatted as constants. Constants normally appear in the General Declarations sections of modules and rarely forms. Unfortunately, you cannot define constants by using functions such as Chr$. The code has to set these variables at run time. These variables are not constants, therefore, but simply behave as constants because you define them only once in the code. For this reason, such variables are called pseudo constants. By using such variables, you need only remember a pseudo constant’s name instead of having to remember, for example, the ASCII code for the linefeed or tab character.

These pseudo constants allow for carefully formatted message box text throughout your projects. You can now space message text within a text box by placing a space between logical phrases. For example, you can place “Warning, Warning!” at the top of a message and then one line further down “You are about to delete data permanently” by inserting two linefeed constant statements in the string assigned to the message box.

Starting with Code

Visual Basic’s Project tab appears on the Options dialog box (which you access by choosing Tools, Options). This dialog box provides a method for identifying the startup form. Along with the names of each of the forms in the current project, you find the strangely out-of-place Sub Main. But you have no form named Sub Main, and when you try to add this subroutine to any of your forms, it does not work properly. The solution is simple: create a new module and place in it a subroutine named Sub Main. Figure 32.1 shows the Options dialog box in which the Sub Main choice appears in the Project tab.

Fig. 32.1

The Options dialog box with Startup Form set to Sub Main

Setup Controls

The project 32Proj01 demonstrates a possible use for Sub Main. To function properly, Visual Basic applications still must be aware of DOS paths. Users might not want to place their copy of your applications in the same directories that you intended. How do you discover what path that the user wants to use? The file 32Proj01 on the companion CD includes a rudimentary method for letting the user indicate the path. Figure 32.2 displays the form that you need to construct. Use this figure as a model as you proceed through the following steps:

Fig. 32.2

The frmDataLocation form at design time.

  1. Create a new form and save it under the DOS name Data Location.
  2. Change the Visual Basic name to frmDataLocation.
  3. Draw a frame on the form and rename it as fraPath.
  4. Make the caption of the new frame “Directory Path.”
  5. Draw three labels on the frame and rename them lblDatabaseName, lblDatabase, and lblPath.
  6. Add a command button, rename it cmdOk, and give it the label “OK.”
  7. Add a comand button, rename it cmdCancel, and give it the label “Cancel.”
  8. Create a new module and rename it STARTUP.BAS.
  9. Add to the frame a text box and rename it txtPath.
  10. Add to the frame a check box and rename it chkPressed.
  11. Add GetDatabasePath, Main, and SaveDatabasePath to STARTUP.BAS.
  12. Add cmdOK_Click and cmdCancel_Click to frmDataLocation.
  13. Create a new module and rename it WINAPI.BAS.
  14. Add the API declarations and code to WINAPI.BAS.

Application Paths

Sub Main contains this project’s startup code (see listing 32.1). First this routine loads frmDataLocation and displays the path stored in the Registry or the current application path in the Path text box. Next the code displays frmDataLocation modally and waits for the user to exit from that form. The path placed in the text box on frmDataLocation provides the path for Sub Main to save into the Registry if the user presses the OK command button.

Listing 32.1 The Sub Main Routine

Sub Main()
Load frmDataLocation
frmDataLocation!txtPath.TEXT = GetDatabasePath
frmDataLocation.Show vbModal
If frmDataLocation!chkPressed.VALUE Then
Call SaveDatabasePath(Trim$(frmDataLocation!txtPath.TEXT))
End If
End Sub
Unload frmDataLocation
End Sub

The GetDatabasePath Function

Listing 32.2 shows the GetDatabasePath function. Notice how you use App.Path to provide the database’s default path. The reference to App.Path appears to path to the current directory if the Registry doesn’t store a database path. Chapter 33, “Accessing the Windows API” features a complete discussion of the API calls for the Registry.

Listing 32.2 The GetDatabasePath Function

Private Function GetDatabasePath() As String
Dim lReturn As Long
Dim sClass As String
Dim lDisposition As Long
Dim Security As SECURITY_ATTRIBUTES
Dim sDatabasePath As String
Dim hWndQue As Long
Dim lLength As Long
'Create or Open Que Registry entry for local machine
sClass = Space$(255)
lReturn = RegCreateKeyEx(HKEY_LOCAL_MACHINE,"QUE",0&,sClass, _
REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, Security, hWndQue, _
lDisposition)
If lReturn <> ERROR_SUCCESS Then
gAPIDisplayError lReturn
End If
lLength = 255
sDatabasePath = Space$(lLength)
lReturn = RegQueryValueEx(hWndQue, "DatabasePath", 0&, REG_SZ, _
sDatabasePath, lLength)
If lReturn <> ERROR_SUCCESS Then
GetDatabasePath = App.Path
Else
GetDatabasePath = Left$(sDatabasePath, lLength - 1)
End If
'Close open keys
lReturn = RegCloseKey(hWndQue)
End Function

The SaveDatabasePath Function

SaveDatabasePath (listing 32.3) provides a way to save the database path to the Registry. Chapter 34 includes a complete discussion of the Registry API database calls.

Listing 32.3 The SaveDatabasePath Function

Private Sub SaveDatabasePath(sDatabasePath As String)
Dim lReturn As Long
Dim hWndQue As Long
Dim sClass As String
Dim Security As SECURITY_ATTRIBUTES
Dim lDisposition As Long
'Create or Open Que Registry entry for local machine
sClass = Space$(255)
lReturn = RegCreateKeyEx(HKEY_LOCAL_MACHINE,"QUE",0&,sClass, _
REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, Security, hWndQue, _
lDisposition)
If lReturn <> ERROR_SUCCESS Then
gAPIDisplayError lReturn
End If
'Save Database path in the Registry
lReturn = RegSetValueEx(hWndQue, "DatabasePath", 0&, REG_SZ, _
sDatabasePath, Len(sDatabasePath))
If lReturn <> ERROR_SUCCESS Then
gAPIDisplayError lReturn
End If
'Close open keys
lReturn = RegCloseKey(hWndQue)
End Sub

The cmdOk_Click and cmdCancel_Click Events

Notice that each of the command-button click events (listing 32.4) hides frmDataLocation instead of unloading it. This is important because you need to access the contents of txtPath in the Sub Main routine. Because you display frmDataLocation modally, none of the code that follows frmDataLocation.Show vbModal processes until the form unloads or becomes invisible. Using the Hide method makes frmDataLocation invisible, thus enabling the processing of the code that follows the Show method in Sub Main.

Listing 32.4 The cmdOk_Click and cmdCancel_Click Events

Private Sub cmdOK_Click()
chkPressed.VALUE = 1
Me.Hide
End Sub
Private Sub cmdCancel_Click()
chkPressed.VALUE = False
Me.Hide
End Sub

The Pressed check box on frmDataLocation provides an excellent, easy way to determine whether the user pressed the Cancel or OK button. Another way to tell the Sub Main routine which button the user pressed is to use a global variable.

Global Variables

Several global variables are necessary to operate this project’s code. Listing 32.5 shows this project’s global variables.

Listing 32.5 The Global Variables

Declare Function RegCloseKey Lib "advapi32" _
(ByVal hKey As Long) As Long
Declare Function RegCreateKeyEx Lib "advapi32" Alias _
"RegCreateKeyExA" (ByVal hKey As Long, ByVal lpSubKey As String, _
ByVal Reserved As Long, ByVal lpClass As String, _
ByVal dwOptions As Long, ByVal samDesired As Long, _
lpSecurityAttributes As SECURITY_ATTRIBUTES, phkResult As Long, _
lpdwDisposition As Long) As Long
Declare Function RegOpenKeyEx Lib "advapi32" Alias _
"RegOpenKeyExA" (ByVal hKey As Long, ByVal lpSubKey As String, _
ByVal ulOptions As Long, ByVal samDesired As Long, _
phkResult As Long) As Long
Declare Function RegQueryValueEx Lib "advapi32" Alias _
"RegQueryValueExA" (ByVal hKey As Long, _
ByVal lpValueName As String, lpReserved As Long, _
lpType As Long, ByVal lpData As String, lpcbData As Long) As Long
Declare Function RegSetValueEx Lib "advapi32" Alias _
"RegSetValueExA" (ByVal hKey As Long, _
ByVal lpValueName As String, ByVal Reserved As Long,
ByVal dwType As Long, ByVal lpData As String, _
ByVal cbData As Long) As Long
'Security Mask constants
Public Const READ_CONTROL = &H20000
Public Const SYNCHRONIZE = &H100000
Public Const STANDARD_RIGHTS_ALL = &H1F0000
Public Const STANDARD_RIGHTS_READ = READ_CONTROL
Public Const STANDARD_RIGHTS_WRITE = READ_CONTROL
Public Const KEY_QUERY_VALUE = &H1
Public Const KEY_SET_VALUE = &H2
Public Const KEY_CREATE_SUB_KEY = &H4
Public Const KEY_ENUMERATE_SUB_KEYS = &H8
Public Const KEY_NOTIFY = &H10
Public Const KEY_CREATE_LINK = &H20
Public Const KEY_ALL_ACCESS = ((STANDARD_RIGHTS_ALL Or _
KEY_QUERY_VALUE Or KEY_SET_VALUE Or KEY_CREATE_SUB_KEY Or _
KEY_ENUMERATE_SUB_KEYS Or KEY_NOTIFY Or KEY_CREATE_LINK) _
And (Not SYNCHRONIZE))
Public Const KEY_READ = ((STANDARD_RIGHTS_READ Or _
KEY_QUERY_VALUE Or KEY_ENUMERATE_SUB_KEYS Or KEY_NOTIFY) _
And (Not SYNCHRONIZE))
Public Const KEY_EXECUTE = ((KEY_READ) And (Not SYNCHRONIZE))
Public Const KEY_WRITE = ((STANDARD_RIGHTS_WRITE Or _
KEY_SET_VALUE Or KEY_CREATE_SUB_KEY) And (Not SYNCHRONIZE))
'Predefined Registry Keys used in hKey Argument
Public Const HKEY_CLASSES_ROOT = &H80000000
Public Const HKEY_CURRENT_USER = &H80000001
Public Const HKEY_LOCAL_MACHINE = &H80000002
Public Const HKEY_USERS = &H80000003
Public Const HKEY_PERFORMANCE_DATA = &H80000004
' Return codes from Registration functions.
Public Const ERROR_SUCCESS = 0&
Public Const ERROR_BADDB = 1009&
Public Const ERROR_BADKEY = 1010&
Public Const ERROR_CANTOPEN = 1011&
Public Const ERROR_CANTREAD = 1012&
Public Const ERROR_CANTWRITE = 1013&
Public Const ERROR_OUTOFMEMORY = 14&
Public Const ERROR_INVALID_PARAMETER = 87&
Public Const ERROR_ACCESS_DENIED = 5&
'Data type Public constants
Public Const REG_NONE = 0
Public Const REG_SZ = 1
Public Const REG_EXPAND_SZ = 2
Public Const REG_BINARY = 3
Public Const REG_DWORD = 4
Public Const REG_DWORD_LITTLE_ENDIAN = 4
Public Const REG_DWORD_BIG_ENDIAN = 5
Public Const REG_LINK = 6
Public Const REG_MULTI_SZ = 7
Public Const REG_RESOURCE_LIST = 8
Public Const REG_FULL_RESOURCE_DESCRIPTOR = 9
Public Const REG_RESOURCE_REQUIREMENTS_LIST = 10
'Options
Public Const REG_OPTION_VOLATILE = 0
Public Const REG_OPTION_NON_VOLATILE = 1
Type SECURITY_ATTRIBUTES
nLength As Long
lpSecurityDescriptor As Variant
bInheritHandle As Long
End Type

Before proceeding any further, you must declare the API calls. Create a new module and rename it WINAPI32.BAS. Copy the code in listing 32.5 into the General Declarations section of this new module. The API declarations and constants are globally available to the entire application and are necessary to make the code in the remainder of this chapter work. Listing 32.6 shows the API call declarations.

Listing 32.6 Declarations of the API Calls

Sub gAPIDisplayError(lCode As Long)
Select Case lCode
Case ERROR_BADDB
MsgBox "Corrupt Registry Database!"
Case ERROR_BADKEY
MsgBox "Key name is bad"
Case ERROR_CANTOPEN
MsgBox "Cannot Open Key"
Case ERROR_CANTREAD
MsgBox "Cannot Read Key"
Case ERROR_CANTWRITE
MsgBox "Cannot Write Key"
Case ERROR_ACCESS_DENIED
MsgBox "Access to Registry Denied"
Case ERROR_OUTOFMEMORY
MsgBox "Out of memory"
Case ERROR_INVALID_PARAMETER
MsgBox "Invalid Parameter"
Case Else
MsgBox "Undefined key error code!"
End Select
End Sub

Make sure that you add the gAPIDisplayError subroutine to WINAPI32. This function provides error checking for the Registry calls. Chapter 34 discusses the API calls and this error-checking routine.

Notice the use of the Visual Basic keyword Public rather than the more familiar Global. This is a major change that Visual Basic 4.0 introduces. Fortunately, Visual Basic 4.0 is backward-compatible, so you need not immediately change to using Public rather than Global. Both keywords behave exactly the same and work properly in a Visual Basic 4.0 project.

The Application Title

To provide some continuity among the different screens, applications need to present a consistent title. Sub Main is an excellent location for setting app.title. By using the Title property of the Visual Basic app object, you can specify that the same message appear for each screen. This property also makes changing the title easy, because you need only change the title in one place to affect each screen. If you had to insert the title text everywhere that you want it to appear, the process of changing the title would be harder and possibly less accurate.

The following code line is the definition of the app.Title object:

app.Title = “Que Publishing”

The Application Help File

Visual Basic provides a place where you can easily set the name of an application’s Help file. Simply set your choice on the Project Options dialog box by choosing the Project tab of the Options dialog box as shown in figure 32.3.

Fig. 32.3

The Project Options dialog box showing the Help File setting.

Unfortunately, if you hard-code the path in this dialog box, the user cannot install your application in another directory. If the user chooses another directory, the Help system does not work properly. Fortunately, an alternative to this unacceptable solution exists: Simply use the app object’s HelpFile property to set the path at run time in the Sub Main subroutine, as follows:

app.HelpFile = app.Path & “\QUE.HLP”

The Splash Form

To create the illusion of a “fast” application, use a splash screen. A splash screen is just a form with a picture showing on screen something about the application—perhaps a company graphic, descriptive text, or even an entertaining picture—to keep the user’s attention while processing takes place in the background. Figure 32.4 shows an example of a splash screen.

Fig. 32.4

The frmSplash displayed at run time.

To create the splash screen shown in figure 32.4, follow these steps:

  1. Create a new form and save it under the DOS name SPLASH.FRM.
  2. Change the Visual Basic name to frmSplash.
  3. Change the borderstyle property to 0-None.
  4. Add THREED.OCX to the project if it does not already exist.
  5. Draw a 3D Frame and size it to fill the screen.
  6. Draw an Image control on the screen and place your own graphic in that image control.

Show versus Load Methods

You often must choose between using a Show or Load method. Show triggers the form’s load event and makes the form visible. Load triggers the same event but leaves the form invisible. To display the form immediately on the screen, simply use Load in Sub Main and change the visible property to True in the form’s Load event.

Chapter 31, “Advanced Form Techniques,” contains a full discussion of multiple-document interface (MDI) forms.

Creating Common Subroutines

Toolbars and menu choices that trigger the exact same code present you with another problem: how to keep their shared code current. The obvious choice is to create a common subroutine that both the menu choice and the toolbar can call. For the sake of simplicity, you should place these common routines in a separate module. By using separate modules, you can easily port code to other applications simply by copying the module from one project to another.

Saving Settings on Exit

Users perceive customizable applications as user-friendly and enjoy working with them. But consider carefully which parts of your application to make customizable. Features like saving column widths in grids and saving the last object opened are excellent choices for customization. The user sometimes does not want to save the settings from one session to another, so you must enable users turn this feature on and off. For example, once satisfied with the column widths in grids, a user does not want to overwrite the settings immediately. In these circumstances, the user need only turn on the feature that saves the settings, save the grid’s column width, and then simply turn off the feature that saves settings.

The shell’s Option menu does not enable the user to save customization choices in an application’s executable file. To save settings, you must use the Windows Registry. Place the GetSavedSettings function in MAIN.BAS. This routine checks for any settings in the Windows Registry and creates the defaults if none exist yet. Before any of this code can work, you must add to the project 32Proj01 the 32-bit API code, which appears in the WINAPI32.BAS module.

Using the gSetValue Subroutine

Unlike the previous projects in this chapter, saving the different settings associated with forms requires many calls to the Registry. You could make the calls exactly as shown in 32ProjJ01, but the code would be longer and less understandable. Instead, use the gSetValue subroutine to make the Registry calls for you.

gSetValue (see listing 32.7) places a setting in the Registry according to the settings of its arguments: sKey, sKeyValue, and sNewValue. sKey identifies the name of the key to set a value and sKeyValue identifies the name of the value. sNewValue contains the string or number to place under the identified value.

Listing 32.7 The gSetValue Subroutine

Sub gSetValue(sKey$, sKeyValue$, sNewValue$)
Dim lReturn As Long
Dim hWndQue As Long
Dim hWnd As Long
Dim sClass As String
Dim Security As SECURITY_ATTRIBUTES
Dim lDisposition As Long
'Create or open Que Registry entry for local machine
sClass = Space$(255)
lReturn = RegCreateKeyEx(HKEY_LOCAL_MACHINE, "QUE", 0&, _
sClass, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, Security, _
hWndQue, lDisposition)
sClass = Space$(255)
lReturn = RegCreateKeyEx(hWndQue, sKey, 0&, sClass, _
REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, Security, hWnd, _
lDisposition)
'Save value in the Registry
lReturn = RegSetValueEx(hWnd, sKeyValue, 0&, REG_SZ, _
sNewValue, Len(sNewValue))
'Close open keys
lReturn = RegCloseKey(hWndQue)
lReturn = RegCloseKey(hWnd)
End Sub

Using the gGetValue Function

gGetValue (listing 32.8) obtains a setting in the Registry according to the settings of the function’s arguments: sKey and sKeyValue. sKey identifies the name of the key to set a value, and skeyValue identifies the value’s name. The gGetValue function returns the contents of the indicated key’s value.

Listing 32.8 The gGetValue Function

Function gGetValue(sKey$, sKeyValue$) As String
Dim lReturn As Long
Dim sClass As String
Dim lDisposition As Long
Dim Security As SECURITY_ATTRIBUTES
Dim sValue As String
Dim hWndQue As Long
Dim hWnd As Long
Dim lLength As Long
'Create or open Que Registry entry for local machine
sClass = Space$(255)
lReturn = RegCreateKeyEx(HKEY_LOCAL_MACHINE,"QUE",0&,sClass, _
REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, Security, hWndQue, _
lDisposition)
sClass = Space$(255)
lReturn = RegCreateKeyEx(hWndQue, sKey, 0&, sClass, _
REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, Security, hWnd, _
lDisposition)
lLength = 255
sValue = Space$(lLength)
lReturn = RegQueryValueEx(hWnd, sKeyValue, 0&, REG_SZ, sValue, _
lLength)
'Close open keys
lReturn = RegCloseKey(hWndQue)
lReturn = RegCloseKey(hWnd)
sValue = Trim$(sValue)
gGetValue = Mid$(sValue, 1, Len(sValue) - 1)
End Function

Using the gGetSettings Subroutine

The GetSavedSettings subroutine accommodates all the settings over which you want to give the user control. This routine currently specifies whether the application should save any settings that change.

You want to create a way for the user to turn the Save Settings On Exit option on and off. To do so, simply create a new menu choice named mnuOptions and labeled “&Options,” with the submenu mnuOptionsSaveSettings with the label “Save Settings On Exit.”

Listing 32.9 contains the code to place in the mnuOptionsSaveSettings_Click event. The code ensures that the user can choose whether to enable or disable the Save Settings On Exit option. To discover the user’s choice, note whether a check mark appears next to the menu choice; if the check mark appears, the user enabled the Save Settings On Exit option.

Listing 32.9 The gGetSettings Function

Sub gGetSettings()
giSaveSettings = Val(gGetValue("Preferences", "SaveSettings"))
frmMain!mnuOptionsSaveSettings.Checked = giSaveSettings
End Sub

Positioning the Form

Where a form appears is at least as important as how it appears. Positioning is a frequently overlooked part of Visual Basic programming; many programmers simply let their forms appear in their default design-time positions. The major problem with this approach is that different users have screens with different resolution sizes. A form that appears in the lower-right corner of an 800-by-600 screen disappears completely off a 640-by-480 screen.

This section introduces some routines that help you fine-tune the positioning of your forms. For the sake of simplicity, place all the position routines in a new module named POSITION.BAS.

MDI Form Position

Higher-resolution screens make programming in Visual Basic easier. They provide more room on which you can display different elements of Visual Basic without creating a confusing screen. Unfortunately, not every user has the luxury of an 800-by-600 resolution screen. Be careful to design screens that adjust their size based on the user’s screen resolution. Listing 32.10 shows the SetMainFrmPosition subroutine, which sizes and positions frmMain on the screen.

Listing 32.10 The SetMainFrmPosition Subroutine

Sub gSetMainFormPosition(frmName As Form)
Dim sWindowState As String
Dim sTopPosition As String
Dim sLeftPosition As String
Dim sFormWidth As String
Dim sFormHeight As String
sWindowState = gGetValue("Main", "WindowState")
Select Case sWindowState
Case "0"
sTopPosition = gGetValue("Main", "Top")
sLeftPosition = gGetValue("Main", "Left")
sFormWidth = gGetValue("Main", "Width")
sFormHeight = gGetValue("Main", "Height")
frmName.Move Val(sLeftPosition), Val(sTopPosition), _
Val(sFormWidth), Val(sFormHeight)
Case "1"
frmName.WindowState = vbMinimized
Case "2"
frmName.WindowState = vbMaximized
Case Else
frmName.Width = 640 * Screen.TwipsPerPixelX
frmName.Height = 480 * Screen.TwipsPerPixelY
If frmName.Width + 100 > Screen.Width Then
frmName.WindowState = vbMaximized
Else
frmName.TOP = (Screen.Height - frmName.Height) \ 2
frmName.Left = (Screen.Width - frmName.Width) \ 2
End If
End Select
End Sub

The gSetMainFormPosition routine changes the appearance of frmMain based on the size of the screen. Therefore, frmMain appears maximized on 640-by-480 resolution screens and centered in 800-by-600 screens.

These choices might be good candidates for a user setting that can save the position of the form so that the next time that the user loads this program, the form appears in exactly the same place. gSaveMainFormPosition (listing 32.11) is a subroutine that gives the user this control.

Listing 32.11 The gSaveMainFormPosition Subroutine

Sub gSaveMainFormPosition(frmName As Form)
Dim sWindowState As String
Dim sTopPosition As String
Dim sLeftPosition As String
Dim sFormWidth As String
Dim sFormHeight As String
If giSaveSettings Then
sWindowState = Trim$(Str$(frmName.WindowState))
Call gSetValue("Main", "WindowState", sWindowState)
sTopPosition = Trim$(Str$(frmName.TOP))
Call gSetValue("Main", "Top", sTopPosition)
sLeftPosition = Trim$(Str$(frmName.Left))
Call gSetValue("Main", "Left", sLeftPosition)
sFormWidth = Trim$(Str$(frmName.Width))
Call gSetValue("Main", "Width", sFormWidth)
sFormHeight = Trim$(Str$(frmName.Height))
Call gSetValue("Main", "Height", sFormHeight)
End If
End Sub

After users start working with your application, they want to be able to customize its behavior. One user might prefer the main form to appear maximized on a 640-by-480 laptop screen. Another might want to display it in the lower-right corner of a 1,024-by-768 desktop machine. gSaveMainFormPosition puts the left and top coordinates and height and width of the main form into the Registry. This subroutine makes these settings available to gSetMainFormPosition when the main form loads.

The Dialog Box Position

Dialog boxes include any information screens that require a response from the user before the program can continue. Figure 32.5 shows an example of a type of dialog box.

Fig. 32.5

A dialog box can be positioned in the center of the screen.

You can use the gSetDialogPosition subroutine to position all dialog boxes on the screen. The code for gSetDialogPosition includes all of that shown in listing 32.12, which shows the code for gCenterDialogPosition. gCenterDialogPosition centers the indicated dialog box over the MDI form. For this demonstration, assume that this form’s name is frmMain. However, frmMain could also be an argument of this routine. If the main form is smaller than the dialog box, gCenterDialogPosition centers the dialog box on the screen.

Listing 32.12 The gCenterDialogPosition Subroutine; the Code for gSetDialogPosition Is Similar

Sub gCenterDialogPosition(WndName As Form)
If (WndName.Height > frmMain.Height) Or _
(WndName.Width > frmMain.Width) Then
WndName.TOP = (Screen.Height - WndName.Height) \ 2
WndName.Left = (Screen.Width - WndName.Width) \ 2
Else
WndName.TOP = frmMain.TOP + _
((frmMain.Height - WndName.Height) \ 2)
WndName.Left = frmMain.Left + _
((frmMain.Width - WndName.Width) \ 2)
End If
End Sub

MDI Children

There is no right or wrong method for displaying MDI children within an MDI parent. Every application is different, so you must make any positioning choices based on the user’s needs and the type of application that you are developing. Figure 32.6 shows where SetChildWindowPosition subroutine places the MDI child. This location is an excellent choice for a default. One good enhancement to this routine is to place the MDI child in the same position as the last time that the user used the form. Listing 32.13 shows the SetChildWindowPosition subroutine.

Fig. 32.6

An MDI child positioned on the screen.

Listing 32.13 The SetChildWindowPosition Subroutine

Sub gSetChildWindowPosition(frmName As Form, sFormTag$)
Dim sWindowState As String
Dim sTopPosition As String
Dim sLeftPosition As String
Dim sFormWidth As String
Dim sFormHeight As String
sWindowState = gGetValue(sFormTag, "WindowState")
Select Case sWindowState
Case "0"
sFormWidth = gGetValue(sFormTag, "Width")
sFormHeight = gGetValue(sFormTag, "Height")
If frmName.BorderStyle = vbSizable Then
sTopPosition = gGetValue(sFormTag, "Top")
sLeftPosition = gGetValue(sFormTag, "Left")
frmName.Move Val(sLeftPosition), Val(sTopPosition), _
Val(sFormWidth), Val(sFormHeight)
Else
frmName.Move Val(sLeftPosition), Val(sTopPosition)
End If
Case "1"
frmName.WindowState = vbMinimized
Case "2"
frmName.WindowState = vbMaximized
Case Else
frmName.Move 0, 0
End Select
End Sub

The gSetChildWindow routine is quite similar to the gSetMainFormPosition routine. The only difference lies in the fact that gSetChildWindow requires height and width properties only for sizeable MDI children. This routine checks whether those settings are necessary and, if so, obtains them. Every form differs, so you give this routine a tag name to differentiate it from other child windows. This tag name is normally the form’s tag property.

The gSaveChildWindowPosition routine (listing 32.14) performs the exact same function as gSaveMainFormPosition. This routine saves the height, width, left position, and top position of the indicated form. Notice that the giSaveSettings variable indicates whether to save these settings. This variable ensures that the settings do not change after the user sets them to his or her desired choices.

Listing 32.14 The gSaveChildWindowPosition Routine

Sub gSaveChildWindowPosition(frmName As Form, sFormTag$)
Dim sWindowState As String
Dim sTopPosition As String
Dim sLeftPosition As String
Dim sFormWidth As String
Dim sFormHeight As String
If giSaveSettings Then
sWindowState = Trim$(Str$(frmName.WindowState))
Call gSetValue(sFormTag, "WindowState", sWindowState)
sTopPosition = Trim$(Str$(frmName.TOP))
Call gSetValue(sFormTag, "Top", sTopPosition)
sLeftPosition = Trim$(Str$(frmName.Left))
Call gSetValue(sFormTag, "Left", sLeftPosition)
sFormWidth = Trim$(Str$(frmName.Width))
Call gSetValue(sFormTag, "Width", sFormWidth)
sFormHeight = Trim$(Str$(frmName.Height))
Call gSetValue(sFormTag, "Height", sFormHeight)
End If
End Sub

Creating a Positioning Project

You can fit all these routines into one project to demonstrate how these different routines work. To assemble this project, follow these steps:

  1. Create a new project.
  2. Save Form1 as frmChild in Visual Basic and Child at the DOS level.
  3. Change frmChild’s Caption property to 32Proj03.
  4. Change frmChild’s MDIChild property to True.
  5. Save the project as 32Proj03.
  6. Add an MDI form to the project and save it as frmMain in Visual Basic and MainMDI at the DOS level.
  7. Create a new form and save it as frmDialog in Visual Basic and Dialog at the DOS level.
  8. Add a command button to frmDialog and change its caption to “OK.”
  9. Change the Name property of command button to cmd_OK.
  10. Add the WINAPI32.BAS module from 32Proj01 to this project.
  11. Create a new module and rename it POSITION.BAS.
  12. Place the following routines in POSITION.BAS: gSetValue, gGetValue, gSetMainFormPosition, gSaveMainFormPosition, gSetChildWindowPosition, and gSaveChildWindowPosition.
  13. Put the following declaration into the General Declarations section of POSITION.BAS:

    Global giSaveSettings As Integer

  14. Place the following code into the appropriate events for frmChild:

    Private Sub Form_Load()
    Me.Tag = "frmChild"
    Call gSetChildWindowPosition(Me, Me.Tag)
    End Sub
    Private Sub Form_Unload(Cancel As Integer)
    Call gSaveChildWindowPosition(Me, Me.Tag)
    End Sub

  15. Include the following code in frmMain’s events:

    Private Sub MDIForm_Load()
    Call gGetSettings
    Call gSetMainFormPosition(Me)
    End Sub
    Private Sub MDIForm_Unload(Cancel As Integer)
    Call gSaveMainFormPosition(Me)
    End Sub

  16. Include the following code in frmDialog’s events:

    Private Sub cmdOk_Click()
    Unload Me
    End Sub
    Private Sub Form_Load()
    Call gCenterDialogPosition(Me)
    End Sub

  17. Create the menu File (mnuFile) for frmMain with the Open (mnuOpen), Dialog (mnuFileOpenDialog), Child (mnuFileOpenChild), and Exit (mnuFileExit) submenu choices. Indent Dialog and Child under Open.
  18. Create the menu options for frmMain with the Save Settings On Exit submenu option (mnuOptionsSaveSettings).
  19. Add the mnuOptionsSaveSettings_Click code to frmMain.
  20. Add the following code to frmMain also:

    Private Sub MDIForm_Load()
    Call gGetSettings
    Call gSetMainFormPosition(Me)
    End Sub
    Private Sub MDIForm_Unload(Cancel As Integer)
    Call gSaveMainFormPosition(Me)
    End Sub
    Private Sub mnuFileExit_Click()
    Unload Me
    End
    End Sub

In Chapter 30, “Advanced Control Techniques,” you added the code for controlling whether the toolbar and status bars appear on the main form. See Chapter 30’s discussion of how you do this.

Using Copy, Cut, and Paste

In their simplest form, Copy and Paste operations and Cut and Paste operations take highlighted text from one screen, place the text in the Clipboard, and then paste it into a new, compatible location.

The difference between a Copy and Paste operation and a Cut and Paste operation is that the former removes text from the original control. Visual Basic controls support Cut, Copy, and Paste operations without the code, but users like to see the familiar menu and icon choices. To provide these choices, you must add them to frmMain as follows:

  1. Create new menu choice Edit and name it mnuEdit with the caption &Edit.
  2. Make three submenus: mnuEditCut, mnuEditCopy, and mnuEditPaste.
  3. Assign the shortcut key combinations Ctl+C to mnuEditCopy, Ctl+X to mnuEditCopy, and Ctl+V to mnuEditPaste.
  4. Add two text boxes to frmChild and make their text properties blank.
  5. Save files under the project name 32PROJ04.

The following code shows the mnuEditCopy_Click event:

Private Sub mnuEditCopy_Click ()
Call gInitiateCopy(Screen.ActiveControl)
End Sub

The mnuEditCopy_Click event calls the InitiateCopy subroutine and identifies the control by using Screen.ActiveControl. The Screen object with the related property ActiveControl provides a great way to create a generic routine that works for any form in this project.

The InitiateCopy subroutine checks which type of control is currently active on the screen:

Public Sub gInitiateCopy (Cntl As Control)
If TypeOf Cntl Is TextBox Then
Clipboard.SetText Cntl.SelText, vbCFTEXT
End If
End Sub

Notice that some of the controls are commented out. This is because these controls ship with Sheridan Software’s DataWidgets and do not work unless you add those VBXs to the project. By examining these references, you can see that you can add any type of control that holds text by referencing the control type with a new ElseIf statement. Notice how the routine uses the SelText property rather than the Text property to obtain only the highlighted text. Also important is the use of the Clipboard object and the related SetText to place the text on the Clipboard.

The mnuEditCut_Click event references the InitiateCut subroutine in the same way that mnuEditCopy references InitiateCopy:

Sub mnuEditCut_Click ()
Call gInitiateCut(Screen.ActiveControl)
End Sub

InitiateCut identifies the control and its type in exactly the same way that InitiateCopy does:

Public Sub gInitiateCut (Cntl As Control)
If TypeOf Cntl Is TextBox Then
Clipboard.SetText Cntl.SelText, vbCFTEXT
Cntl.Text = CutText((Cntl.Text), (Cntl.SelText), _
(Cntl.SelStart), (Cntl.SelLength))
End If
End Sub

Unlike InitiateCopy, InitiateCut uses the CutText function to remove the highlighted text from that control:

Function CutText (Text$, SelText$, SelStart&, SelLength&)As String
If SelStart& > 0 Then
CutText$ = Mid$(Text$, 1, SelStart&) & Mid$(Text$, _
(SelStart& + SelLength& + 1))
Else
CutText$ = Mid$(Text$, (SelStart& + SelLength& + 1))
End If
End Function

The mnuEditPaste_Click event calls the InitiatePaste subroutine in a fashion similiar to that used by mnuEditCut_Click and mnuEditCopy_Click:

Sub mnuEditPaste_Click ()
Call gInitiatePaste(Screen.ActiveControl)
End Sub

The InitiatePaste subroutine uses the control and control type to identify where to paste the Clipboard text:

Sub gInitiatePaste (Cntl As Control)
If TypeOf Cntl Is TextBox Then
Cntl.SelText = Clipboard.GetText(vbCFTEXT)
End If
End Sub

Notice the use of the SelText property to identify the text to replace with the new text. If text is highlighted, this routine replaces it. If no text is highlighted, this routine simply pastes the text in the current location on the control. The routine identifies the actual text to paste with the Clipboard object and the related GetText property.

Look closely at the code lines that reference the Clipboard object. Each call includes the word vbCFTEXT. This ensures that the format is text. If your program needs to copy and paste or cut and paste something other than text, you need only choose the appropriate argument to identify it.

Chapter 30, “Advanced Control Techniques,” includes the code for generating the toolbar and status bar. This discussion includes all the information that you need to know to make toolbars that look like those of Microsoft Office applications.

Using Icon Sources in Windows Files

A good icon can be the difference between an application making sense or confusing the user. Icons are pictures that represent concepts. If the concepts are unclear to the user, they are less than useless. The user must readily recognize an icon’s purpose. In a personal information manager, for example, an icon of a sailboat might have a meaning related to recreation. In another program, such as an inventory system for a boat dealership, the same icon might have another perfectly valid use.

In the shell, you use two types of icons. First are those that ship with Visual Basic. The Programmer’s Guide that ships with Visual Basic 4.0 includes a list of these icons. They use the .ICO format and appear when the user minimizes a form. Some programmers place such icons on the SSCommand button along with text. However, these icons tend to consume too much space on the screen. The second type of icons are bitmap icons such as those on the frmgraphics form. These icons are smaller than the first type, and look similar to the toolbars that you see in Microsoft products.

Where can you find icons to use in your applications? The primary source of .ICO format icons is the library of icons that ships with Visual Basic 4.0. Another source of these types of icons is on CompuServe, in the WINFUN forum in Library 7. This source offers hundreds of icons that might meet your needs.

The primary source of bitmap icons lies hidden in the Windows DLL files. These icons are those that other applications use. These applications use the DLL files to display various kinds of information. The best method for finding and accessing these bitmaps is to use the AppStudio utility that ships with Microsoft Visual C++ 1.5.

Visual C++’s AppStudio

Microsoft Visual C++ 1.5 ships with a key to the DLL files already installed on your machine. Inside the DLL files that you find listed in AppStudio are a wealth of bitmaps and icons that you can use as the basis for your own icons in your own applications. For copyright reasons, you should not use someone else’s icons without permission, but these icons can give you ideas about how to make your own.

Microsoft Paint

If you don’t own Visual C++ 1.5, the Paint application that ships with Microsoft Windows can serve as an excellent method for editing and creating useful bitmap icons. Try opening CUSTOM1.BMP, which you can find on this book’s companion CD. This file contains a series of commonly used graphics that you can use as the basis for your own icons. Figure 32.7 shows a picture of Microsoft Paint with the CUSTOM1.BMP open.

Fig. 32.7

Microsoft Paint editing CUSTOM1.BMP.

Designing Install Solutions

Whenever you design an application in Visual Basic, you eventually must install the application on a user’s machine. Many of the features that you add to your applications require the addition of certain files besides the compiled executable. These files include .OCX files, OLE support files, and the run-time support files that enable your application to run properly on another machine.

Setup Wizard

Visual Basic ships with a newer version of Setup Wizard that offers several fine enhancements that make the wizard easier to use. Figure 32.8 shows Setup Wizard’s enhanced main screen. The wizard now offers an integrated compression utility that does not have to exit to a DOS window to work properly. Setup Wizard also indicates how much space your application will use when installed on a new system.

Fig. 32.8

Microsoft Setup Wizard's main screen.

Setup Wizard does not enable you to minimize the compression part of the setup process so that you can do something else while the wizard is compressing.

Missing Files

The new Visual Basic Setup Wizard keeps track of all the files that are part of Visual Basic 4.0. Unfortunately, ensuring that your application will include any files that you access through API calls or other means (such as database files or any associated graphics files) is more difficult. As you look through the files, carefully ensure that such files appear as part of your setup routine.

From Here...

At the heart of all your applications is the code that makes them work and do what you want. All the advances in technology have not relieved programmers of the need to write code. The more techniques that you have at your disposal, the easier the process becomes for increasing your arsenal of code. You can store information from application session to session in configuration files found in the Registry for Window 95 and NT and .INI files in Windows 3.x. You optimize the startup process by using splash screens and storing necessary information up front in global variables and the application object. Finally, you can provide users with more control over the behavior of an application by enabling them to set the positions of forms and then save those positions for later sessions. All these techniques make your applications more useful.

To learn more about related topics, see the following chapters:


© 1996, QUE Corporation, an imprint of Macmillan Publishing USA, a Simon and Schuster Company.