|
Basically Visual Columns
|
|
| Are you a technical writer preparing long, complex documents in information sciences, hardware design, or software documentation? This is the first book ever written that specifically deals with the many problems and errors that can crop up when you are writing the long, complex documents typical of these technical fields. The book starts by showing you how to set Word's options to avoid unwanted formatting changes and time-wasting document corruption. Then it takes you through those areas of Word that are most diffucult to use and most likely to cause problems, including tables, automatic numbering, headers/footers, document sections, tables of contents, positioning graphics, styles, and templates. It ends with a chapter of dos and don'ts and advice on best practices for document backup and security. Click here for more details and online ordering. |
...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 a