|
|
...and why should you care what he has to say about Visual Basic programming?
Send me e-mail
You can read the full text of past Basically Visual columns. These were originally published in Visual Developer magazine. Unfortunately this excellent magazine stopped as of May 2000, so Column 24 was my last.
Column 24. (March/April 2000) Drag and Drop.
Column 23. (January/February 2000) ActiveX Documents part 2.
Column 22. (November/December 1999) ActiveX Documents part
1.
Column 21. (September/October 1999) Visual Basic for
Applications.
Column 20. (July/August 1999) Data Sources, Data Sinks
(part 2).
Column 19. (May/June 1999) Data Sources, Data Sinks
(part 1).
Column 18. (March/April 1999) VBScript and Active Server Pages
for Easy Web Database Programming.
Column 17. (January/February 1999) Visual Basic and the Common
Gateway Interface.
Column 16. (November/December 1998) Object oriented file access.
Column 15. (September/October 1998) Visual Basic 6.
Column 14. (June/July/August 1998) The Windows Common Controls.
Column 13 (April/May 1998): Internet programming.
Column 12 (February/March 1998): Creating and using custom
events.
Column 11 (December/January 1998): Screen capture the Visual
Basic way.
Column 10 (October/November 1997): Creating bitmaps from
arbitrary pixel data.
Column 9 (August/September 1997): Pointers and callbacks.
Column 8 (June/July 1997): ActiveX programming, part 2.
Column 7 (April/May 1997): ActiveX Programming, part 1.
Column 6 (February/March 1997): Visual Basic 5.
Column 5 (December/January 1997): Using the TreeView control.
Column 4 (October/November 1996): Taskbar tray icons.
Column 3 (August/September 1996): Using resource files with
Visual Basic
Column 2 (June/July 1996): Variant speed penalties, uses for
collections.
Column 1 (April/May 1996): Objects in Visual Basic.
Here I present solutions to a few problems that I have encountered in my programming. Perhaps you will find something that is useful to you. These tips are all relevant to pre-.NET versions of Visual Basic.
The WebBrowser control is a powerful tool useful in many Web-related projects. In effect, this control provides the browser functionality of Internet Explorer in a software component that you can incorporate in your projects. In some applications it is desirable to customize the WebBrowser control's behavior. While some customization is possible through the control's properties and events, this is sometimes not sufficient. Microsoft has made a tool available that permits certain WebBrowser customizations, specifically having to do with accelerator keys and context (right-click) menus. You can disable context menus, and can disable all or selected accelerator keys. The tool is called WBCustomizer and is available by downloading WBCustom.exe from http://support.microsoft.com/support/kb/articles/Q183/2/35.ASP and executing it to extract WBCustomizer.dll and a sample project.
To use the WBCustomizer object, you must register the DLL as follows using the Run command:
regsvr32 wbcustomizer.dll
Use the full path to the DLL file as required. In your Visual Basic project the component must be selected using the References dialog box in order to permit early binding. Then, using the object is relatively simple. First, declare a variable to reference the object and create an instance of it:
Dim CustomWB As WBCustomizer
Set CustomWB = New WBCustomizer
Then, set the object properties to specify the desired customization, and associate it with the WebBrowser control. This code assumes that the WebBrowser control is named WebBrowser1. It turns off all context menus and accelerator keys:
With CustomWB
.EnableContextMenus = False
.EnableAllAccelerators = False
Set .WebBrowser = WebBrowser1
End With
The sample Visual Basic project that comes with the DLL provides examples of disabling specific accelerator keys.
In theory the Internet Transfer Control is the best thing since sliced bread. In reality I consider it a real pain in the neck. Even though the latest version is vastly improved over the earlier releases, it is still buggy. Sometimes the OpenURL method does not retrieve an entire page. Also, I find that trying to use this control in a program that also uses automation to work with Office components often leads to weird and impossible to solve bugs. If you need to retrieve HTML pages, you can do so easily by calling some functions in the WinInet library. Without going into the details, here's how. First, put the following declarations in your project (in a code module):
Public Const INTERNET_OPEN_TYPE_PRECONFIG = 0
Public Const INTERNET_OPEN_TYPE_DIRECT = 1
Public Const INTERNET_OPEN_TYPE_PROXY = 3
Public Const scUserAgent = "VB OpenUrl"
Public Const INTERNET_FLAG_RELOAD = &H80000000
Public Declare Function InternetOpen Lib "wininet.dll" Alias "InternetOpenA" _
(ByVal sAgent As String, ByVal lAccessType As Long, ByVal sProxyName As String, _
ByVal sProxyBypass As String, ByVal lFlags As Long) As Long
Public Declare Function InternetOpenUrl Lib "wininet.dll" Alias "InternetOpenUrlA" _
(ByVal hOpen As Long, ByVal sUrl As String, ByVal sHeaders As String, _
ByVal lLength As Long, ByVal lFlags As Long, ByVal lContext As Long) As Long
Public Declare Function InternetReadFile Lib "wininet.dll" _
(ByVal hFile As Long, ByVal sBuffer As String, ByVal lNumBytesToRead As Long, _
lNumberOfBytesRead As Long) As Integer
Public Declare Function InternetCloseHandle Lib "wininet.dll" _
(ByVal hInet As Long) As Integer
Then, here's a function that returns the entire text of the specified URL:
Private Function GetHTMLFromURL(sUrl As String) As String
Dim s As String
Dim hOpen As Long
Dim hOpenUrl As Long
Dim bDoLoop As Boolean
Dim bRet As Boolean
Dim sReadBuffer As String * 2048
Dim lNumberOfBytesRead As Long
hOpen = InternetOpen(scUserAgent, INTERNET_OPEN_TYPE_PRECONFIG, vbNullString, vbNullString, 0)
hOpenUrl = InternetOpenUrl(hOpen, sUrl, vbNullString, 0, INTERNET_FLAG_RELOAD, 0)
bDoLoop = True
While bDoLoop
sReadBuffer = vbNullString
bRet = InternetReadFile(hOpenUrl, sReadBuffer, Len(sReadBuffer), lNumberOfBytesRead)
s = s & Left$(sReadBuffer, lNumberOfBytesRead)
If Not CBool(lNumberOfBytesRead) Then bDoLoop = False
Wend
If hOpenUrl <> 0 Then InternetCloseHandle (hOpenUrl)
If hOpen <> 0 Then InternetCloseHandle (hOpen)
GetHTMLFromUrl = s
End Function
While Visual Basic makes some things very easy, other things are ridiculously difficult. For example, sending raw data to the printer port seems to be impossible. When I say "raw," I mean really raw - I am not talking about unformatted text. I wanted to make the printer port's 8 data lines output a specific bit pattern, such as 00000101, under program control. My task was to use the computer to control a piece of laboratory equipment that was equipped with a parallel interface, but this technique might also be applicable for programming the many other types of parallel port devices, such as external mass storage, that are available today. I tried a variety of Windows API calls without success and finally realized that this was one of those few times when going outside of Visual Basic is unavoidable. The solution lay in Visual C++, which has the _outp() function to send a byte directly to a hardware port (like Quick Basic's old out() statement). I compiled the code into a DLL called PPORT.DLL, which you can download in ZIP format by clicking here. Put the DLL in the \Windows\System folder, then declare the function in your Visual Basic program as follows:
Declare Function SendByteToPort Lib "pport.dll" (p As Integer, b As Integer) As Integer
The argument p is the port number, which for LPT1: is &H378. The argument b is the value, in the range 0-255, to be sent. The function's return value can be ignored. Be careful when using this function, because by writing directly to the hardware it bypasses the Windows safety nets. You can get into all sorts of trouble if you are not careful! I have had reports that this does not work under Windows 2000.
Since I wrote the above programmer Kris Tilly has informed me that you can send raw data to the printer port without resorting to a DLL written in C++, but rather by calling Windows API functions from your Visual Basic program. I have not tested this, but Kris says it works fine.
Public Type DOCINFO
pDocName As String
pOutputFile As String
pDatatype As String
End Type
Public Declare Function ClosePrinter Lib "winspool.drv" (ByVal _
hPrinter As Long) As Long
Public Declare Function EndDocPrinter Lib "winspool.drv" (ByVal _
hPrinter As Long) As Long
Public Declare Function EndPagePrinter Lib "winspool.drv" (ByVal _
hPrinter As Long) As Long
Public Declare Function OpenPrinter Lib "winspool.drv" Alias _
"OpenPrinterA" (ByVal pPrinterName As String, phPrinter As Long, _
ByVal pDefault As Long) As Long
Public Declare Function StartDocPrinter Lib "winspool.drv" Alias _
"StartDocPrinterA" (ByVal hPrinter As Long, ByVal Level As Long, _
pDocInfo As DOCINFO) As Long
Public Declare Function StartPagePrinter Lib "winspool.drv" (ByVal _
hPrinter As Long) As Long
Public Declare Function WritePrinter Lib "winspool.drv" (ByVal _
hPrinter As Long, pBuf As Any, ByVal cdBuf As Long, _
pcWritten As Long) As Long
Dim lhPrinter As Long
Dim lReturn As Long
Dim lpcWritten As Long
Dim lDoc As Long
Dim sWritteRdata As String
Dim MyDocInfo As DOCINFO
lReturn = OpenPrinter(Printer.DeviceName, lhPrinter, 0)
If lReturn = 0 Then
MsgBox "The Printer Name you typed wasn't recognized."
Exit Sub
End If
MyDocInfo.pDocName = "AAAAAA"
MyDocInfo.pOutputFile = vbNullString
MyDocInfo.pDatatype = vbNullString
lDoc = StartDocPrinter(lhPrinter, 1, MyDocInfo)
Call StartPagePrinter(lhPrinter)
For I = 1 To NumCopies
sWritteRdata = ""
lReturn = WritePrinter(lhPrinter, ByVal sWritteRdata, _
Len(sWritteRdata), lpcWritten)
lReturn = EndPagePrinter(lhPrinter)
Next I
lReturn = EndDocPrinter(lhPrinter)
lReturn = ClosePrinter(lhPrinter)
The Visual Basic Application Setup Wizard makes it easy to create a set of distribution files for your program. Given all of the support files that a Visual Basic program requires, however, distribution files tend to be rather large. This is a special concern when creating distribution files for download. It is important to ensure that your distribution files are as compact as possible. Yet, the Setup Wizard will sometimes include files in the distribution package that you are sure - or almost sure - that your program doesn't need. Why is this?
The problem is that the Setup Wizard is not smart enough to determine which software components your program actually needs, but can only detect those components that were available within the Visual Basic development environment at the time the program was compiled. For example, if the "SooperDooperGrid Control" was checked in the Components dialog box, its files will be included in the distribution files even though your program does not use it. Therefore, you need to remove all of the components that your program does not use from the Components dialog box before compiling your program. Fortunately, Visual Basic is pretty good at preventing you from removing components that are in use. I shrank a 2.5MB download to 1.6MB using this technique, so the gains can be significant.
The function presented here is passed a string and returns the string with its character order reversed. "ABC" becomes "CBA" etc. Note that Visual Basic 6 has its own function StrReverse that does the same thing, so this is for users of earlier versions of Visual Basic.
Public Function Reverse(s As String) As String
' Returns a string with the characters
' in s in reverse order.
Dim buf As String, i As Long
If Len(s) < 2 Then
Reverse = s
Exit Function
End If
buf = ""
For i = Len(s) To 1 Step -1
buf = buf & Mid(s, i, 1)
Next i
Reverse = buf
End Function
Visual Basic has a statement SendKeys that can be used to
"generate" certain keystrokes just as if they had been pressed by the user.
However this function is limited in that it cannot be used to simulate all possible
keystroke combinations. I discovered this recently when trying to capture screens from
within a Visual Basic program - you cannot send Alt+PrintScr using SendKeys.
I was informed by reader Matt Hart that you can accomplish essentially the same thing with
an API function, Keybd_Event which does not have the limitations of SendKeys.
This function "synthesizes" a keystroke, and it does a pretty good job because
this is the same function that is called by the keyboards hardware interrupt
handler. As far as I can tell, the operating system has no way of knowing whether a
keystroke came from someone's grubby little finger actually hitting the keyboard or was
generated in software using Keybd_Event. Heres the function declaration:
Private Declare Sub keybd_event Lib "user32" (ByVal bVk As Byte, ByVal _
bScan As Byte, ByVal dwFlags As Long, ByVal dwExtraInfo As Long)
Without going into all the details, heres how you can capture screens to the clipboard. First declare the following constant:
Const VK_SNAPSHOT As Byte = &H2C
Then capture the active Visual Basic window to the clipboard with:
Call keybd_event(VK_SNAPSHOT, 0, 0, 0)
Or capture the entire screen (hiding the Visual Basic window first, if desired) with:
Call keybd_event(VK_SNAPSHOT, 1, 0, 0)
Youll find a sample program that uses this code to capture AVI video images on
Matts web page at http://www.blackbeltvb.com/
You can be pretty sure that www.microsoft.com will always be a valid link, but many other web links tend to come and go as their authors move on, get bored, or whatever. If you maintain a web page with lots of external links, it is in your interest to ensure that the links are functioning. If lots of the links on your page are dead, your visitors will not be impressed! Many web page design tools, such as FrontPage, have a "validate links" command but I have found these to be fairly useless. They tell you which links are OK and which are not, but they do not permit you to do anything about the bad links (other than manually removing them from the pages). I needed a better solution.
I keep my links database in an Excel worksheet - one column for URL, one for Title, etc. I have written an Excel Basic macro that reads through the table and writes the HTML code for each of my links pages. I decided to write a Visual Basic program that would:
Then, I would re-write the Excel Basic macro so that a link will be written to the HTML file only if the URL is marked as "OK." This approach has worked very well for me. Here's the code. Create a form with a Text Box, an Internet Transfer control, four Label controls, and a control array of two Command Buttons. In the General section put the following code:
Option Explicit
Const FILENAME = "c:\documents\stamps\links.xls"
Dim XLObj As Excel.Application
Change the FILENAME constant to point to your worksheet file. Here is the code for the Form_Load and Command1_Click event procedures:
Private Sub Command1_Click(Index As Integer)
Select Case Index
Case 0 'Start
Command1(0).Enabled = False
Call CheckLinks
Command1(0).Enabled = True
Case 1 ' Quit
End
End Select
End Sub
Private Sub Form_Load()
' Create the Excel object and open the worksheet.
Set XLObj = CreateObject("Excel.Application")
XLObj.Workbooks.Open FILENAME
Inet1.Protocol = icHTTP
End Sub
The bulk of the action goes on in the CheckLinks() procedure.
Public Sub CheckLinks()
Dim row As Integer, url As String
Dim buf As String, msg As String, fnf As Integer
Dim snf As Integer, tout As Integer, ok As Integer
On Error Resume Next
' Make row equal to the Worksheet row where
' your data starts.
row = 4
tout = 0
fnf = 0
ok = 0
snf = 0
' Minimize the form.
Form1.WindowState = 1
Do
' I keep URLs in column 3 (C) of the worksheet.
url = XLObj.Cells(row, 3)
' If it's empty we are done.
If url = "" Then Exit Do
' Try to open the URL.
Text1.Text = Inet1.OpenURL(url)
DoEvents
' If the URL returned any text, put the
' first 50 characters in a buffer. Error
' messages will be found here.
If Len(Text1.Text) > 50 Then
buf = Left(Text1.Text, 50)
Else
buf = Text1.Text
End If
' Catch a time out error.
If Err = 35761 Then
msg = "Timed out"
tout = tout + 1
Err.Clear
' If nothing is returned it usually means
' that the server was not found.
ElseIf Text1.Text = "" Then
msg = "Server not found"
snf = snf + 1
' If error 404 is returned from the URL
' it means the server was found but
' the requested file was not present.
ElseIf InStr(1, buf, "404") Then
msg = "File not found"
fnf = fnf + 1
' Otherwise the link is OK.
Else
msg = "OK"
ok = ok + 1
End If
' Put the result in column 5 of the worksheet.
XLObj.Cells(row, 5) = msg
' Move to the next row.
row = row + 1
' Display current status on form.
Form1.Caption = ok + fnf + snf + tout
Label1.Caption = "OK: " & ok
Label2.Caption = "File not found: " & fnf
Label3.Caption = "Server not found: " & snf
Label4.Caption = "Timed out: " & tout
Loop While True
' When all links checked, restore the form.
Form1.WindowState = 0
' Close the worksheet.
XLObj.Workbooks.Close
' Delete the object.
Set XLObj = Nothing
' Display a summary of results.
buf = "OK: " & ok & vbCrLf
buf = buf & "Server not found: " & snf & vbCrLf
buf = buf + "File not found: " & fnf & vbCrLf
buf = buf & "Timed out: " & tout
MsgBox (buf)
End Sub
The program takes a while to run - a couple of hours to check a few hundred links - but because it is running in the background you can continue to use your system for other tasks. You can download a ZIP file containing the Visual Basic project by clicking here.
Note: Since originally creating this project I have made some modifications that increase its reliability and flexibility. First, the links information is kept in an Access database rather than in an Excel workbook. The Visual Basic program uses ADO to read and write the data table. Second, I have abandoned the Internet Transfer control in favor of using the WinINet library to implement the http protocol. Details of using WinINet are presented here.
For a Visual Basic program to connect to a data source, a connection string is often required. This string specifies the data source, user name, security information, and other aspects of the data connection. Here's an example of a fairly simple connection string:
Provider=Microsoft.Jet.OLEDB.3.51;Persist Security Info=False;User ID=AliceK;Data Source=C:\data\Northwind.mdb
Who can remember all the details required to write connections strings? I know I can't! Visual Basic can help you, as follows:
This method for creating connection strings is a lot faster and results in fewer errors than doing it manually.
The Visual Basic form designer makes it easy to align a group of controls with one member of the group. Simply select the controls by holding down shift while clicking each control, being sure to click the target control (the one the others will be aligned to) last. Then, select Format|Align, then select Lefts for vertical alignment or Tops for horizontal alignment.
But what if you want to align controls at a specific position rather than to an existing control? For example, you might want to align several TextBox controls so their left edges are all exactly 50 twips from the left edge of the form. Here's how:
This technique can be extended to other design tasks. When more than one control is selected, the Properties window displays only those properties that are common to all the controls. Changing a property value is automatically applied to all the selected controls.
Most Visual Basic programmers never make use of the Tag property, available with most controls. After all, this property doesn't do anything so why bother with it? The truth is, it can be very useful in a variety of situations. You can store any information you want in a control's Tag property, and use that information any way you want.
One example is when you create a control array of CommandButton controls. Programmers usually use the Index argument that is passed to the Click event procedure to identify which button in the array was clicked. By assigning each button a descriptive Tag property you can use that instead and have more readable code. Here's the code for a simple example:
Private Sub Command1_Click(Index As Integer)
Select Case
Command1(Index).Tag
Case "Exit"
' Code to exit the program goes here.
Case "Cancel"
' Code to cancel goes here.
End Select
End Sub
Another way to use the Tag property is when working with a TreeView control. A TreeView is made up of nodes, and it can be useful to have some data associated with each node in addition to the text it displays. This is easily accomplished by using the Tag property of each Node object.
Lots of programmers don't even know about Visual Basic's DoEvents function. This is not surprising because few Visual Basic programs need it. DoEvents returns control to the operating system temporarily, allowing it to process other events that may have occurred. In my experience, the only time DoEvents is needed is when a program has code that takes a long time to execute, such as certain complex mathematical calculations. By calling DoEvents at strategic locations in your code you can improve program responsiveness.
To see what I mean, create a Standard EXE project in Visual Basic and place one CommandButton and one TextBox on the form. Then, put the following code in the Command Button's Click event procedure:
Private Sub Command1_Click()
Dim i As Long, j As Long
For i = 1
To 100
Text1.Text = i
For j = 1 To 100000
Next
Next
Text1.Text = "Done"
End Sub
You can see that the code has one loop within another, loops that will take a few seconds to complete (You may want to adjust the value that the inner loop counts to depending on the speed of your system). Each time the outer loop iterates, the current value of i is displayed in the text box. When the loops are finished, "done" is displayed.
What actually happens when you run the program, however, is that the text box does not change until "done" is displayed. The problem is that the system was so busy executing the loops that the requests to display i in the text box got stalled in Windows queue. When the loops were finished, all these requests were processed, too quickly for you to see on the screen.
Now, place a call to DoEvents in the code, just after the Text1.Text = i statement. When you run the program you will see that the text box "counts up" the values of i, just as you would expect. Calling DoEvents frees up the system to process the request, then returns control to the Visual Basic program.
DoEvents is not without potential problems. For example, if you call DoEvents from a procedure you must be sure that the same procedure cannot be called again before execution returns from the first call - otherwise unpredictable results may occur. Likewise, DoEvents should be avoided if other applications could interact with the procedure in unforeseen ways. Use of a Timer control or isolation of long-running code in an ActiveX component are two other approaches to improving program responsiveness.
Capturing images of the screen is useful in many situations, such as when documenting computer glitches or writing technical manuals and software documentation. Can you capture a screen image from a Visual Basic program? You bet. There's a complex way, involving Windows device contexts and the API, but it would be easier to use Window's built-in screen capture capability. Pressing the PrintScrn button copies the entire screen to the clipboard, while Alt+PrintScrn copies just the active window. All a Visual Basic program would need to do is to "press" this key.
Unfortunately, you cannot use Visual Basic's SendKeys for this purpose - it just won't work with PrintScrn. You must use the Keydb_Event API function. This function synthesizes a keystroke, and because it is the same function that is called by the keyboard’s hardware interrupt handler, it should do a good job of fooling an application - or Windows in this case - into thinking that the key was actually pressed. The function declaration is as follows:
Declare Sub keybd_event Lib "user32" (ByVal bVk As Byte, _
ByVal bScan As Byte, ByVal dwFlags As Long, _
ByVal dwExtraInfo As Long)
I can't go into all the details of this statement, but here’s how to capture screens to the clipboard. Start by declaring the following constant:
Const VK_SNAPSHOT As Byte = &H2C
Then you capture the active window to the clipboard with this code:
Call keybd_event(VK_SNAPSHOT, 0, 0, 0)
To capture the entire screen you use the following:
Call keybd_event(VK_SNAPSHOT, 1, 0, 0)
Once the screen image is on the clipboard, you will use the Clipboard object's GetData method to retrieve it as required by your application. The following code captures the screen and displays it in a PictureBox control, from where it can be saved to disk is desired.
Clipboard.Clear
Call keybd_event(VK_SNAPSHOT, 0, 0, 0)
DoEvents
Picture1.Picture = Clipboard.GetData(vbCFBitmap)
Please note two things about this code. First, you should call Clipboard.Clear before "pressing" PrintScrn to remove any old data from the clipboard. Second, you should call the Visual Basic DoEvents function before trying to retrieve the captured image, to allow Windows the time to process the keybd_event call.
A disk drive can be one of several types. Sometimes a program needs to be aware of the type of the drive in order to know what it can and cannot do. For example, a program cannot write to a CD-ROM drive. Likewise, if a drive a removable a program cannot assume anything about what files are on the drive. You can use an API function, GetDriveType, to easily determine the type of a drive. This function has the following declaration, which must be included in your project:
Declare Function
GetDriveType Lib "kernel32" Alias _
"GetDriveTypeA" (ByVal nDrive As String) As Long
The function's argument is s string identifying the drive's root folder - for example, "c:\" or "f:\". The return value is a type Long, one of the following depending on the drive type. This list includes the constants that are usually defined for use in the program.
DRIVE_REMOVABLE = 2
DRIVE_FIXED = 3
DRIVE_REMOTE = 4
DRIVE_CDROM = 5
DRIVE_RAMDISK = 6
If the function returns any other value it means the drive was not recognized (that is, does not exist or is not available, as in a network drive).
The following Visual Basic procedure illustrates the use of this API function. The argument to this procedure must be in the form required by the GetDriveType function, that is the drive letter followed by ":\". Of course the API function declaration and the constant definitions must be included in any program that uses this procedure.
Private Sub DriveType(dr as string)
Dim dr As
String
Dim drivetype as Long
drivetype = GetDriveType(dr)
Select Case drivetype
Case DRIVE_REMOVABLE:
MsgBox "Drive " & dr & " is a removable drive"
Case DRIVE_FIXED:
MsgBox "Drive " & dr & " is a fixed (hard) drive"
Case DRIVE_REMOTE:
MsgBox "Drive " & dr & " is a remote (network) drive"
Case DRIVE_CDROM:
MsgBox "Drive " & dr & " is a CD-ROM drive"
Case DRIVE_RAMDISK:
MsgBox "Drive " & dr & " is a RAM drive"
Case Else:
MsgBox "Drive " & dr & " was not recognized"
End Select
End Sub
The TextBox control is commonly used to accept keyboard input from the user. In some situations you may want to restrict what the user can type to ensure that invalid data is not entered. For example:
- A TextBox for ZIP code could be restricted to accepting
digits (and, if ZIP+4 is being used, the hyphen).
- A TextBox for entry of a person's first name could be restricted to letters.
- A TextBox for an email address could be restricted to letters, digits, and the
period and @ characters.
To restrict entry into a TextBox you use its KeyPress event procedure:
Private Sub Text1_KeyPress(KeyAscii As Integer)
End Sub
The KeyAscii argument is a code identifying the key that was pressed as an ASCII value. Code in the KeyPress event procedure can examine the value of KeyAscii to see if a permitted character was entered. If so, do nothing and the character will be passed through to the TextBox. If the key is not permitted, set KeyAscii to 0 and the keypress is cancelled. KeyPress should always pass through the BackSpace key (ASCII value = 8) to permit the user to delete erroneous characters.
The following KeyPress event procedure lets only the digits 0-9 through, plus Backspace. The ASCII values for the characters 1 though 9 are 48-57.
Private Sub Text1_KeyPress(KeyAscii As Integer)
If KeyAscii = 8 Then Exit Sub
If KeyAscii < 48 Or KeyAscii > 57 Then
Beep
KeyAscii = 0
End If
End Sub
Note that the KeyPress event procedure will not intercept editing keys such as Del or the arrow keys, so you do not need to make provision to pass them through in your code.
Visual Basic makes it easy to find one string within another using the InStr function. But what about counting the number of times one string occurs within another? One use for this is when performing Web searches. The more times a term of interest appears on a Web page, the more likely the page is to be relevant.
It's not too difficult to program string counting yourself. You can use the InStr function, calling it repeatedly until it returns 0. With each call after the first one, update the Start position to the location of the last "hit" plus one so that InStr will find the next occurrence of the target. Keep track of the hit count, and when InStr returns 0 you are done.
The code shown here shows how. The function is named CountSubstrings. You pass it the target string to be searched, the template you are looking for, and a binary value indicating whether you want text searches to be case-sensitive. Note that the code takes care of three "error" conditions that might occur:
- The target string is blank.
- The template string is blank.
- The template is longer than the target.
In any of these situations the function returns -1.
Public
Function CountSubstrings(target As String, _
template As String, CaseSensitive As Boolean) _
As Integer
' Returns
the number of times template occurs in target.
' Returns -1 if either string is blank or if template
' is longer than target. If CaseSensitive is true, performs
' a case-sensitive comparison for text. If false, the
' comparison is case-insensitive.
Dim pos1 As Integer, pos2 As Integer, count As Integer
If
Len(target) = 0 Or Len(template) = 0 Or _
Len(template) > Len(target) Then
CountSubstrings = -1
Exit Function
End If
count = 0
pos2 = 1
Do
If CaseSensitive Then
pos1 = InStr(pos2, target, template, vbBinaryCompare)
Else
pos1 = InStr(pos2, target, template, vbTextCompare)
End If
If pos1
> 0 Then
count = count + 1
pos2 = pos1 + 1
End If
Loop Until pos1 = 0
CountSubstrings = count
End Function
The only requirement for a temporary file name is that the file name not already be in use in the folder where the file will be created. The function presented here takes a path as its one argument - the path where you nee the temporary file - and returns a filename (with path) that is not present in the specified folder and can therefore be safely used for a temporary file. If a blank string is passed as the path argument, the application path (App.Path) is used. The function returns a blank string on error. The function does not verify that the path that is passed to it is valid.
The operation of the code is simple. It starts with the value 1 and makes a file name from it (1.tmp). It then checks to see if this file already exists in the specified folder using the Dir function. If not, the name is suitable and is returned by the function. If the file exists, the value is increased and the new file name (2.tmp, 3.tmp, etc.) is tried until an unused name is found.
Public Function MakeTempFileName(path As String)
' Returns a filename (with path) that is not
' already in use in the indicated path. Name
' has the form path\1.tmp, path\2.tmp, etc.
' If path is blank then App.Path is used.
' Does not verify that path is valid.
Dim x As Integer, s As String
If path = "" Then path = App.path
' Be sure path ends with \.
If (Right(path, 1) <> "\") Then path = path & "\"
x = 0
Do
x = x + 1
s = path & x & ".tmp"
Loop Until Dir(s) = ""
MakeTempFileName = path & x & ".tmp"
End Function
A Visual Basic program needs to be able to run on different systems. In particular, the video equipment is likely to differ from one system to another. Joe may have an older laptop with 800x600 resolution, while Alice is running a new 21 inch monitor at 1600x1200 pixels. For some programs these video differences may not be important, but in other cases you may want to make sure that your program makes the best possible use of the screen real estate and resolution. You can use the Screen object to obtain information about the display hardware.
Recall that Visual Basic uses twips for screen measurements, with 1 twip supposedly equal to 1/1440 inch. You can determine the screen's size in twips from two of the Screen object's properties, Screen.Width and Screen.Height. In theory you could use this information to determine the actual screen size as follows:
ScreenWidthInInches = Screen.Width/1440
ScreenHeightInInches = Screen.Height/1440
I have found, however, that the actual size of a twip varies a lot from system to system. On mine, for example, a twip is a lot closer to 1/1000 inch, and the above calculations will report my 21 inch monitor as being about. 9x12 inches! The twip values are accurate in terms of positioning items on the screen, though, and you can use them to position forms on the screen without going off the edge. This code, for example, centers a form on the screen:
Form1.Move (Screen.Width -
Form1.Width) / 2, _
(Screen.Height - Form1.Height) / 2
Here's another example that positions the form at the lower right corner of the screen on any system (although some of the form may be covered by the Windows task bar):
Form1.Move Screen.Width - Form1.Width, Screen.Height - Form1.Height
With today's multi-gigahertz computers, programmers are rarely concerned with the speed of computations. Code runs plenty fast, and the bottlenecks for program performance are much more likely to be related to network access and other factors that are beyond the programmer's control. At times, however, raw computation speed does become an issue. This is most likely to happen when you are performing complex recursive tasks such as certain mathematical calculations and text processing operations. In these situations, even minor improvements to your code speed are likely to have an impact on the program's usability. Shortening a computation from, say, 60 seconds to 40 seconds is quite noticeable to the user. Here are some coding tips to maximize the performance of long computations:
Do not use type Variant variables unless it is unavoidable.
For numeric variables, use the smallest type possible for the data. That is, for integer data use Byte in place of Integer, and use Integer in place of Long. For floating point data use Single in place of Double.
For string data, use fixed-length strings as opposed to variable length strings whenever possible.
Do not write to the screen during long computations.
Minimize procedure calls. If a section of code is executed many times, it is better to place it in-line than to place it in a separate procedure that must be called with the attendant processing overhead.
While writing your program you can use the Timer function to test the speed of calculations and help you in fine-tuning the code for maximum performance. The Timer function returns the number of seconds since midnight, accurate to 1/100 of a second. Set one variable equal to Timer just before calculations start, and set another variable equal to Timer when they are complete. A simple subtraction will tell you how long the computations took. Be sure to run your tests on a compiler version of the program and not in the IDE.
Blinking text is great way to call attention to something on the screen. It's easy to implement blinking text using Visual Basic's Timer control. This example shows you how to make a Label control blink, but the same approach could be used for other controls as well.
When you place a Timer control on a form, it does not display when the program runs - it's available "behind the scenes." The Timer control has two important properties. The Interval property determines how often the Timer "ticks." The value is in milliseconds, so a setting of 500 will give you a twice per second Timer which I think is a good rate for blinking text. The Enabled property determines whether the Timer is running (Enabled = True) or stopped (Enabled = False).
The actual blinking is done in the Timer event procedure, which fires each time the Timer "ticks." One approach is to toggle the Label control's ForeColor property between black, which makes it visible, and the value of its BackColor property, which makes the text invisible. You must check the current value of the ForeColor property and then set it accordingly, as shown here:
Private Sub Timer1_Timer()
If Label1.ForeColor = Label1.BackColor Then
Label1.ForeColor = vbBlack
Else
Label1.ForeColor = Label1.BackColor
End If
End Sub
You could also implement a different type of blinking, for example having the text alternate between red and green:
Private Sub Timer1_Timer()
If Label1.ForeColor = vbRed Then
Label1.ForeColor = vbGreen
Else
Label1.ForeColor = vbRed
End If
End Sub
To turn the blinking on or off, set the Timer control's Enabled property to True or False. When you turn the blinking off, you want to make sure that the text is not left in the invisible state, or with an inappropriate color. To do so, include code to set the ForeColor property to the desired color at the same time you turn the Timer control off:
Timer1.Enabled = False
Label1.ForeColor = vbBlack
Command line arguments date from the days of DOS when programs were run by entering the program name at the command line. Many programs could accept arguments placed after the program name, as shown here:
C>ProgramName argument1 argument2
These command line arguments could be read by the program and used according to the program's design. For example, some programs accepted an argument specifying the data file to open, and others accepted arguments that set certain program options. With the Windows operating system, where programs are started with a click of the mouse, the command line may seem to be a relic of the past, but it's not. Your Visual Basic programs can still use command line argument.
But Windows has no command line, so how can you pass command line arguments to a program? There are two ways. In the Visual Basic environment, when writing and testing your program, you display the Project Properties dialog box (Select Project|Properties) and, on the Make tab, you enter the arguments in the Command Line Arguments field. After the program is compiled you must use a shortcut to use command line arguments:
Create a shortcut for the compiled Visual Basic program.
Right-click the shortcut and select Properties.
In the Properties dialog box, select the Shortcut tab.
The Target field will contain the command used to start the program. This command will include the path to the compiled program plus the program name, for example "c:\program files\myprogram.exe".
Edit this text to include the command line argument(s) after the program name. Be sure to use a space between the name and the arguments, and put the arguments inside the quotes (if present). For example "c:\program files\myprogram.exe argument1 argument2".
Click OK to close the Properties dialog box.
Now, whenever this shortcut is used to start the program, the specified arguments will be passed to it. By creating more than one shortcut to the same program, you can start the program with different arguments depending on the situation.
In a Visual Basic program, the Command$ function returns a string containing any command line arguments that were passed when the program was started. The arguments (if more than one) are not parsed in any way - they are returned together in a single string. This string is blank if no arguments were passed. It is totally up to your program what it does, if anything, with the arguments it receives. Command line arguments are not for every programming situation, but they can provide some additional flexibility in some circumstances.
Environment Variables are maintained by the Windows operating system. Each variable consists of a name and a value. You can see the current environment variables by opening a Command Prompt window and typing Set. These variables are system-wide and can be used by any program. Many of them have to with the operating system, but you can create environment variables for use by your Visual Basic programs. Because such variables are available to all programs, they provide a convenient approach when you need to set options or operating parameters for multiple programs.
There are two ways to create or change an environment variable. In a batch file, such as AutoExec.bat, you use the Set command as follows:
Set varname=value
You can also open System from the Windows Control Panel. On the Advanced tab, click Environment Variables and use the dialog box tools to add, edit, or delete variables. Note that there are User variables, which are set for and can be changed by the currently logged on user, and System variables, which can be added/changed only by someone with administrator privileges (Windows NT/2000/XP).
To access environment variables in your Visual Basic program, you use the Environ function. There are two ways to use this function. If you pass the name of an environment variable the function returns the value of that variable, or a blank string if the variable is not defined. If you pass a number, the function returns the entire environment string (the variable name, equal sign, and value) of the variable at the specified position. If there is no variable at the specified position, the function returns a blank string. This code displays all environment variables in a Message Box:
Dim buf As String, msg As String, idx As Integer
idx = 1
Do
buf = Environ(idx)
msg = msg & buf & vbCrLf
idx = idx + 1
Loop Until buf = ""
MsgBox msg
By using the Environ function, your Visual Basic program can retrieve the values of any relevant environment variables and use this information as needed.
Many Windows programs, including programs created with Visual Basic, permit more than one instance, or copy, of the program to run at the same time. You can, for example, have two or more copies of Notepad running at the same time, editing different text files. There are situations in which permitting multiple instances of a program is not a good idea. A program might monitor a specific folder and keep track of new files that are placed there; there is never a need to have two instances of such a program running. Likewise, a program that is a front-end to a database should be restricted to one instance because having two or more copies running at the same time could lead to data errors.
To prevent multiple copies of a program from running, use the App.PrevInstance property. This property returns True if there is another instance of the program running. You can check this property in a program's Load event procedure. If it returns True, you can display a message to the user and, if desired, activate the running copy before terminating the new, duplicate instance. Here's a code example that you would place in the Load event procedure:
If App.PrevInstance Then
MsgBox "Another instance of this program is already
open."
AppActivate App.Title
Unload Me
End If
Visual Basic's End statement immediately terminates the program, and because it's so easy to remember a lot of programmers use it regularly for program termination. There's a problem with End, however, in that the program's forms are not unloaded properly. Specifically, each form's Unload event procedure is not triggered. If your program uses the Unload event procedure to save data, close files, and perform other cleanup tasks, then using End to terminate the program is clearly not a good idea.
The preferred way to terminate a program is to unload all of its forms. With a single form program this is not a problem - just execute
Unload Me
When a program has multiple forms, this can be more of a problem - particularly when you do not know which forms are open at the time the program is terminating. To address this task, you can use the Forms collection, which contains all of the program's currently loaded forms. The technique is to loop through the collection, unloading each form in turn until all have been unloaded. The result is that the program is terminated properly, with each form's Unload event rpocedure being triggered. Here's the code:
Public Sub TerminateProgram()
Static Unloading As Boolean
Dim idx As Integer
If Unloading Then Exit Sub
Unloading = True
For idx = Forms.Count - 1 to 0 Step -1
Unload Forms(idx)
Next idx
Unloading = False
End Sub
Note two things about this code. First, the forms are unloaded in reverse order, starting with the last one in the Forms collection. If you started with the first form in the collection, the form numbers would be reassigned by the Forms collection to fill in for the removed form, and the required code would be more complex. Second, a flag is set to prevent the unloading loop from executing if it is already in progress (if, for example, the TerminateProgram procedure was inadvertently called twice).
Remember that a form can block its own unloading by setting the Cancel argument to a non-zero value in the QueryUnload event procedure. In this case the procedure shown above may not be able to close all forms and terminate the program, and the user may have to close one or more forms manually.
You have probably see Web pages that contain many text fields, such as a form for online ordering. Such pages often have a Clear or Reset button that erases the text in all the fields so you can start over. You can implement a similar feature for Visual Basic forms, clearing all the Text Box controls on the form for the entry of new data.
To do so you will use the form's Controls collection. This collection contains an entry for every control on the form. By looping through the collection you can determine the type of each control using the TypeOf keyword. If a control is a Text Box, erase its text; if a control is not a Text Box, ignore it. Here's a procedure to clear all text Box controls on the form:
Public Sub ClearTextBoxes()
Dim c As Control
For Each c in Controls
If TypeOf c Is TextBox Then
c.Text = ""
End If
Next
End Sub
You can extend this technique to other controls. For example, this code clears (unchecks) all CheckBox controls on the form:
For Each c In Controls
If TypeOf c Is CheckBox Then
c.Value = False
End If
Next
A stack stores data in a last-in, first-out manner, and can be very useful in many areas of programming. This tip shows you how to implement a stack in Visual Basic. Traditionally a stack has two operations associated with it: Push to put something on the stack and Pop to retrieve something from the stack. Push always puts a data item on the top of the stack, and Pop always retrieves and removes whatever it on the top of the stack. Suppose you did the following Push operations in this order:
Push A
Push B
Push C
If you then did three Pops, you would retrieve C with the first Pop, B with the second, and A with the first. Note that a stack needs some way to signal when it is empty.
A stack is best implemented as a Class module in Visual Basic. Basically, a stack is an array that holds the data plus a pointer that indicates which position in the array is currently the "top" of the stack. In my Stack class I used a dynamic array that can expand as needed to hold new items that are pushed onto the stack. When items are removed by popping them from the stack, the array does not shrink in size to avoid the overhead of repeatedly calling ReDim.
The code for the Stack class is shown here. This implementation uses type Variant to enable the stack to hold both string and numeric data. You could also create stacks designed specifically for certain data types. Note that the Class_Initialize event procedure initializes the pointer variable to 0 and the data array to a size of 1. Also note that Pop returns Null when the stack is empty, and the program that is using a stack needs to check for this to verify that Pop has returned valid data.
Private data() As Variant
Private pointer As Long
Private Sub Class_Initialize()
pointer = 0
ReDim data(1)
End Sub
Public Function Pop()
If pointer > 0 Then
Pop = data(pointer)
pointer = pointer - 1
Else
Pop = Null
End If
End Function
Public Sub Push(value)
pointer = pointer + 1
If UBound(data) < pointer Then
ReDim Preserve data(pointer)
End If
data(pointer) = value
End Sub
To use the Stack class, create a instance of it using the usual Visual Basic syntax and then call Push and Pop as needed. The following code is from a demo program that you can use to verify how the Stack class operates. To run it, follow these steps:
1. Create a new Visual Basic Standard EXE project.
2. Place two Command Button controls on the form.
3. Create a new Class module and change its Name property to Stack.
4. Add the code listed above to this class module.
5. Add the code below to the form module.
Dim st As Stack
Private Sub Form_Load()
Set st = New Stack
End Sub
Private Sub Command1_Click()
Dim t As Single
t = Timer
Debug.Print "Pushed ", t
st.Push (t)
End Sub
Private Sub Command2_Click()
Debug.Print "Popped ", st.Pop
End Sub
When you run the program, you can click the command buttons to push and pop values (provided by the Timer function) on and off the stack. In a real-world program you'll find numerous uses for the Stack class, whenever you need to temporarily store data on a last-in, first-out basis.
While computers may use binary numbers internally, programmers usually use decimal and sometimes hexadecimal notation in their programs. Even so, it can still be useful to see the binary representation of a number, particularly when you are working with logical operators or doing bitwise comparisons. Converting a binary representation to decimal reverses the process. This tip shows you how to convert a positive integer into a string containing its binary representation, and then back again.
Binary notation is based on powers of 2, and this fact makes the conversion process surprisingly simple. It works as follows (for decimal value X):
1. Calculate X Mod 2. The result will be either 0 or 1.
This is the first (right-most) binary digit.
2. Divide X by 2, discarding any remainder (in other words, perform an integer
division using the \ operator).
3. If X is 0 you are done. Otherwise return to step 1 to determine the next
binary digit.
Let's work this through for the decimal value 13:
1. 13 Mod 2 is 1 so the first binary digit is 1.
2. 13 \ 2 is 6.
3. 6 Mod 2 is 0 so the second binary digit is 0.
4. 6 \ 2 is 3.
5. 3 Mod 2 is 1 so the third binary digit is 1.
6. 3 \ 2 is 1.
7. 1 Mod 2 is 1 so the fourth binary digit is 1.
8. 1 \ 2 is 0 so you are done. The result is 1101, the binary representation of
13
The code is presented here as a Visual Basic function. In addition to the value to be converted, the function takes an argument specifying the minimum number of digits in the binary value. If the actual value has fewer digits that this value, leading zeros are added to make up the difference.
Public
Function DecimalToBinary(DecimalValue As Long, _
MinimumDigits As Integer) As String
' Returns
a string containing the binary
' representation of a positive integer.
Dim
result As String
Dim ExtraDigitsNeeded As Integer
' Make
sure value is not negative.
DecimalValue = Abs(DecimalValue)
' Construct the binary value.
Do
result = CStr(DecimalValue Mod 2) & result
DecimalValue = DecimalValue \ 2
Loop While DecimalValue > 0
' Add
leading zeros if needed.
ExtraDigitsNeeded = MinimumDigits - Len(result)
If ExtraDigitsNeeded > 0 Then
result = String(ExtraDigitsNeeded, "0") & result
End If
DecimalToBinary = result
End Function
Converting a binary representation to a decimal number is the reverse of the process in the above function. Each digit in a binary value represents a power of 2, starting with 2^0 for the first (right-most) binary digit, 2^1 for the second digit, and so on. Note that any number to the 0 power is by definition 1, and any number to the 1 power is the number itself. Using 1101 for another example, you have:
The first digit is 1, and 1 times 2^0 is 1.
The second digit is 0, and 0 times 2^1 is 0.
The third digit is 1, and 1 times 2^2 is 4.
The fourth digit is 1, and 1 times 2^3 is 8.
8 + 4 + 1 equals 13.
A function to perform the binary to decimal conversion is shown here.
Public Function BinaryToDecimal(BinaryValue As String) As Long
' Returns the decimal equivalent of a binary number.
Dim idx
As Integer
Dim tmp As String
Dim result As Long
Dim digits As Integer
digits =
Len(BinaryValue)
For idx = digits To 1 Step -1
tmp = Mid(BinaryValue, idx, 1)
If tmp = "1" Then result = result + 2 ^ (digits - idx)
Next
BinaryToDecimal = result
End Function
This function treats any character other than "1" in the binary value as a "0". You might want to add error-schecking code to ensure that the binary value contains only "0" and "1" characters.
Understanding the AutoRedraw property and the Paint event is important when you are using graphics in your Visual Basic programs. They are related to Visual Basic's graphics methods that let you create output on a form or a Picture Box control. These methods are Circle, Line, PaintPicture, Print, and PSet. These methods may not always work the way you expect. To see what I mean, create a new Standard EXE project and put the following line of code in the Click event procedure for the form:
Me.Circle (1500, 1500), 600
When you run the program and click the form, the circle appears just as you would expect. But try minimizing and then restoring the form - no circle (unless you click the form again)! You get the same result if you switch to another program that covers the form. What's going on - why is the circle temporary?
The answer lies in something called persistence. The form itself and any controls on it are persistent, which means that Visual Basic keeps a copy in memory and uses this copy to redraw the form on the screen when it has been hidden and then redisplayed. In contrast, the output of the graphics methods are not persistent - they are drawn to the screen only and are not saved in memory and cannot be redrawn when the form is hidden then redisplayed.
There are two ways to make the output of graphics methods persistent. One is to set the form's AutoRedraw property to True. This results in any output from the graphics methods being saved in memory as well as being drawn to the screen. They are now persistent and will remain visible when the form is hidden then redisplayed.
The other technique is to place the calls to the methods in the form's Paint event procedure. This event is triggered when the form is first displayed on screen and then again each time it needs to be redrawn after having been covered or minimized. To see this in action, move the line of code that calls the Circle method from the Click event procedure to the form's Paint event procedure. Now, the circle is displayed when the program starts and remains visible when the form is minimized and restored.
When AutoRedraw is set to True the form's Paint event procedure is never called. Which of these techniques you should use depends on the details of your program. Setting AutoRedraw to True usually simplifies programming, but it consumes extra memory and, particularly for complex graphics, can slowe down screen display.
Remember that the question of persistence applies only to the output of the graphics methods. Controls, bitmaps, and metafiles are always persistent.
A Picture Box control can display various kinds of graphics images. Using the technique presented here, you can determine the color at any point in the image.
The Point method returns the color at a specified X,Y location in a Picture Box control. To get the color under the mouse pointer, put the code in the MouseMove event procedure for the control. Because the MouseMove procedure is passed the current X,Y coordinates of the pointer, this code is simple:
Private Sub Picture1_MouseMove(Button As Integer, _
Shift As Integer, X As Single, Y As Single)
Dim rgb As Long
rgb = Picture1.Point(X, Y)
End Sub
The value returned by the point method is a type Long that encodes the RGB value for the color. To be useful, this encoded value must be separated into the individual R, G, and B components, each of which is an integer in the range 0-255 decimal or 00-FF in hexadecimal. Expressed in hexadecimal notation, the value returned by Point is:
00BBGGRR
Hexadecimal notation makes it easy to perform the extraction. If rgb is the value returned by the Point method, this extraction is performed as follows:
red = rgb Mod &H100
green = (rgb \ &H100) Mod &H100
blue = (rgb \ &H10000) Mod &H100
The final MouseMove event procedure is shown here. To see this in action, create a Standard EXE project and place a Picture Box and a Text Box on the form. Load an image into the Picture Box then run the project. You'll see that when the mouse moves over the image the Text Box displays the RGB values of the underlying pixel.
Private Sub Picture1_MouseMove(Button As Integer, _
Shift As Integer, X As Single, Y As Single)
Dim rgb As Long
Dim r As String, g As String, b As String
rgb = Picture1.Point(X, Y)
r = CStr(rgb Mod &H100)
g = CStr((rgb \ &H100) Mod &H100)
b = CStr((rgb \ &H10000) Mod &H100)
Text1.Text = "R " & r & ", G " & g & ", B " & b
End Sub
You could place the same code in the MouseDown event procedure if you want to see the RGB values for only pixels that you click.
It's easy enough to set a form's size in code using its Width and Height properties, but this sets the form's outer size including borders and title bar. What if you want to set the form's interior area to a specific size, for example to precisely fit a Picture Box control? This tip shows you how.
You may think that you can change the form's ScaleWidth and ScaleHeight properties but this does not work. Setting these properties does not change the form interior's size but rather changes the units of measure used for graphics operations on the form. You can, however, read these properties to determine the size of the form's interior. This permits use of the following strategy:
Get the Form.Width value. This is the overall width of the form including borders.
Get the Form.ScaleWidth value. This is the width of the interior of the form.
Subtract the second value from the first. The result is the amount that the form's borders contribute to its overall width. Call this value deltaX.
Take the desired internal width and add deltaX to it. This is the overall width that the form will need in order to have the desired internal width.
Set the Form.Width property to the value obtained in step 4.
The same process is used for the height. To see this technique in action:
Start a new Standard EXE project.
Place a Picture Box control on the form and set its AutoSize property to True. This causes the Picture Box to automatically resize itself to fit the picture loaded into it.
Load an image into the Picture Box control.
Place the following code in the Form's Load event procedure.
Private Sub Form_Load()
Dim deltaX As Single, deltaY As Single
Picture1.Move 0, 0
deltaX = Me.Width - Me.ScaleWidth
deltaY = Me.Height - Me.ScaleHeight
Me.Height = Picture1.Height + deltaY
Me.Width = Picture1.Width + deltaX
End Sub
When you run the project you will see that the image is moved to the top left corner of the form and the form is resized to precisely fit the Picture Box. You can verify this by using the mouse to expand the form, revealing that the borders of the form and the Picture Box coincide exactly.
This technique requires that the ScaleMode, ScaleWidth, and ScaleHeight property of the form not be changed from the default values. This is because the size calculations depend on both the ScaleWidth, and ScaleHeight properties being in the default unit of twips.
It's not uncommon for programs to require some form of password protection. When a password is stored, in the registry for example, you do not want it in readable form because many users have the technical know-how to access the registry. Likewise, passwords are sometimes sent as part of a Web GET request. Some form of simple encryption should be used, as explained here.
There are various complex encryption alorithms available, but sometimes simple is better. A simple ASCII-value transposition is sometimes all that is needed. For example, suppose the password is "ajax7." You could encode it as follows:
1. The ASCII value of "a" is 97. Add 1 to get 98, which is the ASCII value of "b"
2. The ASCII value of "j" is 106. Add 1 to get 107, which is the ASCII value of "k"
3. The ASCII value of "a" is 97. Add 1 to get 98, which is the ASCII value of "b"
4. The ASCII value of "x" is 97. Add 1 to get 98, which is the ASCII value of "y"
5. The ASCII value of "7" is 55. Add 1 to get 56, which is the ASCII value of "8"
The encoded password is therefore "bkby8". The process of decoding is simply the reverse, subtracting 1 from each ASCII value. Visual Basic procedures for encoding and decoding passwords using this algorithm are shown here. It is assumed that passwords are limited to letters and numbers.
Public Function EncodePassword(pw As String) As String
' Passed a pw, returns the encoded pw.
Dim codedPW As String
Dim i As Integer
Dim ch As String * 1
If Len(pw) = 0 Then
EncodePassword = ""
Exit Function
End If
codedPW = ""
pw = Trim(pw)
For i = 1 To Len(pw)
ch = Mid(pw, i, 1)
codedPW = codedPW & Chr(Asc(ch) + 1)
Next i
EncodePassword = codedPW
End Function
Public Function DecodePassword(codedPW As String) As String
' Passed an encoded pw, returns the decoded pw.
Dim decodedPW As String
Dim i As Integer
Dim ch As String * 1
If Len(codedPW) = 0 Then
DecodePassword = ""
Exit Function
End If
decodedPW = ""
codedPW = Trim(codedPW)
For i = 1 To Len(codedPW)
ch = Mid(codedPW, i, 1)
decodedPW = decodedPW & Chr(Asc(ch) - 1)
Next i
DecodePassword = decodedPW
End Function
You could increase the complexity of the algorithm in various ways. For example, the amount added to each character's ASCII could be the character's position in the string rather than a constant. These algorithms would not keep the FBI or CIA at bay for long, but they are more than adequate when all you need to do is hide plain-text passwords from casual observers.
In the workaday world, one of the most important questions about any specific date is whether it falls on a weekend. The function presented here can answer this question.
Visual Basic provides several sophisticated functions for working with dates. One of them, the Weekday function, is the tool needed for this task. This function takes a date as an argument. This date can be a type Date or it can also be a Variant or string that can be interpreted as a date. The function's return value is an integer that identifies the day of the week that the specified date falls on: 1 is Sunday, 2 is Monday, and so on up to 7 for Saturday. It's an easy matter to determine whether the function returned 1 or 7, indicating a weekend, or another value. Here's the code:
Public Function IsWeekend(d As Date) As Boolean
If Weekday(d) = 1 Or Weekday(d) = 7 Then
IsWeekend = True
Else
IsWeekend = False
End If
End Function
The problem with this function is that it assumes that d is a valid date - if not, an error occurs. Of course the calling program can use the IsDate function to make sure a date is valid before calling IsWeekend, but it is preferable to have the function self-contained. This can be accomplished by using the flexibility of the Variant data type, as shown in this rewritten function:
Public Function IsWeekend(d As Variant) As Variant
If Not IsDate(d) Then
IsWeekend = Null
Exit Function
End If
If Weekday(d) = 1 Or Weekday(d) = 7 Then
IsWeekend = True
Else
IsWeekend = False
End If
End Function
With this rewrite, the function returns True if the date is a weekend day, false if it is not, and Null if the date passed was not in fact a valid date.
While most of today's systems have a sound card built in, there are still millions of older computers being used. If your program depends on sounds you may want to check to see if a sound card is available and, if not, take alternate action. To detect a sound card you will use a function in the Windows API. This requires that the function be declared at the module level in your program, letting Visual Basic know about the function and its arguments and return type. The declaration is as follows:
Declare Function waveOutGetNumDevs Lib "Winmm.dll" ()As Long
emember, simply being installed is not enough to make a sound card available - the proper drivers must be installed as well, and the card must be recognized by Windows. This API function returns the umber of sound cards available. That there may be 2 or more cards available is not of interest - as long as at least one card is available then our question has been answered. It's a simple matter to write a short Visual Basic function that returns True or False based on the return value of waveOutGetNumDevs:
Public Function SoundCardAvailable() As Boolean
If
waveOutGetNumDevs() > 0 Then
SoundCardAvailable = True
Else
SoundCardAvailable = False
End If
End Function
When your program detects that a sound card is not available it could revert to using the Beep statement and the computer's built-in speaker to issue sound alerts.
The PC keyboard has three lock keys: Caps Lock, Num Lock, and Scroll Lock. You can read the state of these keys from your Visual Basic program using the technique presented here. This technique depends on a Windows API function. The declarations of this function, which must be included at the module level in a program that uses the function, is shown here:
GetKeyState Lib "user32" (ByVal nVirtKey As Long) As Integer
Getting the state of a key requires that you call GetKeyState and pass it the Visual Basic constant that identifies the key: vbKeyNumLock, vbKeyScrollLock, or vbKeyCapital. The function returns 0 if the key is off and 1 if it is on.
This function is not limited to use with the lock keys - it can also determine the state of the other keys on the keyboard. Passed the key code for a non-lock key, such as vbKeyX of vbKeyF6, the function returns 0 if the key is up and non-zero if it is down. This information is of limited usefullness, however, because the information reflects the state of the key only at the time the most recent input message for the key was processed. In other words, for the non-lock keys the information returned by GetKeyState is only a snapshot of the keyboard state at a given moment in time.
To parse a string means to extract subsections of the string based on their position or, more commonly, a delimiter character. Common needs for parsing strings include using command line arguments and reading CSV (comma separated value) files. This tip presents a Visual Basic function that should meet all your parsing needs.
On the surface, parsing a string based on a delimiter character seems simple enough. For example, the string "a,b,c" is easily parsed into the three substrings "a" "b" and "c" based on using the comma as the delimiter character. But things are not always so simple. What about the string "a,,b,c"? This should be parsed as four substrings because the pair of commas indicates a missing, or blank, second substring. The same is true of the string ",a,b,c" where the initial comma indicates a missing or blank first substring. Thus, any parsing function must be able to take such missing or blank values into account.
A second requirements is the use of multiple characters as a delimiter. In some applications this may be desirable so as to not preclude the use of any characters in the data. For example, using the \ character as a delimiter works fine but means that the data itself cannot contain this character. Using \\ will circumvent this limitation.
The function ParseString() meets all of these requirements. It is passed two arguments, the string to be parsed and the delimiter string. It returns an array containing the parsed substring. Under certain error conditions (explained in the comments in the code) it returns Null. The calling program can use the UBound function to determine the size of the returned array and then access the individual substrings as needed.
Public Function ParseString(StringToParse As String, Delim As String) As Variant
' Returns an array
containing the results of parsing
' a string based on the specified delimiter.
' If StringToParse or Delim is empty returns Null
' If Len(Delim) >= Len(StringToParse returns Null.
' If StringToParse does not contain the delimiter, returns
' the entire string.
Dim a() As String
Dim s As String
Dim DelimPos As Integer
Dim count As Integer
If Len(StringToParse) = 0
Or Len(Delim) = 0 Then
ParseString = Null
Exit Function
End If
If Len(Delim) >=
Len(StringToParse) Then
ParseString = Null
Exit Function
End If
DelimPos = InStr(1,
StringToParse, Delim)
If DelimPos = 0 Then
ReDim a(0)
a(0) = StringToParse
ParseString = a
Exit Function
End If
s = StringToParse
count = 0
Do
ReDim Preserve a(count)
a(count) = Left(s, DelimPos - 1)
s = Right(s, Len(s) - (DelimPos + Len(Delim) - 1))
count = count + 1
DelimPos = InStr(1, s, Delim)
If DelimPos = 0 Then
ReDim Preserve a(count)
a(count) = s
s = ""
End If
Loop Until Len(s) = 0
ParseString = a
End Function
Sometimes you want an application that just runs and does its job without interacting with a user at all, or at most in a minimal way. Such an application might be called by other applications or batch files to perform tasks such as file manipulations. Or, it might be a command line utility that runs without a window. This tip shows you how to create such applications in Visual Basic.
You might think that Visual Basic applications are inherently linked to a form, but that's not the case. Yes, a Visual Basic application has a form by default but there's no reason you cannot get rid of it. That one step in creating a formless Visual Basic application. Here's the entire process:
1. Create a new Standard EXE Visual Basic application.
2. Add a code module to the project.
3. Select the project's default form and delete it using the Project|Remove
Form1 command.
4. Add a procedure named Main to the program's code module (this is where
execution starts when the program is run).
The module of a formless application can contain other Sub And Function procedures as needed. The application can also use class modules, make API calls, and do just about anything that a regular Visual Basic application can that does not involve forms and controls. A formless Visual Basic application can also use visual screen elements such as MsgBox and InputBox, but these calls are usually best avoided because they would defeat the main purpose of having a formless program.
How can you get information to a formless application? The most common method is to use command line arguments. In the program, the Command$ function returns a string containing any command line arguments that were passed when the program was started. These arguments are included after the program name as shown here:
C:\>ProgramName argument1 argument2
The arguments could be typed by the user at a command prompt, included as part of a batch file command, or included in the properties of a shortcut to the program.
The code below presents an example of a formless program. It is passed the name of a text file as a command line parameter. The program reads the file, replaces each Tab character with two space characters, and writes the file back to disk. The code lacks the error checking and other safety features that a real-world program would need. To compile this program you need to have "Microsoft Scripting Runtime" selected in the References dialog box (select Project|References). After compiling the program to an EXE from Visual Basic you would run it from the command line as follows:
C:\TabsToSpaces myfile.txt
The ability to use the powerful Visual Basic programming language to create formless command line programs is an added attraction that you may find useful someday. It becomes even more flexible when your program can access the stdin and stdout streams, a topic that is covered here.
Sub Main()
Dim fs As New Scripting.FileSystemObject
Dim f As TextStream
Dim filename As String
Dim strin As String
Dim strout As String
Dim ch As String
Dim idx As Long
filename = Command$
Set f = fs.OpenTextFile(filename)
strin = f.ReadAll
f.Close
For idx = 1 To Len(strin)
ch = Mid(strin, idx, 1)
If ch = Chr(9) Then
strout = strout & " ""
Else
strout = strout & ch
End If
Next idx
Set f = fs.CreateTextFile(filename, True)
f.Write (strout)
f.Close
End Sub
The standard input and output streams, referred to as stdin and stdout, originated back in the days before graphical user interfaces, when all computing was text based. They may seem archaic, but can still be very useful to a Visual Basic programmer.
Stdin is where a program got its input from, by default the keyboard. Likewise stdout is where a program sent its output, by default to the screen. Much of the power of these streams came from the fact that they could be redirected so that a program could, for example, send its output to a file instead of the screen. Today, there are two common uses for stdin and stdout. One is with formless Visual Basic programs (as was covered in a previous tip) that run from a batch file or the command prompt - by using stdout with redirection, a program's output can go to a file rather than being displayed at the command prompt. The other is for Web programming with CGI, the Common Gateway Interface. While it is not the "latest and greatest," this technology is still used on a lot of Web sites to provide executable functionality, and a CGI program must use stdin and stdout to operate. This tip is not about CGI program, but rather shows you how you can use stdin and stdout in your Visual Basic programs.
Using these streams requires that a program get a handle to the stream using the API function GetStdHandle. The declaration for this function is:
Public Declare Function GetStdHandle Lib "kernel32" (ByVal nStdHandle As Long) As Long
Pass the argument -11& to get the handle for stdout and -10& for stdin. To read data from stdin use this API function:
Private Declare Function ReadFile Lib "kernel32" _
(ByVal hFile As Long, ByVal IpBuffer As Any, _
ByVal nNumberOfBytesToRead As Long, _
IpNumberOfBytesRead As Long, _
ByVal IpOverlapped As Any) As Long
The arguments are:
hFile is the stdin handle obtained from GetStdHandle.
Ipbuffer is a string variable where the data read from stdin will be placed.
nNumberOfBytesToRead is the number of bytes to read. Its value should be one less than the size of Ipbuffer.
IpNumberOfBytesRead is a type Long where the function returns the actual number of bytes read from stdin.
IpOverlapped is used for reading files only. When reading stdin, pass a value of zero.
This following code fragment shows how you would use these two functions to read from stdin. Note that the string variable used to receive the data is padded with spaces before being passed to the API function.
Dim buf as string, BytesRead As Long, hStdIn As Long
buf = Space(5000)
StdIn = GetStdHandle(-10&)
ReadFile hStdIn, buf, Len(buf) - 1, BytesRead, 0&
To send data to stdout use this API function:
Private Declare Function WriteFile Lib "kernel32" _
(ByVal hFile As Long, ByVal IpBuffer As Any, _
ByVal nNumberOfBytesToWrite As Long, _
IpNumberOfBytesWritten As Long, _
ByVal IpOverlapped As Any) As Long
hFile is the handle of stdout obtained with GetStdHandle.
IpBuffer is a string variable containing the data to be written.
nNumberOfBytesToWrite is the number of bytes to write, the length of IpBuffer.
IpNumberOfBytesWritten is where the function returns the actual number of bytes written.
IpOverlapped is not used when writing to stdout and you should pass a value of 0.
Here is a code fragment showing how it is done. The variable temp holds the text to be written to stdout:
Dim hStdOut as Long, BytesWritten As Long
hStdOut = GetStdHandle(-11&)
Call WriteFile(hStdOut, (temp), Len(temp), BytesWritten, 0&)
Note the parentheses around the temp argument to disable Visual Basic's argument type checking. With the ability to use the standard stdin and stdout streams, you can use Visual Basic to create programs for tasks you never thought Visual Basic was suitable for.
A program's user interface can be improved by using short sounds to signal certain events such as a download completing or a command being selected. With the right Windows Multimedia function, playing WAV files from a Visual Basic program is child's play. Of course, the system must be equipped with a sound card for this to work.
The API function you need is called, appropriately enough, PlaySound. Its declaration is shown here:
Public Declare Function PlaySound Lib "winmm.dll" _
Alias "PlaySoundA" (ByVal lpszName As String, _
ByVal hModule As Long, ByVal dwFlags As Long) As Long
The first argument is the name, including path, of the Wave file to play. The second argument is not used when playing sound files and you should pass a value of zero (this function can also play sounds that are associated with system events, but that is not covered here). The final argument consists of flags that control various aspects of how the function works. For the present purposes, two flags are used. They are (with the constants that are typically used for them:
SND_ASYNC (value= 1) - play asynchronously, meaning that the function returns while the sound is still playing.
SND_FILENAME (value = &H20000) - the first argument is a file name.
Thus, the following code plays the sound in DingDong.wav:
PlaySound "dingdong.wav", CLng(0),SND_ASYNC Or SND_FILENAME
When playing sound files in a program there are two other considerations. First, you want to make sure that the specified Wave file exists or else an error will occur. This can be done with the simple function shown here:
Public Function FileExists(FullFileName) As Boolean
' Passed a filename (with path) returns
' True if the file exists, False if not.
Dim s
s = dir(FullFileName)
If s = "" Then
FileExists = False
Else
FileExists = True
End If
End Function
Second, the program should have an option setting that lets users turn sounds off. This can be maintained as a global Boolean variable with a name such as gProgramSoundsEnabled. My approach is to deal with both of these considerations in a function as follows. This code assumes that the Wave files are stored in the application folder.
Private Sub PlaySoundX(filename As String)
' If sound is enabled and filename exists,
' play the specified sound.
filename = App.path & "\" & filename
If FileExists(filename) And gProgramSoundsEnabled Then
PlaySound filename, CLng(0), SND_ASYNC Or SND_FILENAME
End If
End Sub
With this function in place, the program can play any Wave file like this:
PlaySoundX "DingDong.wav"
Another way of playing sounds from a Visual Basic program involves including a multimedia control on a form. When you do not need the extra features of the control, however, the technique presented here is a lot easier.
Have you ever thought it would be great to be able to send email from a Visual Basic program? With Visual Basic's MAPI controls, it's a snap. These two controls let you send messages on any MAPI-compliant email system, such as Outlook and Exchange. To use the MAPI controls, you must select them in your project's Components dialog box. They are listed as "Microsoft MAPI Controls 6.0." The two controls will then appear in your toolbox; they are called MAPISession and MAPIMessages. Place one of each on a form - they are invisible at runtime.
The MAPISession control is used to establish a session, or connection, with whatever MAPI-compliant mail software is installed on the system. The control has UserName and Password properties for signing onto an email account. You can set these properties at design time or prompt the user for them in code. Then, call the control's SignOn method to establish the session. Once the session is established, the control's SessionID property returns a handle of the session.
The MAPIMessages control must be passed the handle of the MAPI session, obtained from the MAPISession control's SessionID property. Once this is done, the MAPIMessages control can be used for various tasks such as accessing messages in the InBox, saving, copying and deleting messages, and working with attachments. For this tip we are interested in creating and sending messages. This requires the following steps:
1. Call the Compose method to create a new message.
2. Put the recipient, the subject, and the body of the message in the
corresponding control properties.
3. Call the Resolve method to verify the message recipient.
4. Call the Send method to send the message.
Sending a message with the MAPIMessages control does not literally send it, but puts it in the Outbox of the mail system. When the message is actually sent depends on the mail system settings. When your program is finished with mail-related activities, call the MAPISession control's SignOff method to terminate the session.
The following code demonstrates this. It assumes that the form containing the code contains TextBox controls for the various bits of information needed: user name, password, etc.
Private Sub SendMail_Click()
MAPISession1.UserName = tstUserName.Text
MAPISession1.Password = txtPassword.Text
MAPISession1.SignOn
MAPIMessages1.SessionID = MAPISession1.SessionID
MAPIMessages1.Compose
MAPIMessages1.RecipAddress = txtTo.Text
MAPIMessages1.MsgSubject = txtSubject.Text
MAPIMessages1.MsgNoteText = txtMessage.Text
MAPIMessages1.ResolveName
MAPIMessages1.Send
MAPISession1.SignOff
End Sub
An alternate way to use the Send method is to pass an argument with the value True (the default for this argument is False, that's why it is not included in the code above).
MAPIMessages1.Send True
In this case, the Send method displays a message dialog box in which the user can enter or edit the elements of the message and then send it by clicking the Send button.
When a Visual Basic program needs to send email messages, perhaps for support or licensing issues, it's a lot nicer to integrate email support in the program rather than requiring the user to switch to their own email software.
In the old days, Windows programs and the operating system itself used INI (for Initialization) files to store program settings and other information. Since Windows 98 these tasks have been taken over by the Windows registry. Even so, there are times when storing a program's settings in an INI files is still preferable. This tip shows you how.
An INI files a text file that is organized into sections. Each section contains keys and values. Here's an example:
[section1]
key1 = value1
key2 = value2
[section2]
key1 = value1
key2 = value2
You can see that an individual entry is identified by both the section it is in and its key name. Your program can write and read INI files using these two API functions:
Private
Declare Function GetPrivateProfileString _
Lib "kernel32" Alias "GetPrivateProfileStringA" _
(ByVal lpApplicationName As String, _
ByVal lpKeyName As Any, ByVal lpDefault As String, _
ByVal lpReturnedString As String, ByVal nSize As Long, _
ByVal lpFileName As String) As Long
Private
Declare Function WritePrivateProfileString _
Lib "kernel32" Alias "WritePrivateProfileStringA" _
(ByVal lpApplicationName As String, ByVal lpKeyName _
As Any, ByVal lpString As Any, ByVal lpFileName As _
String) As Long
Both functions return 1 on success and 0 on failure. A failure would occur if, for example, you try to write an INI file to a location where you do not have sufficient permissions. The arguments are as follows:
lpApplicationName: The name of the section to read or write. This argument name reflects the fact that INI files used to be organized so that each section was named after the application whose settings it held.
lpKeyName: The name of the key to read or write.
lpString: The value to be written to the INI file (when writing).
lpDefault: The default value that is returned in the section and/or key is not found (when reading).
lpReturnedString: The data read from the INI file (when reading).
nSize: The maximum number of characters to return. Set to the length of lpReturnedString
lpFileName: The name, including full path, of the INI file.
When you are writing to an INI file and the section and key exist, the current value is replaced with the new value. Otherwise the section and/or key are created. When reading you get either the value in the file or, if the section/key do not exist you get the default value passed to the GetPrivateProfileString argument. This default value is also returned if the INI file does not exist. Because an INI file is text only, storing a numeric value requires converting it to a string when saving it and then converting the string value back to a number when retrieving it.
The following code shows how to write a value to an INI file and then read it back. In the code for reading an INI file, note how the string variable that will receive the returned value is padded with strings before being passed to the function.
Private Sub cmdWriteINIFile_Click()
Dim retval As Long
retval = WritePrivateProfileString("MyApp", "Key1", _
"Real value", App.Path & "\" & "Data.ini")
If retval = 0 Then MsgBox "Error writing INI file"
End Sub
Private Sub cmdReadINIFile_Click()
Dim retval As Long
Dim value As String
value = Space(128)
retval = GetPrivateProfileString("MyApp", "Key1", _
"Default value", value, Len(value), _
App.Path & "\" & "Data.ini")
If retval = 0 Then
MsgBox "Error reading INI file"
Else
MsgBox "The value read is: " & value
End If
End Sub
Why use INI files in preference to the registry? One reason is that the data in an INI file is a lot easier to view and edit, using any text editor. If you use descriptive names for your sections and keys this becomes even easier. Most of us know what a bother it is to edit the registry. A second and perhaps more important reason is that a program's settings can easily go along with it when the program is moved to or installed on a new system, simply by copying its INI file. Of course there's no reason a program cannot use both the registry and an INI file.
You can use the Shell function to run another executable program from within a Visual Basic program. This tip shows you a simple way to improve on Shell's functionality.
Shell takes two arguments. The first is the name, including path, of the EXE file to run. The second argument specifies the window style of the program. This argument is optional - if omitted, the program is started minimized with the focus. You can refer to the Visual Basic help for more information on the window style argument because it is not relevant here. The techniques presented here work regardless of the program's window style and even if the program does not have a window.
Shell runs its target program asynchronously, meaning that execution can return to the Visual Basic program before the second program is finished executing. In many situations this is not a concern, but at other times it may be. One example is when your Visual Basic program is dependent in some way on the result of the shelled program completing its operation. In this case you need some way to pause the Visual Basic program until the other program is done.
The first step is to get the handle of the shelled program. The Shell function returns its program ID, or PID, and once you have that you can get the handle using the OpenProcess API function:
Public Declare Function OpenProcess Lib "kernel32" _
(ByVal dwDesiredAccess As Long, _
ByVal bInheritHandle As Long, _
ByVal dwProcessId As Long) As Long
Pass the value &H100000 (usually represented by the constant SYNCHRONIZE) for the first argument and 0 (as a long) for the second. The third argument is the PID returned by the Shell function. The function's return value is a handle to the Windows process representing the shelled program. With this handle, you can use the WaitForSingleObject API function to pause the Visual Basic program until the shelled program has terminated. The declaration is:
Private Declare Function WaitForSingleObject Lib _
"kernel32" (ByVal hHandle As Long, _
ByVal dwMilliseconds As Long) As Long
The first argument is the handle of the program to wait for, obtained from the OpenProcess function. The second argument is the length of time to wait. If you pass a millisecond value, the function returns when the shelled program completes or when the specified interval is up, whichever occurs first. If you want the function to wait as long as necessary for the shelled program to complete, pass the value &HFFFF (often represented by the constant INFINITE).
Here is sample code that shows how to use these API functions to shell a program and wait for it to complete.
Const SYNCHRONIZE = &H100000
Const INFINITE = &HFFFF
Private Sub ShellProgramAndWait(ProgramName As String)
Dim hHandle As Long, pid As Long
txtStatus.Text = "Processing"
txtStatus.Refresh
pid = Shell(ProgramName, vbNormalFocus)
If pid <> 0 Then
hHandle = OpenProcess(SYNCHRONIZE, 0&, pid)
WaitForSingleObject hHandle, INFINITE
txtStatus.Text = "Finished"
Else
txtStatus.Text = "Error shelling " & ProgramName
End If
End Sub
The WaitForSingleObject function effectively freezes your Visual Basic program, something you need to take into account so the user does not think something is wrong. In this code, for example, a message is displayed to the user in a Text Box control. Note the use of the Refresh method to make sure the message is actually displayed before the program enters its wait state. Then, when the shelled program terminates, a "finished" message is displayed. With the techniques presented here, Visual Basic's Shell function becomes an even more useful tool.
This tip shows you how to clip the mouse cursor, restricting its movement to your program's form. This is a good way to ensure that a user's attention stays focused on your program. It's not perfect, because there are ways to get around it, but it can still be handy in some situations. You use the API function ClipCursor to restrict the cursor to a specific area of the screen. Here's the declaration:
Declare Function ClipCursor Lib "user32" (lpRect As Any) As Long
The function's one argument is a RECT structure that defines the clipping area. The area is expressed in screen pixels, and sample code given below shows how to convert Visual Basic's twips to pixels. The RECT structure, or UDT in Visual Basic, is shown here:
Public Type RECT
Left As Long
Top As Long
Right As Long
Bottom As Long
End Type
To turn off clipping you can call ClipCursor again, passing a RECT structure that contains the coordinates of the entire screen. It's easier, however, to use the ClipCursorBuNum function that is intended specifically for turning clipping off:
Declare Function ClipCursorByNum Lib "user32" Alias _
"ClipCursor" (ByVal lpRect As Long) As Long
Note that this function is actually an alias of ClipCursor that accepts a type Long argument. Pass the value zero to turn clipping off. The following two Click event procedures can be associated with buttons on a form to demonstrate this technique.
Private Sub cmdClip_Click()
Dim ClipArea As RECT
With ClipArea
.Left = Me.Left / Screen.TwipsPerPixelX
.Top = Me.Top / Screen.TwipsPerPixelY
.Right = .Left + Me.Width / Screen.TwipsPerPixelX
.Bottom = .Top + Me.Height / Screen.TwipsPerPixelY
End With
Call ClipCursor(ClipArea)
End Sub
Private Sub cmdRelease_Click()
Call ClipCursorBynum(0)
End Sub
There are several actions that disable clipping, including resizing or moving the Visual Basic form, activating another program (for example, with Alt+Tab), or pressing Ctrl+Alt+Del. You program can deal with the form actions by re-establishing clipping whenever the form is moved or resized. Note that clipping remains in effect after the program ends, so a program should always be sure to turn clipping off before it terminates.
Clipping the mouse cursor is not a foolproof way to restrict mouse input to your program, but in some cases, particularly with users who are not too computer-savvy, it can work.
Have you ever wanted to move the mouse cursor under program control? This tip shows you how to do so using the API function SetCursorPos. The declaration for this function is:
Declare Function SetCursorPos Lib "user32" (ByVal x As Long, _
ByVal y As Long) As Long
The two arguments give the cursor position. This must be given in screen pixels, so there's a twip-to-pixel conversion required when using the function in a Visual Basic program. The return value, which is often ignored, is 0 on failure and nonzero on success.
Why would you want to move the mouse cursor under program control? The main reason that I use this technique is to position the cursor at the location where the user is most likely to want it. For example, if your program displays a form for confirmation of program settings, and the user is most likely to simply accept the default settings by clicking the OK button, you could position the mouse cursor over the OK button when the form is displayed, saving the user the effort.
To use the SetCursorPos function you obviously must know the pixel coordinates of the location that you want to move the cursor to. Suppose, for example, you want the cursor positioned in the center of a Command Button. At first glance the following calculations seem to be what's needed (these are for the X position; the Y position follows the same logic):
(The form's Left property) plus (the control's Left property) plus (one-half the control's Width property)
This is on the right track, but it ignores the width of the form borders and, for the Y coordinate, the title bar. Remember, the Form.Left property gives the position of the outer corner of the form, while a control's Left property gives its position relative to the form's client area (the interior). To determine the width of the form's border you can subtract the form's ScaleWidth, the width of the interior area, from the Width (the width of the entire for including borders). By adding this value to the calculation above you will be right on target. Note that this technique requires that the form's ScaleMode be at the default Twips setting, otherwise it will not work.
Wait, we are not quite done - this calculation gives the desired cursor position in Twips while the SetCursorPos function needs pixels. This is easily remedied by dividing with the Screen.TwipsPerPixelX and Screen.TwipsPerPixelY properties to convert twips to pixels.
The sample code below shows how this is done. This is a form's Load event procedure. When the form is loaded, the mouse cursor is automatically positioned on a Command Button control named Command1.
Private Sub Form_Load()
Dim x As Long, y As Long
x = ((Me.Left + (Me.Width - Me.ScaleWidth) + _
Command1.Left) + Command1.Width / 2) _
/ Screen.TwipsPerPixelX
y = ((Me.Top + (Me.Height - Me.ScaleHeight) + _
Command1.Top) + Command1.Height / 2) / _
Screen.TwipsPerPixelY
SetCursorPos x, y
End Sub
If you choose to add this feature to your program, it's advisable to make it an option that the user can turn on or off. Some users love it, others hate it, but this way you can please everyone!
I am sure that you have seen the rubber-band boxes, sometimes called selection boxes, that some programs use to select areas on the screen. Point with the mouse, hold down the mouse button, and move the mouse, and a rectangular box is drawn between the original and the current mouse positions. The term "rubber-band" comes from the way the box grows and shrinks as the mouse is moved. When you release the mouse button the box remains on-screen. This tip shows you how to create rubber-band boxes in Visual Basic.
Drawing a box on a form or a Picture Box control is easily done with the Line method. The problem is how to make the previous box disappear when the mouse is moved and a new box is drawn. The answer to this lies in the DrawMode property. The default setting draws a solid line, but this won’t work for our purposes. Rather we will use the setting vbInvert which means that drawing operations such as the Line method draw using the inverse of the color that is already there on the form or Picture Box control. Thus, if the background is white, vbInvert draws a black line, and if the background is black a white line is drawn. Colors have their inverses too. This solves two problems:
The box will always be visible against the background.
If we draw the box a second time the colors will be re-inverted back to their original values and the box will disappear.
The approach, then, is as follows:
When the user presses the mouse button, record the current mouse coordinates. This will be the fixed corner of the box. At the same time, set a "drawing" flag to True.
When the mouse is moved, erase the previous box by drawing it again. This step is not required the first time a box is drawn but it required all subsequent times.
Also when the mouse is moved, draw a box between the starting coordinates and the new mouse coordinates.
When the mouse button is released, set the drawing flag to False.
This is demonstrated in the following code snippet. To try this out, create a Standard EXE project and place a Picture Box control on the form. Load the Picture Box with an image of your choice, and set its DrawMode property to 6 - Invert and its AutoRedraw property to True. Next, put the following variable declarations at the module-level in the form:
' True if a box is being drawn.
Dim drawing As Boolean
' True if the box has just started.
Dim first As Boolean
' Previous box's mouse coordinates.
Dim oldX As Single, oldY As Single
' The box's starting coordinates.
Dim startX As Single, startY As Single
Finally, put the code shown here in the mouse-related event procedures for the Picture Box control:
Private
Sub Picture1_MouseDown(Button As Integer, _
Shift As Integer, X As Single, Y As Single)
drawing = True
first = True
startX = X
startY = Y
End Sub
Private Sub Picture1_MouseMove(Button As Integer, _
Shift As Integer, X As Single, Y As Single)
If Not drawing Then Exit Sub
If Not first Then
Picture1.Line (startX, startY)-(oldX, oldY), , B
Else
first = False
End If
Picture1.Line (startX, startY)-(X, Y), , B
oldX = X
oldY = Y
End Sub
Private Sub Picture1_MouseUp(Button As Integer, _
Shift As Integer, X As Single, Y As Single)
drawing = False
End Sub
When you run the program you will be able to draw rubber band boxes on the picture. Each box you draw remains in place, and you can draw essentially as many as you like. If you want an existing box to vanish when a new one is started, put code in the MouseUp event procedure to save the coordinates of the existing box. Then, in MouseDown, erase any existing box by drawing over it. You would want to save the coordinates of the box in any case so your program can make use of them, for example to perform processing on the selected region of the image.
Few Visual Basic programmers realize it, but the TextBox control has an undo feature built right into it. Here's how to use that feature from a Visual Basic program. Undo is a wonderful feature that is available in most commercial programs for editing text or graphics. Many programs offer multi-level undo so you can "roll back" a whole series of actions. Many also offer "redo" to cancel the last undo command. These sorts of sophisticated features require some complex programming, but a simple version of undo is readily implemented for text Box controls. The undo capability is already built into the control, so all that you need to do is send the control a message saying "undo yourself." This is done using the API SendMessage function. Its declaration is:
Declare Function SendMessage Lib "user32" Alias _
"SendMessageA" (ByVal hWnd As Long, _
ByVal wMsg As Long, ByVal wParam As Long, _
lParam As Any) As Long
The first argument is a handle to the message recipient, in this case the hWnd property of the Text Box control.
The second argument is a number identifying the message. For undo, pass the value &HC7, often represented by the constant EM_UNDO.
The third and fourth arguments are used for some messages but not this one - you should pass 0.
The return value depends on the message being sent. In this case it can be ignored.
To see this technique in action, create a project with a form that contains one Text Box control and one Command Button control. Put this line of code in the Command Button's Click event procedure:
SendMessage Text1.hWnd, EM_UNDO, 0&, 0&
Add a module to the project and add the following code to it:
Public Const EM_UNDO = &HC7
Public Declare Function SendMessage Lib "user32" _
Alias "SendMessageA" (ByVal hWnd As Long, _
ByVal wMsg As Long, ByVal wParam As Long, _
lParam As Any) As Long
When you run the project, enter and edit text in the Text Box control. You'll see that the button undoes the latest action. If there's nothing to undo, the message has no effect. You could implement undo as a menu command that affects the active Text Box as follows:
If TypeOf ActiveControl Is TextBox Then
SendMessage ActiveControl.hWnd, EM_UNDO, 0&, 0&
End If
If a Text Box is not active, no action is taken.
An undo feature for the Text Box controls in your program can be an attractive user-friendly feature that is very little work to implement.
A Visual Basic form's system menu normally displays a set
of default commands for window-related commands, such as closing, minimizing, or
maximizing the window. A program can add its own commands to this menu, as this
tip explains. This is particularly useful because this menu can be displayed not
only by clicking the icon at the left end of the title bar, but also by
right-clicking the window's icon on the Windows task bar. This permits the user
to send commands to the program while it remains minimized, a feature that can
be useful in a variety of situations. As you might expect, you use Windows API
functions to work with a form's system menu.
You start by getting a handle to the form's system menu using the GetSystemMenu
API function. Once you have this handle you then use the AppendMenu API function
to add the command to the end of the menu. Arguments to AppendMenu specify the
menu item text and also assign a numeric ID that is used to identify when the
command is selected by the user.
Once you have created the new menu command you must write the code that enables
the program to respond to it. To do so, write a function with the proper
arguments (as you'll see in the sample code). This function must be in a code
module and not in a form. Code in this function will examine the data passed in
the arguments to determine if your custom menu item was selected. If so, the
function will take whatever actions are required. If not - that is, if one of
the default system menu commands was selected - the command will be passed along
for processing by the usual internal Windows mechanisms by calling the
CallWindowProc API function.
The final step is to tell Windows to call your program's function when a system
menu command is selected. This is accomplished by using the AddressOf operator
to pass your function's address to the SetWindowLong API function.
You can try this out for yourself by creating a standard EXE project, adding a
code module, and copying the code shown below into the code module. Then put the
following line of code in the form's Load event procedure:
AddToSystemMenu Me.hWnd
The Windows API provides even more flexibility for customizing the system menu.
You can, for example, add separator lines, enable and disable menu commands,
insert a new command at any location in the menu and display checkmarks next to
commands. These functions are beyond the scope of this tip, but you can find
details in most Windows API reference books.
Private OriginalWindowProc As Long
Public Const MF_STRING = &H0&
Public Const MF_ENABLED = &H0&
Public Const IDM_MYMENUITEM = 2003
Public Const WM_SYSCOMMAND = &H112
Public Const GWL_WNDPROC = (-4)
Public Declare Function GetSystemMenu Lib "user32" _
(ByVal hWnd As Long, ByVal bRevert As Long) As Long
Public Declare Function AppendMenu Lib "user32" _
Alias "AppendMenuA" (ByVal hMenu As Long, _
ByVal wflags As Long, ByVal wIDNewItem As Long, _
ByVal lpNewItem As String) As Long
Public Declare Function SetWindowLong Lib "user32" _
Alias "SetWindowLongA" (ByVal hWnd As Long, _
ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Public Declare Function CallWindowProc Lib "user32" _
Alias "CallWindowProcA" (ByVal lpPrevWndFunc As Long, _
ByVal hWnd As Long, ByVal msg As Long, ByVal wParam As Long, _
ByVal lParam As Long) As Long
Public Sub AddToSystemMenu(ByVal hWnd As Long)
Dim hSystemMenu As Long
' Get the system menu's handle.
hSystemMenu = GetSystemMenu(hWnd, False)
' Append a custom command to the menu.
AppendMenu hSystemMenu, MF_STRING + MF_ENABLED, _
IDM_MYMENUITEM, "My Menu Item"
' Tell Windows to call MyMenuProc when a system
' menu command is selected.
OriginalWindowProc = SetWindowLong(hWnd, GWL_WNDPROC, _
AddressOf MyMenuProc)
End Sub
Public Function MyMenuProc(ByVal hWnd As Long, ByVal msg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
' If the custom menu item was selected display a message.
If msg = WM_SYSCOMMAND And wParam = IDM_MYMENUITEM Then
MsgBox "New menu item clicked!"
Exit Function
End If
' Otherwise pass the command on for normal processing.
MyMenuProc = CallWindowProc(OriginalWindowProc, hWnd, msg, _
wParam, lParam)
End Function
You may sometimes need to distribute two versions of the
same program. One version is full-featured while the other, perhaps a demo or
shareware version, is limited in some way. When a user registers or pays the
shareware fee, you want them to be able to unlock the demo version to access the
program's full functionality. This tip shows you how.
A program usually keeps information about whether it is locked or unlocked in
the Windows registry. On program startup, code checks the registry and sets a
global program variable to indicate the program status, locked or unlocked.
Throughout the program, code checks this global variable as needed to enable or
disable specific program features such as printing or "nag" screens. For
example, put the following code in a program's code module and then call the
CheckLockedStatus procedure from the main form's Load event procedure:
Public Unlocked As Boolean
Public Const APP_NAME = "MyApp"
Public Const SECTION_NAME = "Locking"
Public Const KEY_NAME = "Locked status"
Public Sub CheckLockedStatus()
Dim temp As String
temp = GetSetting(APP_NAME, SECTION_NAME, KEY_NAME, "")
If temp = "unlocked" Then
Unlocked = True
Else
Unlocked = False
End If
The registry section and key names used in this example are pretty obvious. If
you want to prevent registry-savvy users from unlocking the program by editing
the registry you should use non-obvious names for these items.
How would the user unlock the program? Most often, the user is given an
unlocking key that is based on their user name. The program has an "unlock"
command that prompts the user for this information. If the user name and the key
match, the program is unlocked by writing the required information to the
registry. This requires a method to generate a key from a user name. There are
many ways you could approach this. My approach is to simply add up the ASCII
values of all the individual characters in the user name. Because this might
result in a key number that is too short, I multiply the generated value by a
constant to create a larger number. The following function shows how to generate
a key number from a user name using this technique.
Public Function GenerateKeyNumber(UserName As
String) As Long
Dim i As Integer
Dim s As String * 1
Dim key As Long
key = 0
For i = 1 To Len(UserName)
s = Mid(UserName, i, 1)
key = key + Asc(s)
Next i
GenerateKeyNumber = Int(key * 12345.67)
End Function
When the user wants to unlock the program, he or she enters the user name and
key that you provided to them. The program then checks to see if the key if
valid for the user name. If so, the program will enter information in the
registry that marks the program as unlocked. Otherwise, a message will be
displayed to the user. The following code provides an example. It assumes that
the program has prompted the user for his or her user name and key and that this
information is passed in the procedure's arguments.
Public Sub UnlockProgram(Username As String,
key As Long)
Dim CorrectKey As Long
CorrectKey = GenerateKeyNumber(Username)
If key = CorrectKey Then
SaveSetting APP_NAME, SECTION_NAME, KEY_NAME, "unlocked"
MsgBox "Program successfully unlocked"
Unlocked = True
Else
MsgBox "Invalid user name or key"
End If
End Sub
In the typical Visual Basic development process, a program is created and tested within
the Integrated Development Environment, or IDE. Only when it is nearing
completion will the programmer compile and run an executable. It can be useful
for a program to know whether it is running in the IDE or as a compiled program.
For example, you may want to provide certain testing and debugging commands when
running in the IDE, but hide these commands in a executable. This tip shows you
how.
The technique relies on the Debug.Print statement. In the IDE, this statement
displays its output in the Immediate window. In an executable, however, all
Debug.Print statements are removed. If you include a Debug.Print statement that
will cause a run-time error - by dividing by 0, for example - the error will
occur only when the program is running in the IDE. Here's a function that
returns True or False depending on the situation:
Public Function RunningInIDE() As Boolean
On Error Resume Next
Debug.Print 1/0
If Err.Number = 0 Then
RunningInIDE = False
Else
RunningInIDE = True
End If
End Function
Your program can use this function to set a global flag and then hide or display
menus and other program features as needed.
Visual Basic's Kill statement deletes files from disk. You
can also use the methods that are associated with the FileSystemObject class in
the Windows Scripting Runtime. Both of these methods have the shortcoming that
they delete a file permanently - in other words, they do not delete the file to
the Windows recycle bin from where it can be restored if needed. This tip shows
you how to delete files to the recycle bin.
To delete a file to the recycle bin, use the SHFileOperation API function. This
function takes as its one argument a user-defined type (UDT) that contains
information, such as the file name, about the operation to perform. Note that
this function has many other uses besides deleting files, but these are beyond
the scope of this tip. You can see the definition of the UDT in the sample code
that follows. To delete a file, there are three pieces of information that are
required in the UDT: the name of the file, a flag specifying a delete operation,
and another flag specifying that the delete is to be undoable.
The code that follows shows how this is done. Place this code in your project,
in a code module, and then call the DeleteFileToRecycleBin procedure, passing
the name (including path) of the file to be deleted. If there is a problem, such
as the file not being found, insufficient permissions, or the file being
read-only, an error message will be displayed. Otherwise the file will be
deleted. Depending on the system settings, a confirmation dialog box may or may
not be displayed. By using this technique, you have the added safety of knowing
that any files deleted by your program can be recovered if needed.
Private Type SHFILEOPTSTRUCT
hWnd As Long
wFunc As Long
pFrom As String
pTo As String
fFlags As Integer
fAnyOperationsAborted As Long
hNameMappings As Long
lpszProgressTitle As Long
End Type
Private Declare Function SHFileOperation Lib "Shell32.dll" _
Alias "SHFileOperationA" (lpFileOp As SHFILEOPTSTRUCT) As Long
Private Const FO_DELETE = &H3
Private Const FOF_ALLOWUNDO = &H40
Public Sub DeleteFileToRecycleBin(Filename As String)
Dim fop As SHFILEOPTSTRUCT
With fop
.wFunc = FO_DELETE
.pFrom = Filename
.fFlags = FOF_ALLOWUNDO
End With
SHFileOperation fop
End Sub
When a Visual Basic program has two or more forms displayed
at the same time, the default behavior is for the active form - the one with the
focus - to display on top of all other forms. There are situations when you
might want to keep a form on top even when it does not have the focus. For
example, a form containing instructions could remain on top and visible even
when the user is working in another of the program's forms. Or, a graphics
program can display a toolbox that remains on top at all times. It's easy to do
this in Visual Basic, and this tip shows you how.
The technique relies on the ability to establish a parent-child relationship
between two forms. When one form is a child of another, it always remains on top
even when the parent has the focus. To make one form the child of another,
display the child by using the following code in the parent form (assuming the
child form is named InformationForm):
Dim frmChild As New InformationForm
frmChild.Show vbModeless, Me
The second argument to the Show method is the keyword Me, which is a reference
to the form the code is running in. Usually this argument is omitted from calls
to the Show method and the resulting form is parentless, which is appropriate
when you want the form to behave normally (not stay on top). The above syntax,
however, makes the new form a child of the current form, exactly what we need.
Note the use of the vbModeless argument. This displays the child form as a
modeless form, permitting the user to move the focus to the parent form without
first closing the child form - something that is clearly required in this
situation.
Most Visual Basic programmers know that you can use an object's (a
control or form) MousePointer property to change the appearance of the mouse
pointer when it is over that object. As useful as this can be, it limits you to
selecting from Visual Basic's predefined pointers. It's true that this set of
predefined pointers includes those that are needed most often, such as the
hourglass, crosshairs, and I-beam. But what if you need a mouse pointer that is
not provided in this set? This tip shows you how to use any icon as your mouse
pointer.
Icons are small images that are saved in ICO files. They are typically 32x32
pixels in size. You can create your own icons using one of the many shareware
icon editors that are available, or you can use any of the icons that are
provided as part of the Visual Basic installation (by default in the \Program
Files\Microsoft Visual Studio\Common\Graphics\Icons folder).
Once you know the icon you want to use, there are two steps required. The first
is to assign the icon to the object's MouseIcon property, using the LoadPicture
function to load the icon file. Then, set the object's Mouse Pointer property to
the value 99. For example, assume that your project contains a picture box
control named Picture1. Putting this code in the form's Load event procedure
will cause the specified icon to be displayed when the mouse pointer is over the
picture box. This code assumes that the icon file is located in the folder from
which the program is running.
Picture1.MouseIcon =
LoadPicture("arw03dn.ico")
Picture1.MousePointer = 99
You should be aware that each icon has a "hotspot" associated with it. This spot
identifies the precise spot that is the mouse pointer's location in terms of
clicking and other operations. For example, the hotspot of the standard arrow
cursor is at the tip of the arrow. If you create your own cursors using an icon
editor you can define the location of the hotspot as well.
When a Visual Basic program ends, all of its forms should be unloaded and removed from
memory. Unfortunately Visual Basic does not take care of this automatically. Particularly
if your program contains a lot of forms, it is possible for one or more forms to
remain in memory even after the program terminates. Remember, calling a form's
Hide method or setting its Visible property to False does not unload it. Even if
you explicitly unload a form (using the Unload procedure) it can still take up
resources unless the form's reference is set to Nothing. This tip shows you how
to ensure that all of a program's forms are unloaded and their resources
released upon program termination.
This technique makes use of the fact that a Visual Basic application has a global Forms
collection whose elements represent all of the application's loaded forms. You
could loop through this collection, unloading all forms as shown in this code
snippet:
Dim f As Form
For Each f In Forms
Unload f
Set f = Nothing
Next f
There's a problem with this straightforward approach, however. If you execute
this code from a procedure it works fine, but if you call it from the main
form's Form_Unload event procedure it will try to unload the main form which is
already in the process of unloading itself (otherwise the Form_Unload event
procedure would not have fired). You can get around this potential problem as
follows:
Public Sub UnloadAllForms(Optional
FormToIgnore As String = "")
Dim f As Form
For Each f In Forms
If f.Name <> FormToIgnore Then
Unload f
Set f = Nothing
End If
Next f
End Sub
With this procedure in place you would call it like this from the main form's
Form_Unload event procedure and it will unload all forms except the main form:
UnloadAllForms Me.Name
If calling it from a separate procedure, pass no argument and the procedure will
unload all of the program's forms.
Visual Basic's default is for a program to start each time with the form at the size and
position that were specified when the program designed. For some applications,
it can be a nice user-friendly touch to have the program "remember" its size and
position from the last time it was executed. In order to do this, the program
must store its size and position when it terminates and then retrieve them the
next time it starts up.
The Windows registry is the best place to store this information. The following
procedure, called from the form's Unload event procedure, will save the form's
position (its Left and Top properties) and its size (its Width and Height
properties) in the registry:
Public Sub SaveFormPosition(f As Form)
SaveSetting App.EXEName, "Form Position", "Left", Str(f.Left)
SaveSetting App.EXEName, "Form Position", "Top", Str(f.Top)
SaveSetting App.EXEName, "Form Position", "Width", Str(f.Width)
SaveSetting App.EXEName, "Form Position", "Height", Str(f.Height)
End Sub
Restoring the program's size and position when it starts up is a little more
involved. The first time a program runs, this information will not have been
placed in the registry. Your code must take this into account. Here's a
procedure that can be run from the form's Load event procedure to restore its
position. If the information is not found in the registry, the GetSetting
function will return a blank string. In this situation the procedure simply
exits and the form will be displayed at its default size and position.
Public Sub RestoreFormPosition(f As Form)
Dim buf As String
Dim l As Long, t As Long, w As Long, h As Long
buf = GetSetting(App.EXEName, "Form Position", "Left")
' If buf = "" then the settings were not saved perhaps because
' this is the first time the program was run. Exit the sub and
' let the form display at its default size and position.
If buf = "" Then Exit Sub
l = CLng(buf)
buf = GetSetting(App.EXEName, "Form Position", "Top")
t = CLng(buf)
buf = GetSetting(App.EXEName, "Form Position", "Width")
w = CLng(buf)
buf = GetSetting(App.EXEName, "Form Position", "Height")
h = CLng(buf)
f.Move l, t, w, h
End Sub
Your program would call these two procedures in the form's Load and Unload event
procedures as shown here:
Private Sub Form_Load()
RestoreFormPosition Me
End Sub
Private Sub Form_Unload(Cancel As Integer)
SaveFormPosition Me
End Sub
These procedures will work for only one of a program's forms. If you want to
save the size and position for two or more forms you need some way to
differentiate the saved settings for each form. You can do this by including the
form's name in the registry key as shown here for saving the Left setting:
SaveSetting App.EXEName, "Form Position",
f.Name & "Left", Str(f.Left)
And here for retrieving the Left setting:
buf = GetSetting(App.EXEName, f.Name & "Form
Position", "Left")
Many Visual Basic projects use data that is stored in external files. This can include
bitmap images, icons, cursors, and WAV files. Including these elements as
separate files in your installation has two drawbacks. First, the installation
can become cluttered with a large number of files, and if any one of them is
deleted or moved inadvertently by the user the program will not function
properly. Second, the material in the files is freely accessible to the user and
may be used in ways you did not want. You can avoid both of these problems by
using a resource file. Another advantage of resource files is that you can
create version-specific resource files to be included with installation in
different locales.
A Visual Basic project can have only one resource file associated with it. This file can
contain both binary and string items. Each item in the file has an identifier -
you use this identifier to retrieve a specific resource when the program needs
it. To create or modify a project's resource file use the Tools|Resource Editor
command from within the Visual Basic IDE (you may need to use the Add-in Manager to load
the Resource Editor first). Then, use the Resource Editor's commands to add
bitmaps and other items to the file. Each resource is assigned an identifier at
this time. You can also add an existing resource file to the project using the
Project|Add New Resource File command.
In your program you use three functions to load data from the resource file:
LoadResPicture for bitmap images, LoadResString for strings, and LoadResData for
various kinds of data. Let's look at some examples. This line of code loads an
icon resource with identifier 101 and assigns it as the icon for Form1:
Form1.Icon = LoadResPicture(101, vbResIcon)
Likewise, this code loads a bitmap image and displays it is a picture box
control:
Picture1.Picture = LoadResPicture(102, vbResBitmap)
The following code loads a string from the resource file and displays it in a
label control:
Label1.Caption = LoadResString(103)
The LoadResData function is used for arbitrary binary data such as WAV (sound)
and AVI (video) files. Your program must contain the code to make proper use of
such data. Use of the LoadResData function is beyond the scope of this tip but
there is more information in the Visual Basic documentation.
When your program makes use of the CD-ROM drive, it can be a nice touch to open
and close the door under program control. You can do so with the mciSendString
function from the Windows API. This function provides a general purpose
interface to Windows' multimedia capabilities. Its declaration is shown here:
Declare Function mciSendString Lib "winmm.dll"
Alias "mciSendStringA" _
(ByVal lpCommandString As String, ByVal lpReturnString As String, _
ByVal uReturnLength As Long, ByVal hwndCallback As Long) As Long
Because the CD-ROM is considered to be a multimedia device, you can use this API
function to control it. The first argument tells the device what you want to do.
For example, pass the string "set CDAudio door open" to open the door. For
example:
retval = mciSendString("set CDAudio door
open", "", 0, 0)
You can see that the second through fourth arguments are not used in this case
and are passed either a blank string or the value zero. Likewise the function's
return value can be ignored. Along with the function declaration shown above you
can put the following two procedures in a code module in your program to provide
control of the CD-ROM door.
Public Sub OpenCDDoor()
Dim retval As Long
retval = mciSendString("set CDAudio door open", "", 0, 0)
End Sub
Public Sub CloseCDDoor()
Dim retval As Long
retval = mciSendString("set CDAudio door closed", "", 0, 0)
End Sub
Note that calling OpenCDDoor when the door is already open, or CloseCDDoor when
it is closed, has no effect.
Microsoft's Excel spreadsheet is a popular program for storing all kinds of data. What if your Visual Basic programs need a piece of data that is stored in an Excel workbook? This is not too hard to do, thanks to the technique of automation, and this tip shows you how. You'll use a techniaue called automation, which permits one program to manipulate another program. In this case, your Visual Basic program will use automation to do the following:
1) Start Excel.
2) Open a workbook.
3) Retrieve data from a specific cell in that workbook.
4) Close Excel.
In order to automate Excel from within a Visual Basic program, you must add a reference to the Excel object library to your project. To do so, select Project|References from the Visual Basic menu to display the References dialog box. Scroll down to find the entry that says "Microsoft Excel 10.0 Object Library" and put a checkmark next to it, then close the dialog box. The version number may be different - "10.0" is for Excel XP - but the programming is the same for all versions of Excel from Office 97 through Office 2003.
With this reference in place, the first step is to start Excel with the following code:
Dim xlApp As New Excel.Application
After this code executes you will use the variable xlApp to refer to Excel. Note that Excel will not be visible on-screen when started this way - it just runs in the background. You also need two variables to refer to the workbook and worksheet. They are declared as shown here:
Dim wb As Excel.Workbook
Dim ws As Excel.Worksheet
Next, open the desired workbook and get a reference to the desired worksheet within that workbook. This code assumes the data is in "Sheet1" in the workbook "c:\data\testdata.xls":
Set wb = xlApp.Workbooks.Open("c:\data\testdata.xls")
Set ws = wb.Sheets("Sheet1")
Finally, get the data from the desired cell. The following code puts the data in a Text Box but your program can do anything it wants with the data:
Text1.Text = ws.Cells(2, 3).Value
The Cells method takes arguments that identify the cell by row and column. In this example it references the cell in row 2 and column 3 - in other words, cell C2. Finally, destroy your references to close Excel:
Set wb = Nothing
Set ws = Nothing
Set xlApp = Nothing
In a real-world program you will have to include error handling to deal with various possibilities such as the workbook not being found at the specified path. This is best done using an On Error Resume Next statement and testing the Err object after each automation statement that could cause an error. Here's an example. This Click event procedure, when the associated Command Button is clicked, gets the data from cell A1 of the specified workbook/worksheet and displays it in a Text Box.
Private Sub Command1_Click()
Dim xlApp As New Excel.Application
Dim wb As Excel.Workbook
Dim ws As Excel.Worksheet
On Error Resume Next
Set wb = xlApp.Workbooks.Open(App.Path & "\testdata.xls")
If Err > 0 Then
If Err = 1004 Then
MsgBox "File not found"
Else
MsgBox "Error " & Err.Number & " occurred."
End If
Exit Sub
End If
Set ws = wb.Sheets("Sheet1")
Text1.Text = ws.Cells(1, 1).Value
Set wb = Nothing
Set ws = Nothing
Set xlApp = Nothing
End Sub
There's a great deal more to automating Excel and other Office programs, and you can add a lot of power to your Visual Basic programs this way. You must be aware, however, that this technique requires that the person running your program have the required Office program installed on their system. You cannot legally distribute Office components with your Visual Basic program.
Rich Text Format, or RTF, is a widely accepted standard for storage and
transmission of formatted text. RTF consists of plain ASCII text with special
codes embedded to control formatting. Most word processors, including Microsoft
Word, support RTF. RTF is a very sophisticated standard and can control all of
the aspects of formatting that you normally work with, such as fonts,
indentation, boldface, underline, bulleted lists, and tabs. With the Rich
TextBox, or RTB, control you can add RTF capability to your Visual Basic
programs. To use this control in a program you must add a reference to it by
selecting Project|Components and then selecting Microsoft Rich Textbox Control
from the list.
When placed on the form, the RTB control looks the same as a regular TextBox
control - and in fact it behaves pretty much the same, too. Most of its special
features must be accessed programmatically by means of the control's properties
and methods. For example, the RTB control's SelBold property is used to boldface
text. Set this property to True and selected (highlighted) text in the control,
or text that is subsequently typed in, will be boldface. Set SelBold to false to
remove boldface from text. Similar properties exist to control italics,
underlining, and strikethrough.
An RTB control can display any font that is installed on the system. Of course a
Text Box control can do the same thing, but a TextBox control must use the same
font for all its text while an RTB control can use different fonts for different
sections of text. For example, you could use this code in a form's Load event
procedure to load a Combo Box with a list of all the available fonts:
Private Sub Form_Load()
Dim i As Integer
For i = 0 To Screen.FontCount - 1
Combo1.AddItem Screen.Fonts(i)
Next
End Sub
Then, when the user selects a font from the Combo Box this code assigns it to the RTB control:
Private Sub Combo1_Click()
RichTextBox1.SelFontName = Combo1.Text
End Sub
File operations are also handled by built-in methods. You use the LoadFile
and SaveFile methods to directly load and save data. Or, if you prefer, your
program can use the TextRTF property to access the control's data and then use
Visual Basic's usual file-related statements to perform disk operations. One very useful
feature is that the TextRTF property gets all of the control's text, including
any RTF codes, but the Text property returns the text minus any codes.
The Rich Text Box control can be used for unformatted text as well. When used in
this way the control can be though of as a replacement for the regular TextBox
control without its 64 k length limit on text. In fact the contents of a RTB
control are limited only by system memory.
There's quite a bit more to this powerful and flexible control, including
support for printing and for embedded OLE objects, that you can look into on
your own. When you need to add formatted text support to a Visual Basic application, the
RTB control may be just what you need.
Permitting the user to toggle a form's title bar on or off can be a great
program feature. Unfortunately the relevant form property, BorderStyle, is
read-only at run-time. THis means that you must turn to the Windows API for this
functionality. This tip shows you how.
Every Windows form has a set of style bits that controls various aspects of its
appearance and behavior. The "caption" bit controls display of the title bar,
and by setting or clearing that bit you can turn the title bar display on or
off. The strategy is as follows:
1) Retrieve the window's style bits.
2) Set or clear the caption bit as needed.
3) Write the modified style bits back to the window.
4) Redraw the window to display its new style.
You'll need to use three API functions for this task. Their declarations are as
follows (place these in a code module in your project):
Public Declare Function GetWindowLong Lib "user32" Alias _
"GetWindowLongA" (ByVal hwnd As Long, _
ByVal nIndex As Long) As Long
Public Declare Function SetWindowLong Lib "user32" Alias _
"SetWindowLongA" (ByVal hwnd As Long, _
ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Public Declare Function SetWindowPos Lib "user32" _
(ByVal hwnd As Long, ByVal hWndInsertAfter As Long, _
ByVal x As Long, ByVal y As Long, ByVal cx As Long, _
ByVal cy As Long, ByVal wFlags As Long) As Long
You will use GetWindowLong to retrieve the window's style bits, SetWindowLong to write the modified bits back to the window, and SetWindowPos to redraw the window. You'll also need the following constant declarations which should be placed in the same code module:
Private Const GWL_STYLE = (-16)
Private Const WS_CAPTION = &HC00000
Private Const SWP_FRAMECHANGED = &H20
Private Const SWP_NOMOVE = &H2
Private Const SWP_NOZORDER = &H4
Private Const SWP_NOSIZE = &H1
The final element is the function shown here. You pass it a reference to the window and a flag indicating whether to display or hide the title bar.
Public Sub ToggleTitleBar(f As Form, ShowTitle As Boolean)
Dim style As Long
' Get window's current style bits.
style = GetWindowLong(f.hwnd, GWL_STYLE)
' Set the style bit for the title on or off.
If ShowTitle Then
style = style Or WS_CAPTION
Else
style = style And Not WS_CAPTION
End If
' Send the new style to the window.
SetWindowLong f.hwnd, GWL_STYLE, style
' Repaint the window.
SetWindowPos f.hwnd, 0, 0, 0, 0, 0, SWP_FRAMECHANGED Or _
SWP_NOMOVE Or SWP_NOZORDER Or SWP_NOSIZE
End Sub
With this code in place, a program can set the title bar display for any of
its forms. For example, this code turns off a form's title bar when the Command
Button is clicked:
Private Sub Command1_Click()
ToggleTitleBar Me, False
End Sub
The Windows operating system has a set of system sounds that are associated with
certain system events such as a warning dialog box being displayed. By using
these sounds in your program you will have more variety than if you limited
yourself to the standard Visual Basic Beep command. This tip shows you how.
It's important to understand that the system sounds are not fixed. When Windows
is first installed, the default sound scheme assigns specific sounds to certain
system event. For example, the sound in the file Chord.wav is by default
assigned to the "Question" sound. However the user can change these assignments
using the Sounds applet in the Control Panel, assigning a different WAV file to
each system sound or even specifying that a sound be silent. This means that you
cannot be sure what sound, if any, will be played when you use the techniques in
this tip. This may sound like a disadvantage but it really is not because the
sounds that will play on each user's system are the specific sounds the user
chose himself.
To play Windows system sounds use the API function MessageBeep. Its declaration
is as follows:
Declare Function MessageBeep Lib "user32" (ByVal wType As Long) As Long
The function's one argument identifies the sound to play. The sound is usually identified using the constants shown here:
Public Const MB_ICONASTERISK = &H40&
Public Const MB_ICONEXCLAMATION = &H30&
Public Const MB_ICONQUESTION = &H20&
Public Const MB_ICONHAND = &H10&
The first three of these sounds are the ones identified as Asterisk, Exclamation, and Question in the Control Panel. The fourth one, MB_ICONHAND, corresponds to the Critical Stop sound in the Control Panel. You can also play the standard Beep sound by passing the value &HFFFFFFFF. Here's an example of using this function in a program, playing the Question sound when asking the user to confirm a file delete operation.
MessageBeep (MB_ICONQUESTION)
reply = MsgBox("Delete file - are you sure?", vbYesNoCancel, "Delete file?")
Most programmers compile their Visual Basic programs using the default advanced compiler settings. If you know about these settings you may be able to change them to improve performance of your compiled application. Visual Basic's compiler settings are found on the Compiler tab of the Project Properties dialog box, which you display by selecting ProjectName Properties from the Project menu. Click on the Advanced Optimizations button to set these compiler options, described below.
Assume no Aliasing. An alias is when a program refers to the same variable – that is, the same memory location – by two different names. It can occur when you use global variables and pass procedure arguments ByRef.. Here's an example:
Dim var1 As Long
MySub var1
Sub MySub(var2 As Long)
var2 = 1 ' Refers to the memory location of var1
' because var1 was
passed to the
' procedure ByRef
(the default).
var1 = 2 ' Refers directly to the memory
' location of var1.
End Sub
If your program does not use aliases, select the Assume No Aliasing option to permit the compiler to apply optimizations that it could not use if aliases were present, speeding program execution.
Remove Array Bounds Checks. When your code accesses an array element, Visual Basic normally checks to make sure the array element exists and, if not, triggers a run-time error. For example:
Dim MyArray(100) As Integer
MyArray(200) = 25
If you are confident this error will not happen - for example if your code uses the UBound and LBound functions to ensure that only legal array elements are accessed – you can select this compile option for faster, smaller compiled code. Be careful – if this option is selected and your code does write to a non-existent array element, it can result in erratic program behavior or a crash.
Remove Integer Overflow Checks. By default, Visual Basic checks the results of every calculation involving integer data types (Byte, Integer, and Long) to ensure that the result does not exceed the range of the data type. Selecting this option disables these checks and speeds integer calculations. Be aware, however, that if an overflow does occur the calculation will produce incorrect results without a warning or run-time error.
Remove Floating Point Error Checks. Floating point error checks do the same thing for calculations involving the floating point data types (Single and Double). Select this option to disable these checks with the same benefits and potential problems as described above for integer overflow checks.
Allow Unrounded Floating Point Operations. When you select this option, Visual Basic can perform certain operations with floating point data more efficiently. The downside is that when you compare floating point values, they may be considered not equal when in fact you expect them to be equal.
Remove Safe Pentium FDIV Checks. Some older Pentium CPUs have a bug that produces slightly inaccurate results when performing floating point divisions. Visual Basic's default is to check for and correct this inaccuracy. By selecting this option you disable the check and your code will perform floating point divisions faster. These old CPUs are pretty scarce these days, so this option is usually a safe one to select.
The advanced compiler options described in this tip should be applied
carefully and only if you understand the potential effects on your program.
The ComboBox control is very useful in a variety of situations, but many
programmers don't make the most of it because they do not understand how its
Style property works. This tip explains the details.
By default a ComboBox control's Style property is set to vbComboDropDown (value
= 0). This is the default style because it results in a ComboBox that works the
way programmers usually want it to work. The control displays like a single-line
text box with a down arrow at the right side. The user can type directly into
the control or, by clicking the arrow, can select from the predefined items that
have been loaded into the control. This style saves screen real estate because
the list of items is hidden until the user clicks the arrow.
Setting the Style property to vbComboSimple (value = 1) results in a control
that looks like a text box above a list box. As with the default style, the user
can type in the text box or select from the list. The difference is that the
list is always displayed with the advantage that the available choices are
always visible to the user. The disadvantage is, of course, that the control
takes up more room on the screen when this style is used.
The third and final ComboBox style is vbComboDrop-DownList (value = 2). When
this style is selected, the ComboBox displays the same way as with the default
style, a single-line text box with a down arrow at the right side. The
difference is in the behavior – the user cannot type data into the control but
must select from the list. In other words, a ComboBox with Style set to
vbComboDrop-DownList looks like a ComboBox but acts like a ListBox.
With an understanding of how a ComboBox control's Style property affects its
behavior and appearance, you can select the setting that is best for your
application needs.
While Visual Basic is not a full-fledged object oriented language, it does provide some OO features that can be very useful. Most important, it lets you define classes for use in your programs. Each class in defined in a Visual Basic class module, and can contain properties, methods, and event handlers. While it is possible to write a class's code by hand, you'll save a lot of time a frustration if you use the Class Builder utility – particularly for large, complex classes. This utility can be used to work with collections as well.
The Class Builder utility is an add-in and is accessed from Visual Basic's Add-Ins menu. If it is not listed there, you will have to use the Add-In Manager to add it to the menu before you can access it. The utility displays any existing classes and collections in your project as well as their hierarchical arrangement. To give you an idea of how handy this utility is, let's take a look at the process of creating a new class:
The new class is empty, of course (except for any members it inherited from the base class if one was specified in step 3 above). Adding members to the class is just as easy. Here's how to add a property:
When you are finished, the code for the new property is added to the class. For a public property, for example, this consists of:
Adding methods is equally easy – you specify the method name, argument names and types, and optionally return value of the method and the utility inserts the skeleton of the method code in the class. You must write the code within the method, of course, but the dull scut-work is taken care of for you.
There's a lot more to the Class Builder utility that you can explore on your
own. Even if you use classes infrequently in your Visual Basic projects, this tool can
save you time and hassles.
Conditional compilation is a Visual Basic feature that lets you specify that some source code be included and other source code be ignored when the program is compiled. It is useful for creating different distribution versions of your program from the same source code files –for example, a limited "demo" version and a full-featured version for a shareware program, or different program versions for different languages. I have also found it useful for defining debug and distribution versions of a program. Visual Basic provides two compilation directives for this purpose: #Const and #If...#End If.
#Const is used to define conditional compilation constants as follows:
#Const ConstantName = 0 ' to set the constant to False
#Const ConstantName = 1 ' to set the constant to True
A conditional compilation constant define in this way is valid throughout its module. To define a constant that is valid throughout the project, enter them in the Conditional Compilation Constants box on the Make tab of the Project Properties dialog box.
You use the #If...#End If directive to block out code that will be compiled or not depending on the value of one or more conditional compilation constants. The syntax is very similar to the regular If...End If statement:
#If constant1 Then
' Statements to be compiled if constant1 is True
#ElseIf constant2
' Statements to be compiled if constant2 is True
#Else
' Statements to be compiled if no previous constant is True.
#End If
The #ElseIf and #Else sections are optional. Note that you cannot use #If...#End If with regular Visual Basic constants defined with the Const keyword.
Here's an example of using conditional compilation. Suppose you have written a shareware program and want printing to be functional only in the registered version, not the free trial version. You could put this code in the project:
#If RegisteredVersion Then
' Code to print goes here.
#Else
MsgBox "Printing is supported only in the registered version."
#End If
When compiling the free trial version, define RegisteredVersion as 0 (False),
and when compiling the registered version define it as 1 (True).
The SysInfo control is designed to respond to changes in the operating system that occur while a program is running. These changes trigger events in the control, and your Program can respond as needed. To use the SysInfo control in a project you must first select it in the Components dialog box so it will be available in the Toolbox.
What changes does the SysInfo control detect? It's a fairly long list; here are some of the most commonly used events:
One way I use this control is to write code to respond to the DisplayChanged event. If the user changes the screen resolution while the program is running, the program can adjust the size of its forms to accommodate the new screen settings.
The SysInfo control also has a set of properties that let your program obtain certain operating system information. Some of them are relevant to only when a program is running on a laptop. For example, the ACStatus property can be used to determine whether the laptop is running on AC power, and the BatteryLifeTime property returns the time remaining on the battery.
Other SysInfo control properties provide information about the Windows desktop. Specifically, there are four properties that provide information about the work area – that part of the desktop not occupied by the Windows task bar. These properties are WorkAreaHeight, WorkAreaWidth, WorkAreaTop, WorkAreaLeft. The values returned by these properties are in twips. For example, when the task bar is in its default position at the bottom of the screen, WorkAreaHeight returns the height of the entire screen minus the height of the taskbar. Likewise, when the taskbar has been moved to the left edge of the screen, WorkAreaWidth returns the width of the entire screen minus the width of the taskbar.
Here's an example of using these properties. This Form_Load procedure causes the form to fill the entire work area when the form is displayed:
Private Sub Form_Load()
With SysInfo1
Me.Move .WorkAreaLeft, .WorkAreaTop, .WorkAreaWidth, .WorkAreaHeight
End With
End Sub
Since the task bar can be hidden, moved to different edges of the screen, and
resized, you may want to use the the SysInfo properties to ensure that your
program makes the best use of the work area.
Windows provides excellent support for using drag-and-drop to move or copy information from one location to another. For example, you can use this technique in Windows Explorer to move or copy files from one location to another. This tool is available in Visual Basic as well. This tip shows you how you can set up a Visual Basic program to accept file names dragged from Windows Explorer.
Data that is dragged and dropped from another program is registered in Visual Basic in the OLEDragDrop event procedure. This means that any control that has this event procedure can be the recipient of file names dragged from Explorer. This includes forms as well as the TextBox and most other Visual Basic controls. When the user drops the data on the control this event procedure is passed a DataObject that contains the data. Specifically, it includes a Files collection that contains the names of the file or files being dropped. Note that drag-and-drop can be used to transfer other kinds of data to a Visual Basic program, but this tip will be limited to file names.
To permit the dropping of file names in a Visual Basic program, set the OLEDropMode property of the recipient object (a form or control) to 1 – Manual. Then, put code in the object's OLEDragDrop event procedure to retrieve the file names. Here's an OLEDragDrop event procedure for a Text Box control that displays the names of the dropped files. In addition to setting the OLEDropMode property of the Text Box to 1 – Manual you must also set its MultiLine property to True.
Private Sub Text1_OLEDragDrop(Data As DataObject, Effect As
Long, _
Button As Integer, Shift As Integer, X As Single, Y As Single)
Dim i As Integer
For i = 1 To Data.Files.Count
Text1.Text = Text1.Text & Data.Files(i) & vbCrLf
Next
End Sub
To try this program, open Windows Explorer and drag one or more file names to
the Text Box. You'll see that the transferred data includes the full path of the
file. What your program does with this information is, of course, up to you.
A Text Box control normally has essentially no margins – text will run right up to the right and left edges. This tip shows you how to change the margins of a Text Box control. This is relevant only when the control's MultiLine property is True.
To change the margins to send a message to the control using the API SendMessage function. The declaration is as follows (put this in a code module in your project):
Public Declare Function SendMessage Lib "user32" Alias
"SendMessageA" _
(ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal _
lParam As Long) As Long
You'll also need the following constant values:
Public Const EM_SETMARGINS = &HD3
Public Const EC_LEFTMARGIN = &H1
Public Const EC_RIGHTMARGIN = &H2
Margin values are expressed in pixels. To change the margins of a TextBox, send it a message as shown here. The desired values of the left and right margins are assumed to be in the variables left_margin and right_margin, and the variable margins is a type Long.
margins = right_margin * &H10000 + left_margin
SendMessage Text1.hwnd, EM_SETMARGINS, _
EC_LEFTMARGIN Or EC_RIGHTMARGIN, margins
In theory, the control is supposed to repaint itself to display the new margins after receiving this message (if the control already contains some text). In my experience, sometimes only the left margin goes into effect. To get the new right margin setting to "take" you must delete and then re-write the contents of the control after setting the margins:
s = Text1.Text
Text1.Text = ""
Text1.Text = s
Problems can arise in some situations because users enter text with different capitalization. This problem can be avoided by converting all text to uppercase as it is entered. This tip shows you how.
Letters and other characters are represented numerically in Visual Basic. The lowercase letters a-z are represented by the values 97-122 while the uppercase letters A-Z are coded as 65-90. Thus, each uppercase letter's value is 32 less that the corresponding lower case letter's value. To force input to uppercase all you need do is check each character that is input and, if its value is between 97 and 122, subtract 32 from it. You would do this in the Text Box control's KeyPress event procedure:
Private Sub Text1_KeyPress(KeyAscii As Integer)
If KeyAscii >= 97 And KeyAscii <= 122 Then
KeyAscii = KeyAscii - 32
End If
End Sub
Non-letter characters, such as digits and punctuation, are not affected. Note
that this technique works only for text entered from the keyboard – it will not
change text inserted into the Text Box by code.
By default a Visual Basic form displays a close button – an "X" – in the title bar that the user can click to close the window. A Close command is also available on the form's System menu. In some programs you may not want the user to be able to close the form in this way – you want to force them to use a program command to close the form to ensure that certain housekeeping tasks are performed first. Setting the form's ControlBox property to False does the trick but it also hides the minimize and maximize buttons, which you may not want. This tip shows you how to selectively disable the close button and remove the Close command from the system menu.
This technique relies on three API functions. You'll need to put their declarations in a code module in your project, along with a couple of constant declarations, as shown here:
Public Const SC_CLOSE = &HF060
Public Const MF_BYCOMMAND = &H0
Public Declare Function GetSystemMenu Lib "user32" _
(ByVal hwnd As Long, ByVal bRevert As Long) As Long
Public Declare Function DeleteMenu Lib "user32" _
(ByVal hMenu As Long, ByVal nPosition As Long, ByVal wFlags As Long) As Long
Public Declare Function DrawMenuBar Lib "user32" (ByVal hwnd As Long) As Long
Then, put the following code in the form's Load event procedure:
Dim hMenu As Long
hMenu = GetSystemMenu(Me.hwnd, 0&)
If hMenu Then
Call DeleteMenu(hMenu, SC_CLOSE, MF_BYCOMMAND)
DrawMenuBar (Me.hwnd)
End If
When you disable the close button on a form, be sure to give the user another way to close it, such as a Command Button that executes an Unload statement.
Visual Basic has plenty of tools for displaying regular text, but what if you want special effects? This tip shows you how to display 3-D text in your program.
The Windows API has a function TextOut that displays text on the screen. The declaration is as follows (place this in a module in your program):
Public Declare Function TextOut Lib "gdi32" Alias _
"TextOutA" (ByVal hdc As Long, ByVal x As Long, _
ByVal y As Long, ByVal lpString As String, ByVal _
nCount As Long) As Long
The arguments are:
- hdc is the device context of the destination (more on this soon).
- x and y are the coordinates of the location to display the text.
- lpString is the text to display.
- nCount is the length of the string.
A device context is the way Windows represents certain display objects. In
Visual Basic, only forms, Picture Box controls, and the printers have a device
context, so that means this technique is limited to displaying 3-D text on those
objects.
The technique used here to display 3-D text is to output the string repeatedly
at slightly different, overlapping locations. By varying the color of the text
with each iteration, a 3-D effect is obtained. Here's an example.
Private Sub Command1_Click()
Dim i As Integer
Dim s As String
With Form1
s = "Text to display"
For i = 0 To 127
.Font.Name = "Times New Roman"
.Font.Size = 36
.ForeColor = RGB(i * 2, i * 2, i * 2)
TextOut .hdc, i / 15, i / 15, s, Len(s)
Next
End With
End Sub
You can see that the text is output 128 times. Each time, the position is shifted slightly down and to the right, and the color is changed. In the entire process, the color changes from RGB(0, 0, 0), or black, to RGB(254, 254, 254), or essentially white. You could get a different but equally effective result by varying the color from white to black by changing a single line of code as follows:
.ForeColor = RGB(256 - i * 2, 256 - i * 2, 256 - i * 2)
This code varies the color from blue to red:
.ForeColor = RGB(i * 2, 0, 256 - i * 2)
There are lots of attractive effects available with this technique. I
recommend that you experiment until you get just what you want.
Many powerful Visual Basic programming techniques involve calling functions in the Windows API. This requires you to place the function declarations in your program. In some cases you'll need constant and type declarations as well. Typing them in manually is time consuming and prone to errors. It's much better to use the API Viewer utility that is installed with Visual Basic. To start this program select Microsoft Visual Basic 6.0 Tools and then API Text Viewer from the Start menu.
The API data is supplied as a text file. The first time you use the Viewer you can convert this to a database file for faster access. Here's how:
Once the database conversion has been perform you will load the database each time you start the API Viewer by selecting File|Load Database File from the menu.
To use the Viewer, first select the type of item you are looking for in the API Type list: Constants, Declares, or Types. The available items will be listed in the Available items list. You can scroll to find the item you need or, if you know the name, start typing it in the Type box to scroll automatically. Once you have located the item, select it by clicking.
The next step is to specify whether you want the item declared as Public or Private by selecting the appropriate option. Then click Add to add the item to the Selected Items list.
Repeat these steps until all the items you need have been added to the list. Finally, click the Copy button to copy the items to the clipboard. Now you can switch back to the Visual Basic IDE and use the Edit|Paste command to insert the declarations in your source code. No typing, no errors – a real time-saver!
When the user clicks a Combo Box, it drops down a list from which the user can select. The height of the dropped-down list is set as follows:
If there are 7 or fewer items in the list the height is adjusted to match the list.
If there are more than 7 items in the list the first 7 are displayed and the user must scroll to view the others.
This tip shows you how to change the maximum height of a Combo Box. This can be useful when, for example, your list contains 9 or 10 items and you do not want the user to have to scroll to see the last few. It requires a Windows API function whose declaration must be put in a project module:
Public Declare Function MoveWindow Lib "user32" (ByVal hwnd As
Long, _
ByVal x As Long, ByVal y As Long, ByVal nWidth As Long, _
ByVal nHeight As Long, ByVal bRepaint As Long) As Long
Note that the size and location values are in pixels so you must change the scale mode of the form as shown in the example. The code shown here demonstrates how to use this function.
Private Sub Form_Load()
Dim
OldScaleMode As Integer
Dim cbx_left As Integer
Dim cbx_top As Integer
Dim cbx_width As Integer
Dim cbx_height As Integer
Combo1.AddItem
("Item1")
Combo1.AddItem ("Item2")
Combo1.AddItem ("Item3")
Combo1.AddItem ("Item4")
Combo1.AddItem ("Item5")
Combo1.AddItem ("Item6")
Combo1.AddItem ("Item7")
Combo1.AddItem ("Item8")
Combo1.AddItem ("Item9")
Combo1.AddItem ("Item10")
Combo1.AddItem ("Item11")
Combo1.AddItem ("Item12")
' Save old
scale mode.
OldScaleMode = Me.ScaleMode
' Set scale
mode to pixels.
Me.ScaleMode = vbPixels
' Get the
combo box current location and width.
cbx_left = Combo1.left
cbx_top = Combo1.top
cbx_width = Combo1.Width
' Calculate
the height that would make the combo box
' drop down to near the bottom of the form.
cbx_height = Me.ScaleHeight - cbx_top
' Restore scale
mode.
Me.ScaleMode = OldScaleMode
' Change combo
box height.
MoveWindow Combo1.hwnd, cbx_left, cbx_top, _
cbx_width, cbx_height, 1
End Sub
Note that this technique sets the maximum height of the Combo Box, not its actual height. This means that if the height you specify is more than required to display the entire list of items, the size will be automatically adjusted to fit the list. The size also adjusts automatically if items are later added to the Combo Box while the program is running.
When a List Box control has the focus, it will automatically scroll to the first item that begins with the letter you type. It will not, however, automatically select items based on more than the first letter. For example, if you type "ap" you may want the control to select the first item that begins with "ap". You can implement this behavior using an API function as explained in this tip.
The function you need is SendMessage. It's declaration, which must be included in the program, is as follows:
Public Declare
Function SendMessage Lib "user32" _
Alias "SendMessageA" (ByVal hwnd As Long, _
ByVal wMsg As Long, ByVal wParam As Long, _
ByVal lParam As String) As Long
You'll also need the following constant which tells the List Box control to select the first item that begins with the specified prefix:
Public Const LB_SELECTSTRING = &H18C
In addition to the List Box, this technique requires a Text Box control. The user enters text in the Text Box and the List Box automatically selects the first matching item. The message is sent in the Text Box's Change event procedure:
Private Sub Text1_Change()
If Text1.Text
<> "" Then
SendMessage List1.hwnd, LB_SELECTSTRING, -1, Text1.Text
End If
End Sub
Letting the user select List Box items based on more than just the first letter can be particularly useful when the list contains many items.
A Visual Basic form has a KeyPreview property. Few programmers have ever heard of this property let alone knowing what it is for. It can be quite useful for some programming situations.
When KeyPreview is at its default setting of False, keyboard input goes to the control that has the focus. The form is unaware of keyboard events, and this is fine for most programs. When KeyPreview is True, however, the form's keyboard event handlers (KeyDown, KeyUp, and KeyPress) are fired before the keystroke is passed to the control with the focus. This lets you handle keyboard input at two levels, intercepting keystrokes that have a global meaning at the form level.
When would you want to do this? One good example is the function keys. You may want certain function keys to have program-wide uses, such as F1 for help, F2 to save data, F3 to open a new file, and so on. By setting KeyPreview to true you can catch these keys in the form's KeyDown event procedure:
Private Sub Form_KeyDown
(KeyCode As Integer, Shift As Integer)
Select Case KeyCode
Case vbKeyF1:
' Code to display help.
Case vbKeyF2:
' Code to save data.
Case vbKeyF3:
' Code to open a file.
End Select
End Sub
If you want to handle keyboard events only at the form level, not allowing controls to receive keyboard events, set KeyAscii to 0 in the form's KeyPress event and set KeyCode to 0 in the form's KeyDown event. You can block selected keys or all keys from reaching controls.
Remember that if a form has no controls on it, or if all of its controls are disabled, it will receive keyboard events regardless of the value of KeyPreview. You also need to be aware that some controls intercept specific keystrokes so that the form can't receive them. This includes the Enter key when focus is on a CommandButton control and the arrow keys when focus is on a ListBox control.
When the focus is moved to a Text Box that contains text, Visual Basic's default is to position the caret (editing cursor) at the start of the text. In some applications it might be more appropriate to have all of the control's text selected when it receives the focus. One advantage to this is that the existing text will be deleted if the user starts typing something new. This tip shows you how to implement this behavior.
A Text Box has two properties that determine what text, if any, is selected:
SelStart specifies the character position where the selection starts.
SelLength specifies the length of the selection, in characters.
You can select all text in a Text Box by setting SelStart to 0 and SelLength to the length of the text. Here's a short procedure that does this:
Sub
SelectAllText(tb As TextBox)
tb.SelStart = 0
tb.SelLength = Len(tb.Text)
End Sub
Then, for each Text Box that you want to behave this way, call the procedure from the GotFocus event procedure, passing the name of the Text Box as the argument:
Private Sub
Text1_GotFocus()
SelectAllText Text1
End Sub
This works when the Text Box gets the focus either by tabbing or by a mouse click.
Visual Basic's Setup and Deployment Wizard is a handy tool for creating installation packages that you can distribute. Unfortunately it has at least one bug. This applies only if you are using the Windows XP operating system for your Visual Basic development and someone running Windows 2000 is trying to install a setup package that you created. I can't say it happens all the time but it certainly happens enough to be a serious problem.
Here's what happens: during the setup process the user is told that some files need to be undated and the system rebooted. When the user clicks OK the system reboots – so far so good. But then they are presented with another dialog box with the same message - files need to be undated and the system rebooted. They are in and endless loop of reboots and the only way out of it is to click Cancel instead of OK. The end result is that the application setup is not complete.
Microsoft has been aware of this problem for some time and has claimed that it was fixed as part of service pack 4 for Visual Basic. It has not, however, been fixed reliably – in other words some developers still have this problem despite installing the service pack. There are only two sure fixes that I know of. One is to maintain a Windows 2000 system to use only for generating setup packages. The other is to use a third party program, such as Wise InstallMaster, to create your setup packages.
XML is becoming a widely accepted standard for platform-independent data storage and exchange. While it is a text-based standard it can be very complex and Visual Basic's built-in text handling tools are not up to the job of doing any serious XML processing. Fortunately, Microsoft has developed software tools that you can download and use in your Visual Basic programs. They are available in the Microsoft XML Parser that you can download by going to msdn.microsoft.com/downloads and searching for "MSXML parser." The latest version is 4.0. You'll actually download the Microsoft XML Software Development Kit which includes the parser, online documentation, an XML reference document, and a tutorial.
Once the parser has been installed it will be listed in the Visual Basic References list as MSXML 4.0. When you select it, the parser components will be available for use in your program. The one you'll probably use most often is the DOMDocument class. This class implements the Document Object Model which is an accepted standard for programmatically accessing, validating, and modifying XML data. An XML document that has been loaded into a DOMDocument object is represented as a tree of nodes corresponding to the elements, attributes, data, and other components of the XML file. Using the concept of parent, sibling, and child nodes, the model makes the entire contents of the XML file available for reading or modification. You have essentially complete control – your program con move forward and backward in the tree, read data, modify data, add new elements, and so on. Another essential task that this class can perform is to validate an XML file – in other words, to verify that the logical structure and data types in the file are in agreement with the associated Document Type Definition or Schema. Finally, the DOMDocument object can apply an XSLT transform to XML data.
Another XML tool that is provided in the parser is an implementation of the Simple API for XML, or SAX. The SAX does not have nearly as many capabilities as the Document Objet Model – you cannot, for example, apply XSLT transforms or modify the document structure. Because of its limited capabilities the SAX is very fast and is easy on your memory resources. It is the tool of choice when you need to quickly look through an XML file for specific data.
With XML becoming more and more widespread it is a good idea for the Visual Basic programmer to have some familiarity with the available tools. With the XML Software Development Kit, Microsoft has provided a powerful set of tools for you to use – and for free, which is hard to beat!
By default, every control on a Visual Basic form is an independent entity with its own name and its own event procedures. In some situations your programming work will be simplified by using a control array. This permits two or more controls of the same type to have the same name and same event procedures but retain their own separate properties. This tip shows you how.
To create a control array you must first put a single control of the desired type on the form. Then, with the control selected, press Ctrl+C followed by Ctrl+V. Visual Basic will display the following prompt:
You already have a control named XXXXX. Do you want to create a control array?
Click yes to create the array. You'll now have two identical controls – Command Buttons, Check Boxes, or whatever – on the form. Repeat the steps to add more controls to the array – there are no limits to the size of a control array other than those imposed by system resources, although you'll rarely find arrays of more than 8-12 controls to be useful.
When you select a control in an array the Properties window displays its properties. Except for the Name, these properties are completely independent from other controls in the array. If all the controls have the same name how are they told apart? The answer lies in the Index property. The first control in an array has Index = 0, the second has Index = 1, and so on. Controls that are not part of an array have a blank Index property.
As I have mentioned, all the controls in an array share the same event procedures. The procedure is passed an argument indicating the Index property of the control that received the event. This is where the usefullness of control arrays is most evident. You can write a single event procedure that handles the events for multiple controls. For example, you could create an array of three Command Button controls and then write the Click event procedure as follows:
Private Sub Command1_Click(Index As Integer)
Select Case Index
Case 0
' Code for first action here.
Case 1
' Code for second action here.
Case 2
' Code for third action here.
End Select
End Sub
Keeping code together like this rather than spreading it out over multiple event
procedures makes it easier to write and debug.
In most Visual Basic projects the number of controls on a form are set at design time and do not change when the program runs. But what if you have a situation where the number of controls needed is not known until runtime? For example, the program could read data from a database and need to display one control for each record that was found. This tip shows you how to add controls to a form at runtime. This technique is applicable to essentially any Visual Basic control.
To add controls at runtime there must already be at least one instance of the control on the form, and it must be part of a control array. To create a control array containing only a single control, add the control to the form and then set its Index property to 0. This control does not have to be visible.
When the program runs, you add additional controls with the Load statement:
Load ControlName(Index)
Each control that you add in this way initially has the same properties as the original control. You'll have to change at least the Top and Left properties to prevent all the added controls from displaying at the same position on-screen.
Here's an example. The variable rs refers to a recordset that contains information about rooms in a dormitory. The code goes through the recordset and for each room creates a Label control (lblRoom) and a ListBox control (lstRoom) and positions them in a grid on the form. ROOM_LIST_HEIGHT and ROOM_LIST_WIDTH are constants defined elsewhere that specify the control size.
col = 0
row = 0
Do While Not rs.EOF
recordnumber = rs.AbsolutePosition
Load lblRoom(recordnumber)
lblRoom(recordnumber).Caption = "Room " & rs.Fields("RoomNumber").Value
lblRoom(recordnumber).Top = row * (ROOM_LIST_HEIGHT * 1.3) + 300
lblRoom(recordnumber).Left = col * (ROOM_LIST_WIDTH * 1.2) + 500
lblRoom(recordnumber).Visible = True
Load lstRoom(recordnumber)
lstRoom(recordnumber).Left = lblRoom(recordnumber).Left
lstRoom(recordnumber).Top = lblRoom(recordnumber).Top + _
lblRoom(recordnumber).Height + 10
lstRoom(recordnumber).Visible = True
rs.MoveNext
col = col + 1
If col = 3 Then
col = 0
row = row + 1
End If
Loop
Adding controls to a form at runtime can give your program the flexibility needed to react to changing data and circumstances.
If you need to place more controls on a form than can be displayed at one time, one option is to create a tabbed interface using the TabStrip control. But sometimes the functions of the controls do not lend themseves to being divided among two or more tabs. IN this situation you can create a scrolling form that lets the user scroll controls into view.
Start by placing a Picture Box control on the form and naming it pbOuter. Then place a second Picture Box within the first one and name it pbInner. This is important – the second Picture Box must be a child of the first one. It can be useful to change the background color of one of the Picture Box controls so it is easy to tell them apart.
Now you can start placing the form's controls. They all must be placed within the second Picture Box, pbInner. You can increase the Picture Box size, both height and width, as needed to accommodate all the controls. The size of pbOuter does not matter as it will be adjusted in code when the program runs. The size of pbInner should be as tall as needed to fit all your controls, but no wider than the form. Add a vertical scroll bar on the form itself. Set the scroll bar to the desired width, but the height does not matter at this time. Finally, add a horizontal scroll bar on the form – set its height as desired but the width doesn’t matter.
The remaining action takes place in code. First, put code in the Form_Resize event procedure that positions the scroll bars against the right and bottom edges of the form and sizes pbOuter to fill the remainder of the form:
VScroll1.Move Me.ScaleWidth - VScroll1.Width, 0, VScroll1.Width,
Me.ScaleHeight
HScroll1.Move 0, Me.ScaleHeight - HScroll1.Height, Me.ScaleWidth - _
VScroll1.Width, HScroll1.Height
pbOuter.Move 0, 0, Me.ScaleWidth - VScroll1.Width, _
Me.ScaleHeight - HScroll1.Height
You also need code in Form_Resize to determine if pbOuter is large enough to display all of pbInner without scrolling. In this case you should hide one or both scroll bars. If it's not large enough you need to show the scroll bars and set the scrolling parameters for the scroll bars:
If pbInner.Height <= pbOuter.Height Then
VScroll1.Visible = False
Else
VScroll1.Visible = True
VScroll1.Max = pbInner.Height - pbOuter.Height
VScroll1.LargeChange = 2500
VScroll1.SmallChange = 250
End If
If pbInner.Width <= pbOuter.Width Then
HScroll1.Visible = False
Else
HScroll1.Visible = True
HScroll1.Max = pbInner.Width - pbOuter.Width
HScroll1.LargeChange = 2500
HScroll1.SmallChange = 250
End If
Finally, you must place code in the Change event procedures for the scroll bars to perform the scrolling:
Private Sub HScroll1_Change()
pbInner.Left = -HScroll1.Value
End Sub
Private Sub VScroll1_Change()
pbInner.Top = -VScroll1.Value
End Sub
Using this technique you can place as many controls as needed on a form and the user will be able to scroll them into view as needed.
Have you ever wondered when the last time a specific program was run? This information can be useful for security purposes in some situations where multiple people have access to a computer. If a user comes to work on Monday and sees that a program was last run on Saturday, when he was at the beach, he will know that someone else has been using the program. This tip shows you how to implement this.
The technique uses the Windows registry to store information. You should define constants for the application name and section, used for locating information in the registry:
Const APP_NAME = "MyAppName"
Const SECTION_NAME = "Usage"
Then place the following code where it will be executed each time the program is run. This would usually be the main form's Load event procedure, but you could also use the Unload event procedure. This code converts the current date and time to a string and stores it in the registry under the "Last used" key:
SaveSetting APP_NAME, SECTION_NAME, "Last used", CStr(Now)
Finally, you need code to read the registry value and display the information to the user. Here's an example of how you could do this:
Dim LastUse As String
LastUse = GetSetting(APP_NAME, SECTION_NAME, "Last used")
If LastUse = "" Then
MsgBox "This program has not been run before."
Else
MsgBox "This program was last run " & LastUse
End If
If you put this code in the Form_Load event procedure, be sure it comes
before the call to SaveSetting. With a little additional programming you could
also use the registry to keep track of when the program was started and stopped
and how much total time it was running.
Many Windows programs use a window with panes separated by a splitter. You can
drag the splitter to make one pane larger and the other smaller, all without
changing the parent window size. While Visual Basic does not provide a control
for this purpose, you can do it yourself as shown in this tip.
The technique makes use of the fact that you can use a Picture Box control as a
container for other controls. When designing the form, place two Picture Box
controls on the form, one for each pane. You can call them pbTop and pbBottom,
for example, for the upper and lower panes. Then place the other controls for
each pane on the corresponding Picture Box.
When the program runs, code arranges the two Picture Box controls - the two
panes - to fill the window at predefined sizes. The sizes are computed so that a
thin strip of the underlying form shows through between the Picture Boxes - this
will serve as the splitter. Set the form's MousePointer property to "7 - Size
NS" so that an up/down sizing cursor will display when the mouse is over the
splitter. Set each Picture Box control's MousePointer property to "1 - Arrow."
The program also needs constants for the splitter height and for the minimum
permitted height of each pane.
Then here's what happens. When the user starts dragging the splitter, the new
splitter position is determined from the position of the mouse cursor. This
occurs in the MouseMove event procedure. A procedure named ChangePaneSizes is
called; code in this procedure uses the new splitter position to change the
sizes of the two panes accordingly. Neither pane is permitted to be smaller than
the specified minimum.
Code to perform these actions is shown below. You can try this out by creating a
Visual Basic program with a form that contains two Picture Box controls named pbTop and
pbBottom, then paste this code into the form's code window. This example is for
two panes separated by a horizontal splitter, but could be adapted for other
pane arrangements as well.
Option Explicit
Const SPLITTER_HEIGHT = 40
Const MIN_PANE_HEIGHT = 400
' The percentage of the window height
' occupied by the top pane.
Dim TopPanePercent As Single
' True when the splitter is being dragged.
Private Dragging As Boolean
Private Sub ChangePaneSizes()
' Arrange the panes according to the new splitter position.
Dim TopHeight As Single
Dim BottomHeight As Single
' Do nothing if window is minimized.
If WindowState = vbMinimized Then Exit Sub
TopHeight = (ScaleHeight - SPLITTER_HEIGHT) * TopPanePercent
If TopHeight < MIN_PANE_HEIGHT Then TopHeight = MIN_PANE_HEIGHT
pbTop.Move 0, 0, ScaleWidth, TopHeight
BottomHeight = (ScaleHeight - SPLITTER_HEIGHT) - TopHeight
If BottomHeight >= MIN_PANE_HEIGHT Then
pbBottom.Move 0, TopHeight + SPLITTER_HEIGHT, ScaleWidth, BottomHeight
End If
End Sub
Private Sub Form_Load()
' Initially each pane gets half the window.
TopPanePercent = 0.5
End Sub
Private Sub Form_MouseDown(Button As Integer, Shift As Integer, X As Single, _
Y As Single)
' Start dragging the splitter.
Dragging = True
End Sub
Private Sub Form_MouseMove(Button As Integer, Shift As Integer, X As Single, _
Y As Single)
' As the splitter is dragged.
' Do nothing if we're not dragging.
If Not Dragging Then Exit Sub
TopPanePercent = Y / ScaleHeight
If TopPanePercent < 0 Then TopPanePercent = 0
If TopPanePercent > 1 Then TopPanePercent = 1
ChangePaneSizes
End Sub
Private Sub Form_MouseUp(Button As Integer, Shift As Integer, X As Single, _
Y As Single)
' End dragging the splitter.
Dragging = False
End Sub
Private Sub Form_Resize()
' Change pane sizes if the window is resized.
ChangePaneSizes
End Sub
A Visual Basic program can dislay text on a form or a Picture Box using the Print method. The location of the text is determined by the destination object's CurrentX and CurrentY properties. The text is always left-aligned at this position, starting at the location (CurrentX, CurrentY) and extending to the right. Visual Basic itself has no provision for changing this alignment setting, but this tip shows you how to do so with a Windows API call. You'll use the SetTextAlign API function whose declaration is:
Declare Sub SetTextAlign Lib "gdi32" (ByVal hDC As Long, ByVal wFlags As Long)
- hDC is the destination object's device context which you obtain from its
hDC property.
- wFlags is a value that specifies the desired alignment.
You specify the horizontal alignment using one of the following constants (You must define these constants in your program): TA_LEFT (value = 0), TA_RIGHT (value = 2), or TA_CENTER (value = 6). You can also specify the vertical alignment with these constants: TA_TOP (value = 0), TA_BOTTOM (value = 8), or TA_BASELINE (value = 24). Top alignment is the default - in other words the top of the text is aligned with the CurrentY position. The other two options are
- Bottom: The lowest any character descends such as the descender on a "y" or
"g".
- Baseline: The bottom of characters that don’t have descenders such as "a" and
"w".
To apply both a horizontal and a vertical alignment, simply use the Or operator
to combine the two flags. This code sets the alignment for Form1 to center,
baseline:
Call SetTextAlign(Form1.hDC, TA_CENTER Or TA_BASELINE
Note that changing alignment does not affect text that is already on the form
or Picture Box.
Computers are often shared among two or more users. The Windows operating system
provides for different user accounts so that each user can log on and then have
access to their files and settings. Perhaps you would like to keep track of
which users are running a Visual Basic program - you'll need to determine the
name of the current user and then store it in some manner. This tip shows you
how.
The API function GetUserName is designed specifically for this purpose. Its
declaration is:
Declare Function GetUserName Lib "advapi32.dll" Alias
"GetUserNameA" _
(ByVal lpBuffer As String, nSize As Long) As Long
The first argument is a string that will be used to return the user name, and
the second argument is a number giving the length of the string. The return
value is non-zero (True) on success and 0 (False) on failure. I have never seen
this function fail so I always omit checking the return value.
To use this function you must first create a string and fill it with spaces. The
number of spaces will determine the length of the string and therefore the
maximum length user name that can be returned. Since people never choose long
user names, I have found that a length of 40 is always more than adequate.
Here's the code.
Dim UserName As String
UserName = Space(40)
Then it's simply a matter of calling the GetUserName function like this:
GetUserName UserName, Len(UserName)
After this call the variable UserName will contain the name of the current
user. Your program can use this as needed, for example saving it to a log file
or the registry to keep track of who is using the program.
On the Windows operating system the mouse usually has two buttons, right and
left. You may want to swap the buttons so that pressing the left button triggers
a right button event, and vice versa. This might be useful for left-handed
users, for example. While the Windows control panel provides this option, you
can also do it from a Visual Basic program. This tip shows you how.
To swap the mouse buttons you will use the function whose declaration is shown
here:
Declare Function SwapMouseButton Lib "user32" (ByVal bSwap As Long) As Long
Set the argument bSwap to a non-zero value to swap the mouse buttons. Set it
to zero to return to the normal non-swapped state. The return value is True if
the mouse buttons were already swapped when the function was called, False if
not.
Because the mouse is a shared resource, the setting you make with this function
affects all applications, not just your Visual Basic application.
One of the reasons that Visual Basic 6 has become such a popular programming
tools is it's sophisticated support for database programming. Tools such as the
Visual Data Manager and the User Connection component greatly simplify tasks
that used to be very time consuming. Another powerful database tool that Visual
Basic
programmers have at their disposal is the Data Report Designer which les you
create complex reports from almost any data source.
The Data Report Designer creates what are known as banded hierarchical reports.
While the name may sound strange these are in fact the most common type of
database report and you have probably seen many of them, with headings,
subheadings, details, and summaries organized in a hierarchical manner.
A Data Report is similar to a Visual Basic form in that it has a visual designer and a
code module. Using the visual designer, you can divide the report into two or
more sections each with its own headings. Each section can contain controls to
display the report details. Design of the details sections is simplified by
drag-and-drop functionality. The available controls are distinct from Visual
Basic
controls but have similar functionality. In particular the Function control lets
you easily perform calculations on field data (sum, average, minimum, and
maximum) and display the results as the report is generated. Headers and footers
can be defined for the report as a whole and for each page of the report.
At run-time, output options are quite impressive. A print preview mode shows
what the report will look like when printed. Printing is a simple matter of
calling the PrintReport method. Export to a file is supported in both HTML and
text formats.
The Data Report Designer is a very impressive tool. It is not suitable for every
type of report, but when it fits your needs it can save you a tremendous amount
of time.
With some programs it may be desirable to let them run unattended. But what if
you want to give the user the option of changing program defaults or letting the
program run on its own using the defaults? One way to do this is to use what I
call "auto-OK" dialog boxes. As the program runs the dialog pops up to let the
user change options if desired. If the user does not respond within a certain
time, the dialog box closes itself and the program continues uaing the default
settings. This tip shows you how to create this kind of dialog box.
This technique is based on the Timer control. The approach is as follows:
1. Place a timer on the form with its Interval property set to 1000 (1 second).
2. In the form's Load event procedure, initialize a "Time Remaining" variable to the total number of seconds the dialog box should remain open. At the same time, set the Timer control's Enabled property to True so that it starts running.
3. In the Timer control's Timer event procedure, decrement the Time Remaining variable by 1. Optionally, you can display the time remaining on the dialog box.
4. When the Time Remaining variable has reached zero, call the Click event
procedure for the form's OK button or equivalent, closing the form and
continuing program execution.
Here's some sample code. At the form level, declare the following:
Const AUTOCLOSE = 5
Dim TimeLeft As Integer
Here's the form's Load event procedure:
Private Sub Form_Load()
TimeLeft = AUTOCLOSE
Timer1.Interval = 1000
Timer1.Enabled = True
End Sub
And finally, here's the Timer event procedure. In this example, the form's OK
button is named OKButton. The code in the OK button's event procedure, which is
not included here, will take whatever actions are required to close the dialog
box and continue program execution.
Private Sub Timer1_Timer()
TimeLeft = TimeLeft - 1
lblTimeRemaining.Caption = TimeLeft
If TimeLeft = 0 Then
Timer1.Enabled = False
Call OKButton_Click
End If
End Sub
An optional feature would be to use the Change event procedure of any controls
on the form to turn the timer off. This cancels the countdown if the user makes
any changes to the options that are presented on the form.
Many development tasks that are undertaken with Visual Basic can be accomplished within a single project. In other circumstances, however, you may need to work on more than one project at the same time because they are dependent on each other in some way. For example, you may be working on an ActiveX control project at the same time that you are working on a Standard EXE project that uses the control. In situations such as this you can have two or more projects open at the same time in what's called a group. When you save the group, information about all the contained projects is saved. You can later open the group and have all contained projects opened at the same time. Each project can also be opened independently regardless of whether it is part of a group.
To create a group, first open or create the first project in the group. Then
you can add an existing or new project to the group by select Add Project from
the File menu. Visual Basic will display the Add Project dialog box. Use the New tab to
add a new project or use the Existing tab to add an existing project. When you
have two or more projects open, each one is listed in the Project window as
usual, with forms and other project components arranged hierarchically under the
project name. You can remove a project from the group by right-clicking it in
the Project window and selecting Remove Project from the popup menu. Removing a
project from a group does not affect the project files on disk.
Visual Basic groups are saved as VBG files. To save a group select Save Project Group from
the File menu. Saving the group also saves the individual projects.
When you are working with a group, Visual Basic needs to know which project is the startup
project. To set this, right-click a project name in the Project window and
select Set as Startup from the popup menu. By default, the first EXE project
that was added to a group is the startup project. Of course, any group can have
only one startup project.
Most programmers know what properties are - the primary method by which objects
store data and make it available to the program. In Visual Basic, you add
properties to a class definition using Property Get and Property Let procedures.
Relatively few programmers know that you can also create global properties that
are not associated with any particular object but are available throughout the
entire application. This tip shows you how.
But why would you want global properties - why not just create a global
variable? The answer lies in the way properties work. A global variable is
accessed directly, like any other variable. In contrast, a property is accessed
indirectly via its Property Let and Property Get procedures. This lets you write
additional code that is executed whenever the property is set or read. This code
can be used for data validation or any other task that the program may require.
This is not possible if you are using a global variable.
You create a global property in a code module. There are three things required:
1) A Private variable to hold the property value.
2) A Property Let procedure for setting the property value.
3) A Property Get procedure for retrieving the property value.
The Get and Set procedures must have the same name. This is the property name
that you will use in the program. Here is sample code for a global property that
holds a string value:
Private name As String
Public Property Set gName(ByVal newvalue As String)
name = newvalue
End Property
Public Property Get gName() As String
gName = name
End Property
Here's how it works. Suppose that code elsewhere in the program sets the
property as follows:
gName = "Alice"
The Property Let procedure is automatically called with the new value - "Alice"
in this case - passed as its argument. The code in the Property Let procedure
assigns this value to the associated Private variable which is called
name in this example. When code reads the value
of the property, for example:
MsgBox(gName)
the Property get procedure is called and returns the value of the Private
variable name to the program (Clearly the Get procedure is really a function).
In this example there is no extra processing so the property behaves just like a
global variable. You could however include value checking, for example to store
a default name if the program tries to store a blank name. That would require a
change to the Property Set procedure as follows:
Public Property Set gName(ByVal newvalue As String)
If newvalue = "" Then
name = "Default name"
Else
name = newvalue
End If
End Property
The use of global properties provides the programmer with more control over
global data storage than is possible with global variables.
A DLL, or Dynamic Link Library, is a file that contains compiled code. In this sense it is like an executable program (EXE file), but there is one major difference: once a DLL has been installed and registered on a system, its code is available to any program that calls it. This can be an advantage when you are writing multiple programs that will usually be installed together. By placing shared code in a DLL you can decrease program size and simplify the task of upgrades, and also make the code easily reusable in other projects.
Another use for DLLs is in web programming. ASP (Active Server Page) technology lets you include code in your web pages to provide dynamic functionality and database access, but the ASP code is viewable by anyone who visits your site. If you want to hide your code so that others cannot "borrow" it for their own use, you can create a DLL and install it on the server. Your ASP page can then call the DLL as needed, so the full functionality is available without exposing your source code to others.
In Visual Basic, you create a DLL by selecting ActiveX DLL from the New Project dialog box. The new project will contain a single class module, which illustrates how Visual Basic DLLs work. A DLL that you create will contain one or more classes, which are essentially identical to the classes you can create in a standard Visual Basic EXE project. When a program calls a DLL, it will actually be making use of the DLL's classes and their methods and properties. An ActiveX DLL can contain forms and other components as well, but classes are at the heart of their functionality.
The Visual Basic online documentation contains a lot of background information as well as a tutorial on ActiveX DLLs. I recommend reviewing this material before deciding whether there's a place for DLLs in your Visual Basic projects.
A user defined type, or UDT, is a Visual Basic technique for defining a data type that exactly meets the needs of your program. A UDT can contain two or more individual data items that can be of different types, such as String and Integer. A UDT can even contain other UDTs and arrays, and you can create arrays of a UDT.
To define a UDT, use the Type...End Type statement, which must appear in the declarations section of a code module. Within the statement you define the individual items, or members, that the UDT will contain. Here's an example of a UDT you might create for keeping track of employee data:
Public Type Employee
FirstName As String
LastName As String
DateOfHire As Date
Salary As Currency
EmployeeNumber As Integer
End Type
Once the type is defined you can create instances of it just like any other data type:
Dim OneEmployee As Employee
Dim AllEmployees(500) As Employee
To access the members of a UDT you use the Name.Member notation. Here are some examples:
OneEmployee.FirstName = "Jack"
OneEmployee.LastName = "Sprat"
AllEmployees(1).FirstName = "Maria"
AllEmployees(1).LastName = "Sanchez"
Nesting UDTs can be useful in some situations. Here's an example:
Public Type Person
FirstName As String
LastName As String
End Type
Public Type Employee
Name As Person
DateOfHire As Date
Salary As Currency
EmployeeNumber As Integer
End Type
When UDTs are nested you use the same Name.Member notation but with an extra level:
Dim OneEmployee As Employee
OneEmployee.Name.FirstName = "Jack"
UDTs can also be used for arguments to functions and procedures and as the return type for functions. They can also be helpful when storing data on disk because Visual Basic's Random file type is specifically designed to work with UDTs. All in all, UDTs are a tool that every Visual Basic programmer should know about.
Procedures are an essential part of almost every Visual Basic program. When you define a
procedure, whether a Function or a Sub procedure, you need to decide whether the
procedure arguments are passed by reference or by value. What difference does it
make?
Visual Basic's default is to pass arguments by reference. You can include the ByRef
keyword in an argument list if desired, but because this is the default it has
no effect:
Sub Foo(ByRef Arg1 As Integer, ByRef Arg2 As String)
When an argument is passed by reference, the procedure is passed the address of the argument variable - in other words a reference to the variable:
Dim Total as Integer
Call MySub(Total)
In this example, MySub receives a reference to Total. The practical consequence of this is that code in MySub can change total. Here's an example. First the procedure:
Sub MySub(Total As Integer)
Total = 50
End Sub
Now the code that calls the procedure:
Dim Total As Integer
Total = 100
Call MySub(Total)
After this code executes, the variable Total equals 50 because the code in the procedure changed its value. To pass an argument by value, use the ByVal keyword:
Sub MySub(ByVal Total As Integer)
When you use ByVal, the procedure is passed a copy of the argument variable and not a reference to the argument variable itself. Code in the procedure cannot change the variable's value:
Sub MySub(ByVal Total As Integer)
Total = 50
End Sub
Now the code that calls the procedure:
Dim Total As Integer
Total = 100
Call MySub(Total)
After this code executes, Total is still equal to 100.
Note that array arguments and user-defined type arguments cannot be passed
ByVal. Also, using ByVal or ByRef does not have any effect when the argument is
a literal constant, only when it is a variable.
For most procedures, the default ByRef argument passing is fine. You can use
ByVal when you want to ensure that code in the procedure cannot change the
variable that was passed as an argument. You also must use ByVal when declaring
Windows API and other DLL functions for use in your programs.
Most programming languages, such as C and Fortran, require that all variables be
explicitly declared before being used. Visual Basic does not - you can simply
use a variable in code without declaring it. Some Visual Basic programmers tout this as an
advantage but in fact it is a serious shortcoming. Here's why.
If explicit variable declaration (with a Dim, Private, or Public statement) is
not required, Visual Basic creates a new variable in memory every time a new variable name
is encountered in code. If you misspell a name, Visual Basic cannot know that it's an
error - it just happily creates a new variable and initializes it, a process
called implicit variable declaration. This can cause serious and difficult to
find bugs in a program when, for example, a calculation uses a misspelled
variable that was initialized to zero rather than the correct variable that you
intended.
Another shortcoming is that such implicitly declared variables are always of
type Variant, and you are denied the program efficiencies that result from using
the proper data type for your variables.
Fortunately there is a way around this. If you include the Option Explicit
statement in a module, then Visual Basic will require explicit declarations for all
variables in that module. Any misspelled variables will be flagged as syntax
errors when you try to run the program, and you can make the needed corrections.
Place the Option Explicit statement at the start of each module, before any
procedures. You can make this automatic by selecting Tools|Options and, on the
Editor tab, selecting the Require Variable Declaration option.
The Visual Basic code editor has several handy features to make the programmer's
task easier. One is the ability to display different code elements in different
colors, making it easier for you read and interpret code. Many programmers do
not know that this feature can be customized so you do not have to stay with the
default code display scheme.
To change the settings, select Tools|Options and then click the Editor Format
tab. The Code Colors list displays the various code categories that you can
customize. They are as follows:
- Normal text: Variable names (if not changed under Identifier Text), operators,
literals.
- Selection Text: Text that you have selected.
- Syntax Error Text: Code in which Visual Basic has found a syntax error.
- Execution Point Text: When the program is paused during debugging, the next
line of code to be executed.
- Breakpoint Text: Lines of code where you have set a breakpoint.
- Comment Text: Comments in the code.
- Keywords: Visual Basic statements and data types such as Do, Dim, and Integer.
- Identifier Text: Variable names.
- Bookmark Text: Lines of code where you have defined a bookmark.
Below this list are three drop-down lists that you use to define the appearance
of the selected code component:
- Foreground: The color of the text.
- Background: The color behind the text.
- Indicator: The color of the indicator displayed in the left margin.
For each of these you can select a color. You can also select Auto which means
that Visual Basic does not change the specified display attribute from the default.
On the right side of this dialog box you can view a sample of how the selected
settings will appear. You can also turn the margin indicator bar on and off and
set the font and size. This latter setting affects all code.
Visual Basic provides a set of predefined symbolic constants that you can use
anywhere in your program. They are useful because the constant name, which
always starts with the letters "vb," is descriptive of what the constant is.
This tip takes a look at a few of the more useful ones.
The constant I use most often is vbCrLf, which is a combination of a carriage
return and a line feed. In other words, it starts a new line when you are
outputting text. For example, you can break Message Box text over two or more
lines when you have a long message to display:
MessageBox "This is a long message that should be broken over "
& vbCrLf & _
"two lines to prevent the message box from being too wide."
If you have need for a carriage return or a line feed alone, you can use vbCr
and vbLf.
The constant vbNullChar is the character with the value 0. It is equivalent to
Chr(0). It is useful in various situations such as calling external procedures
that use C calling conventions, in which strings must be terminated with a null
character.
The constant vbNullString represents a string with the value 0. This is not the
same as vbNullChar, and it is not the same as an empty string (""). It too is
used for calling external procedures such as those in the Windows API.
vbObjectError represents the highest error code used by Visual Basic. When you are
defining your own errors you should always use values greater than this, for
example:
Err.Raise vbObjectError + 500
Finally, two text-related constants are vbTab for the tab character and
vbBackspace for the backspace character.
When you create an application that will be distributed to multiple users,
it's a pretty sure bet that they will be calling you with problems. In order for
you to diagnose the problem, it can be very useful to have a record of what
exactly the program has been doing. The easiest way to do this is to maintain a
log file of program activity. The user can send this file to you and by
examining it you may well be able to locate the problem.
The first step is to add a procedure to your project that writes to the file. I
like to write the date and time as well as whatever information is relevant at
the time. Here's an example:
Public Sub WriteToLogFile(logentry As String)
Dim fn As Integer
fn = FreeFile
Open App.Path & "\ProgramNameLog.txt" For Append As #fn
Write #fn, Now & ": " & logentry
Close #fn
End Sub
When called and passed a message, this procedure opens the specified file in
the application directory, or creates it if it does not exist. It then writes
the current date and time and the information to the file and closes the file.
The next step is to call the procedure at the appropriate locations in your
code. Where this will be depends on your program, but some examples of
information that may be useful in a log file include:
- Names of procedures that are called.
- SQL statements passed to a database.
- Names of files opened or created.
- Values of intermediate values in calculations.
- Data input by the user.
I like to have the log file as an option that the user can turn on or off, for
example with a Checkbox in an Options dialog box. Set a global Boolean variable
according to the option setting and then write to the log file only when the
option is selected. For example:
If Logfile Then WriteToLogFile(sqlStatement)
It's a little more effort to program log file capability in a program, but it
may save a lot of time and headaches down the road.
One of the places where Visual Basic really shines is in the handling of
dates using the Date data type. You can display a date in any one of several
standard date formats, but internally it is represented as a serial number
specifying the number of days since December 31, 1899, with negative values used
for dates before then. The decimal portion of the value can be used to represent
a time of day, but that is beyond the scope of this tip.
The way dates are represented makes it really easy to compare dates, but some
Visual Basic programmers don't realize how easy it is and will go to unnecessary
complexity to perform a comparison. There's no need to extract the year, month,
and day individually for comparison--rather you can simply compare dates
directly using the standard comparison operators.
For example, the date June 30, 2004 is represented by the serial number 38168.
Comparing it with another date to see if it is earlier, the same, or later is
really just comparing the serial numbers to see if one is smaller, the same, or
larger than the other. Here's an example that executes one block of statements
if the date stored in the type Date variable MyDate is earlier than January 1,
2000 and another block if it is the same or later than that date.
If MyDate < #1/1/2000# Then
' Statements here are executed if MyDate is earlier.
Else
' Statements here are executed if MyDate is the same or later.
End If
This is just one example of the flexibility of Visual Basic's Date data type.
Using Objects as Properties
VB provides a limited degree of object-oriented programming. The objects you
define can have properties which are, in essence, data storage locations within
the object. Most properties are ordinary variables, and are usually defined
using the Property Let and Property Get procedures within the class definition.
Many programmers do not know that a property can also be an object reference. In
other words, one object can contain a reference to another object as a property.
This approach can be very useful. For instance, suppose you are writing an
application that creates financial charts. You might define classes for the
X-axis, the Y-axis, the title, and so on. Then you could define a "Chart" class
that encapsulates all of the individual parts such as the axes and titles.
Clearly the Chart object will need a way to reference these other objects.
Properties that are object references are treated a little differently from
other properties. This is because of the VB syntax requirement that object
reference assignments use the Set keyword. Thus, you cannot write simply
obj = New SomeClass
But you have to write like this:
Set obj = New SomeClass
Because of this requirement, the procedure you use in the class to assign a value to the property is not a Property Let procedure but rather a Property Set procedure. To read the property value you still use Property Get. The Property Set procedure looks like this:
Property Set PropertyName(o as Type)
Set PropertyVar = o
End Property
In this example, Type is either the specific type of the property or the
generic object type Object. PropertyVar is the internal class variable that is
used to hold the property value.
Having your program's objects reference other objects can be a useful technique,
and it's easy if you know how to use Property Set procedures.
Using the Friend Keyword
When writing property procedures and methods for a VB class there are three
keywords you can use to control access to the property or method. Two of these
are straightforward and easy to understand:
The third keyword is Friend, and its use and meaning are not well understood.
You use Friend when you want a property or method to be accessible to other
objects in the project but not to the project as a whole. Or, within a
component, objects can access each other's Friend members but the program that
is using the component cannot.
Suppose you are writing a component as a DLL or ActiveX, and there are several
objects in the component that need to communicate with each other. However you
do not want this communication channel to be available to the program that is
calling the component. This is perhaps the most common use of Friend properties
and methods.
Friend properties and methods are not part of a class's interface. They do not
appear in the type library of the class and they are not included when you
implement an interface using the Implements keyword.
Making Use of Polymorphism
The term polymorphism means "many forms" and in object-oriented programming it
refers to the ability to define more than one class with the same interface. An
interface is a class's methods and properties and there are many programming
tasks that can benefit from the use of polymorphism. It can save programming
time as well.
Suppose you are writing a program to handle vehicles and you know that you will
have a Car class and a Truck class. Both of these classes will need a Drive()
method and a Turn() method, so it's a good place to use polymorphism. You start
by defining a more general class called Vehicle:
1. Add a new Class Module to your project.
2. Change its name to Vehicle.
3. Add the following code to the class module:
Public Sub Drive(Speed As Integer)
End Sub
Public Sub Turn(Direction As Single)
End Sub
This defines the interface--two methods named Drive and Turn with their
specified arguments. The interface could include properties as well. They are
empty, and you'll see why in a minute. The next step is to create a class that
implements the Vehicle interface:
1. Add a new Class Module to your project.
2. Change its name to Car.
3. Add the following code to the class module:
Implements Vehicle
With the Car code window open, pull down the Object list at the top left of the
window. You'll see that Vehicle is listed there. When you select Vehicle, the
members of its interface--namely the methods Drive and Turn--become available in
the Procedure list at the top right of the code window. Select one and it is
entered in the Car class--still empty. Then, add the code to the method to
implement it as needed for this specific class. You would do the same for the
Truck class and for any other classes that need the same interface, such as
Motorcycle and Bicycle.
So what's the big deal? Each of these classes is likely to implement the methods
differently. Car for example will have a higher top speed than Bicycle, and
Motorcycle will be able to turn more quickly than Truck. But because all these
classes implement the same interface, you know they will have Drive and Turn
methods with specified arguments. You can, for example, call the method
obj.Drive(25)
on any obj regardless of whether it is a Car, Truck, Motorcycle, or Bicycle.
You can add more members to an object's interface beyond the ones it implements
from the base class. For example, Car might have a MilesPerGallon property while
Bicycle would not. In any event, you should always implement the full interface,
not just part of it. After all, implementing an interface can be seen as a
contract that the class will in fact have all the members.
Using the Collection Object
Visual Basic makes use of collections in many ways, but some of you may not be
aware that it also provides a generic Collection object for use in your
programs. A collection is an ordered set of items that can be referred to as a
unit. What makes the Collection object so useful is that the items it contains
can be essentially anything--variables, literal numbers or text, objects, and so
on. What's more, they do not have to be the same data type.
You create a Collection object using the usual VB syntax:
Dim col As New Collection
You use the Add method to add items to the collection:
col.Add item, key, before, after
Obviously you would not use the before and after arguments at the same time.
If both are omitted the new item is placed at the end of the collection. If you
use one of these arguments it must refer to an existing member of the collection
or else an error will occur.
To retrieve an item from a collection use the Item method:
col.Item(index)
The index argument is either a number specifying the position of the item in the collection or the key string that was specified when the item was added. If the index argument does not match an item in the collection, an error occurs. Note that the numerical index is one-based - in other words, it runs from 1 to the number of items in the collection. Since Item is the default method, you can use the shorthand syntax like this:
col(index)
To delete a item, use the Remove method. Its syntax is exactly the same as
Item. If you remove an item that is not the last item, other items move up to
fill in the space of the deleted item.
The Collection object has one property, Count. It returns the number of items in
the collection.
Collections can be useful in various situations. Suppose your program lets the
user create one or more child windows. You can use a collection to keep track of
them and then destroy them as needed. First create the collection:
Dim col As New Collection
Then, when each window is created add it to the collection:
Dim f As New Form2
col.Add f
f.Show
When it's time to close all the windows, loop through the collection like this:
Dim i As Integer
Dim f As Form
For i = 1 To col.Count
Set f = col(i)
f.Visible = False
Set f = Nothing
Next
VB's Command Buttons are widely used for registering user input such as mouse clicks. The normal behavior of these buttons is to appear "raised" normally, then to appear "down" during the click, and return to the "raised" appearance when the mouse button is released. For most uses this is just what you want. But how about a Command Button that can be toggled? Click it once and it stays in the "down" state; click it again to return it to the "raised" appearance. Such "sticky" buttons are useful for selecting on/off options as an alternative to a checkbox. This tip shows you how to create them
You need to send a message to the Command Button using the SendMessageBynum Windows API function. You must put the function declaration in your program, as well as a constant definition:
Public Const BM_SETSTATE = &HF3
Declare Function SendMessageBynum Lib "user32" Alias
"SendMessageA" _
(ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, _ ByVal lParam As
Long) As Long
Then it's a simple matter of toggling the button in its Click event procedure. The code shown here changes the button's caption as well.
Private Sub Command1_Click()
Static Downstate As Long
Downstate = Not Downstate
If Downstate Then
Command1.Caption = "Option On"
Else
Command1.Caption = "Option Off"
End If
Call SendMessageBynum(Command1.hwnd, BM_SETSTATE, Downstate, 0)
End Sub
If you make the Downstate variable global rather than local, code in other parts f the program will be able to check the button's state as needed.
Simplify Programming with Enumerations
VB lets you create enumerations, programmer-defined data types that can take on a set of constant values. The use of enumerations can simplify certain programming tasks and make your program code easier to read. This tip shows you how to create and use enumerations.
You create an enumeration with the Enum...End Enum statement:
Public Enum EnumName ConstantName1 ConstantName2 ... ContantNameN
End Enum
EnumName is the name of the enumeration following VB's usual variable naming rules. Each constant name-you can have as many as you like-becomes a member of the enumeration. By default, the constants are automatically assigned numerical values in order, starting with 0. Here's an example:
Public Enum Flavor
flVanilla
flChocolate
flCoffee
flStrawberry
End Enum
This results in the constant flVanilla being equal to 0, flClocolate being equal to 1, and so on. Usually the actual numerical values of the constants in an enumeration do not matter, but if you want to assign specific values you can:
Public Enum Flavor
flVanilla = 2
flChocolate = 4
flCoffee = 8
flStrawberry = 16
End Enum
Note the use of a prefix, in this example "fl," as part pf each constant name. This helps to identify the constant, when used in code, as a member of an enumeration. It's not required but it can make code easier to read.
Once you have defined an enumeration, then what? It becomes available in your program as a data type along with VB's built-in data types. You can declare variables of this type as shown here:
Dim MyFavoriteFlavor As Flavor
You can use the type for procedure arguments:
Public Sub MakeCone(f As Flavor)
...
End Sub
Finally, you can use the type as a return value of a function:
Public Function Taste() As Flavor
...
End Function
When you are typing code, the VB editor will display a drop-down list of enumeration members for you to choose from - a real convenience. Also, if you try to use an incorrect value-one not in the enumeration-you'll receive an error message. In terms of saving coding time and preventing errors, enumerations have a place in every VB programmer's toolkit.
Creating Graphical Command
Buttons
The Command Button control is widely used in almost every VB program. Almost always, this control will display a short text caption that describes its function. For some program designs, however, you may want to get a bit more creative and use graphical buttons that use images instead of text to indicate their function. Many VB programmers do not know that the Command Button control has this capability built in. There are three relevant control properties:
- Picture: The image to display normally.
- DownPicture: The image to display when the button is clicked (in the down state).
- DisabledPicture: The image to display when the button is disabled (when the Enabled property is False).
Each of these properties can be set at design time by specifying a bitmap, icon, or metafile image to use. You can also set them at runtime by using the LoadPicture function:
Command1.Picture = LoadPicture(filename)
You must also change the button's style property to 1 - Graphical for the images to display. If the Caption property is not blank, the caption displays along with the graphic. If the graphic is smaller then the button, it will be centered. If it is larger, it will be centered and clipped.
Implementing Mouse-Over
Effects on VB Forms
If you spend much time browsing the web you have probably see mouse-over effects on some web pages (sometimes called a hover effect). These effects change the appearance of a screen element when the mouse cursor passes over them without clicking. The appearance changes back to the way it was originally when the mouse cursor exits the element. Changes in the element's color or its font size are the most commonly used effects. You can do the same thing in VB, as this tip explains.
The technique makes use of two events-or more precisely, the same event for two objects. It's the MouseMove event which fires whenever the mouse cursor moves over a screen element. Here's what to do:
I also like to save the control's default appearance when the form loads. This lets me change its default appearance in the VB form editor and not have to worry about changing my code.
Here's an example that implements a mouse-over effect for a command button. In the form, at the module level, declare two variables to hold the control's default color and font size:
Private OrigColor As Long
Private OrigSize As Integer
In the form's Load event procedure, load these variables with the control properties:
Private Sub Form_Load()
OrigColor = Command1.BackColor
OrigSize = Command1.Font.Size
End Sub
In the Command Button's MouseMove event procedure, change it's appearance to the "over" state:
Private Sub Command1_MouseMove(Button As Integer, Shift As
Integer, X As Single, Y As Single)
Command1.BackColor = vbRed
Command1.Font.Size = 14
End Sub
Finally, in the form's MouseMove event procedure, change them back to the default values:
Private Sub Form_MouseMove(Button As Integer, Shift As Integer,
X As Single, Y As Single)
Command1.Font.Size = OrigSize
Command1.BackColor = OrigColor
End Sub
Understanding the PictureClip
Control
The PictureClip control can simplify the organization and use of graphical resources in your program. It is most commonly used with small graphics that you have a lot of, such as icons and toolbar buttons. With the PictureClip control you can create a single graphical image that contains all the individual icons or buttons, then extract each one for display as needed. The PictureClip control itself is never visible on screen when the program is running.
To use this control you must select Microsoft PictureClip Control 6.0 in the Components dialog box. Place it on the form where it will be needed. You can load the composite image at design time or at run time:
PictureClip1.Picture = LoadPicture("c:\images\icons.bmp")
Once the image is loaded you can retrieve any arbitrary portion of it by setting the ClipX and ClipY properties to specify the top left corner of the region and the ClipHeight and ClipWidth properties to specify the size. Then the Clip property retrieves the region for use in, for example, a PictureBox control:
PicClip1.ClipX = 20
PicClip1.ClipY = 34
PicClip1.ClipWidth = 22
PicClip1.ClipHeight = 22
Picture1.Picture = PicClip1.Clip
You can also divide the composite image into a rectangular grid of same-size sections, and then retrieve a section based on its position. This technique is used when the composite image contains a collection of same-size sub-images such as you might use for a toolbar. You can set the number of rows and columns at design time or at run time via the Cols and Rows property. Then retrieve an individual subimage using the GraphicCell property:
Picture1.Picture = PicClip1.GraphicCell(2)
The top left cell is index 0 and this value increases from left to right and then top to bottom.
Implement a "Mouse Exit"
event for controls
You can detect when the mouse enters a control via the MouseMove event. What about then the mouse exits a control? VB has no built-in event for this but it is easy to create your own. First, put the control on a Frame control that is just slightly larger than the control on all sides. Set the caption of the Frame control to a blank string and set its BorderStyle property to 0-None.
Next, declare a form-level boolean variable that will be true when the mouse in over the control and false if not. For a button named Command1 this might be as follows. Booleans are false by default.
Dim MouseInCommand1 As Boolean
Now, write a sub that performs the actions that you want done when the mouse leaves the control:
Private Sub Command1_MouseExit()
...
End Sub
In the MouseMove event procedure for the control, set the Boolrean variable to true:
Private Sub Command1_MouseMove(Button As Integer, Shift As
Integer, X As Single, Y As Single)
MouseInCommand1 = True
End Sub
Finally, in the MouseMove event procedure for the Frame, check the Boolean variable. If it is true, set it to false and call the procedure that you defined before:
Private Sub Frame1_MouseMove(Button As Integer, Shift As
Integer, X As Single, Y As Single)
If MouseInCommand1 Then
MouseInCommand1 = False
Call Command1_MouseExit
End If
End Sub
Display the Windows Search dialog from a VB Program
The Windows Search dialog box is very handing for finding a variety of items including files, computers, and documents. It is usually displayed by pressing the Windows key + F. It can be useful to display this dialog box from within a VB program. The technique is simply to mimic the normal keypress, Windows+F. This is done with the keybd_event API function. It's definition and a few constants must be placed in a module:
Public Declare Sub keybd_event Lib "user32" _ (ByVal bVk As Byte, _ ByVal bScan As Byte, _ ByVal dwFlags As Long, _ ByVal dwExtraInfo As Long)
Public Const VK_LWIN = &H5B
Public Const KEYEVENTF_KEYUP = &H2
Public Const VK_APPS = &H5D
Then send the required keystrokes from the program as follows:
Private Sub Command1_Click()
Call keybd_event(VK_LWIN, 0, 0, 0)
Call keybd_event(&H46, 0, 0, 0)
Call keybd_event(VK_LWIN, 0, KEYEVENTF_KEYUP, 0)
End Sub
While you cannot transfer information from the Search dialog back to your program, it can still be very useful to locate a file in this way while running a VB program.
Displaying File Properties from
VB
If you right-click a file name in Windows Explorer and select Properties, Windows dislays a dialog box with a variety of information about the file including its size, creation and modification dates, and attributes. This tip shows you how to display the file properties dialog box from within a VB program.
First you need to declare a UDT, some constants, and an API function in a module in your program:
Public Const SW_SHOW = 5
Public Const SEE_MASK_INVOKEIDLIST = &HC
Public Type SHELLEXECUTEINFO
cbSize As Long
fMask As Long
hwnd As Long
lpVerb As String
lpFile As String
lpParameters As String
lpDirectory As String
nShow As Long
hInstApp As Long
' optional fields
lpIDList As Long
lpClass As String
hkeyClass As Long
dwHotKey As Long
hIcon As Long
hProcess As Long
End Type
Public Declare Function ShellExecuteEx Lib "shell32.dll" (ByRef s As SHELLEXECUTEINFO) As Long
Then all you need to do is declare an instance of the UDT, load it with the required information, and call the function:
Dim shInfo As SHELLEXECUTEINFO
With shInfo
.cbSize = LenB(shInfo)
.lpFile = FullPathAndNameOfFile
.nShow = SW_SHOW
.fMask = SEE_MASK_INVOKEIDLIST
.lpVerb = "properties"
End With
ShellExecuteEx shInfo
If you try this for a file that does not exist, Windows pops up a message informaing you of the fact. There is no runtime error, however. Finding the properties of a file from within a VB program can be a useful technique.
Determine if a Credit Card Number is Valid
Credit card numbers are not assigned at random. Each number, usually 16 digits long, must adhere to certain mathematical conditions to be valid. This is called the Luhn Check and it is used by almost all major cards. The first step in any processs that accepts credit card numbers should be verifying the number. This tip shows you how.
The function CCNumberValid is passed a card number as a string. The string must contain only digits - no embedded spaces or dashes. The function returns true if the number is valid and false if not.
Public Function CCNumberValid(ByVal CCNumber As String) As Boolean
Dim Result As Long
Dim Total As Long
Dim idx As Integer
Dim i As Integer
Dim j As Integer
j = 1
For i = Len(CCNumber) To 1 Step -1
Result = (CInt(Mid$(CCNumber, i, 1)) * j)
If Result >= 10 Then
Total = Total +
(CInt(Mid$(CStr(Result), 1, 1)) + CInt(Mid$(CStr(Result), 2, 1)))
Else
Total = Total + Result
End If
If j = 2 Then
j = 1
Else
j = 2
Next
If Total Mod 10 = 0 Then
CCNumberValid = True
Else
CCNumberValid = False
End If
End Function
Of course the fact that a credit card number is valid does not mean that is it actually assigned to an account or that the account is in good standing. But checking the validity of a number is the best and fastest way to weed out errors in number entry.
Some text processing routines require that spaces be normalized. This means that any trailing or leading strings be removed, which is easily done with the Trim() function. It also means that any sequence of 2 or more spaces that is present within the string be replaced with a single space. This takes a little coding, but it is quite easy with the Instr() and Replace() functions. The following function shows how.
Private Function NormalizeSpaces(s As String) As String
' Removes all leading and trailing spaces and
' replaces any occurrence of 2 or more spaces
' with a single space.
s = Trim(s)
Do While InStr(s, String(2, " ")) > 0
s = Replace(s, String(2, " "), " ")
Loop
NormalizeSpaces = s
End Function
Display a Select Folder Dialog
VBs Common Dialog control makes it easy to let the user select a file, but what about selecting a folder? This too is easy with the Microsoft Shell and Automation component. First add a reference to this component, which is located in Shell32.dll, to your project. You'll need these delarations:
Private shlShell As Shell32.Shell
Private shlFolder As Shell32.Folder
Private Const BIF_RETURNONLYFSDIRS = &H1
Then create an instance of the Shell class:
Set shlShell = New Shell32.Shell
Finally display the dialog box and return the result:
Set shlFolder = shlShell.BrowseForFolder(Me.hWnd, "Select a Folder", BIF_RETURNONLYFSDIRS)
Now the selected folder is available in the property shlFolder.Title.
Fire a Command Button Repeatedly
Normally a Command Button fires one event each time it is clicked. Here's how to create a button that fires an "event" repeatedly as long as it is down (as long as the user has not released the mouse button):
1) Add a Timer Control to the form and set its Interval property to the desired interval between repeated events.
2) Put the code to be executed repeatedly in the Timer's Timer event procedure.
3) Set the Timer's Enabled property to True in the Command Button's MouseDown event procedure.
4) Set the Timer's Enabled property to False in the Command Button's MouseUp event procedure.
This kind of repeating Command Button can be useful in a variety of programming situations.