October/November 1996
by Peter G. Aitken ©1996
Originally published in Visual Developer magazine
Down at the lower right corner of your Windows 95 screen, at the end of the Task Bar, is a small rectangular area where the time is displayed. Sometimes, as I'm sure you have noticed, small icons are displayed there as well. This is the system notification area, sometimes called simply the tray because of its recessed appearance. Programs that do not need to be visible as a regular screen window can display icons here, either instead of or in addition to the normal "minimized" icon on the Task Bar. As we'll see, tray icons can be quite useful and in this column I'll show you how to get your Visual Basic programs to display and use them.
Tray icons can serve two purposes. One is simply that of notification. One example is Microsoft Exchange, which puts a small envelope icon in the tray when my Inbox contains unread mail. There are no annoying beeps or dialog messages popping up, but I can see at a glance if I've received a message. Another example is Dial Up Networking, which puts a small modem icon in the tray when I'm connected to my host. This icon flashes red and green "send" and "receive" lights, which means I no longer need to keep my external modem in view to see its send and receive LED's. The modem is now relegated to dangling from a cable behind my desk, where it catches all sorts of interesting dust balls while freeing up valuable desktop real estate.
Tray icons can also accept user input. To be more precise, they can respond to mouse input, with the mouse actions being sent to the underlying program. An example of this is the Windows 95 Volume Control, which can be set to display a small speaker icon in the tray. Double-clicking the icon pops up the Volume Control, providing quick access to the underlying utility. Another way that tray icons respond to input is with "tool tips", a brief descriptive message that displays when the mouse cursor is briefly rested over the icon. You can change the tip text programmatically, permitting up-to-date status messages from the underlying program. My favorite example of this is my laptop's battery meter, which displays the amount of battery time left in its tool tip.
Like just about everything else in Windows, we access the tray by means of an API function. This function lets you add icons to the tray, modify them as needed, and remove them when finished. The function declaration is as follows:
Public Declare Function Shell_NotifyIcon Lib "shell32.dll" _
Alias "Shell_NotifyIconA" (ByVal dwMessage As Long, _
lpData As NOTIFYICONDATA) As Long
The first argument, dwMessage, indicates whether a new icon is being added to the tray, an existing icon is being modified, or an icon is being removed. The constants NIM _ADD (value 0), NIM_MODIFY (value 1), and NIM_DELETE (value 2) are generally used for this argument.
The second message is a data structure that contains all the required information about the icon. The Type declaration is shown here.
Type NOTIFYICONDATA
cbSize as Long
hWnd as long
uID as Integer
uFlags as Integer
uCallbackMessage as Integer
hIcon as Integer
szTip as String * 64
End Type
Let's look at the individual elements of the NOTIFYICONDATA data structure.
I think that most of these data items are pretty clear, and I won't waste valuable space explaining them. The one possible exception is the second item, hWnd. This argument identifies the window that is to receive icon-related messages. This may sound mysterious, but it is not. Windows uses messages for all sorts of communication. For example, when you click on a form in a Visual Basic program, Windows sends the message "you have been clicked" to that form. If you have written a Click() event procedure for that form, it will respond to that message. As Visual Basic programmers we are more accustomed to thinking in terms of events and event procedures rather than messages, but it's really the same thing under a different name.
We are still faced with the problem of what window will receive the messages from the tray icon. When I first became interested in this topic, I found an article in Microsoft Systems Journal that showed how to use tray icons in Visual Basic. The author's approach was to use Visual C++ to write an OLE control that included a hidden window for receipt of icon messages. By including this custom control in your Visual Basic project, you could access the icon-related messages and respond to them as desired.
All very well and good, and this method certainly works fine, but there was one major shortcoming it required going outside of Visual Basic. Now don't get me wrong I have nothing against C++, and it's true there are some things it can do that you just cannot accomplish using Visual Basic alone. Yet one of the main attractions of Visual Basic is that it shields the programmer from many of the arcane details of C++ (I'm tempted to show you some of the C++ code just to give you a good fright!). I almost always prefer to do things completely within Visual Basic and this obviously includes calling the API as needed if it is at all possible.
Fortunately, Visual Basic has all of the tools we need. Several of the standard controls have an hWnd property, permitting you to get their window handle. Most appropriate is the PictureBox control, since we'll be using one to load the icon file and can make it do double duty as the message recipient as well (The Image control does not have an hWnd property, otherwise it would be preferable due to its lower overhead).
Our approach, therefore, is as follows. Start by placing a hidden PictureBox control on the form and load the desired icon into it. Note that the picture must be an icon other image types won't work. If you'll be using only one icon you can assign the icon to the control's Picture property at design time; to switch icons you'll have to load different icons at run-time using the LoadPicture() function.
Next, load the NOTIFYICONDATA structure with the appropriate information, including the PictureBox control's hWnd property as the hWnd structure member, and its Picture property as the hIcon member. We'll specify WM_MOUSEMOVE (value = &H200) as the uCallbackMessage, which results in the PictureBox receiving the MouseMove message each time the icon detects a mouse event. For the uFlags member we'll specify a combination of NIF_MESSAGE, NIF_ICON, and NIF_TIP. If you are not specifying tool tip text in szTip, then you would omit the NIF_TIP flag.
Once you've done this, call Shell_NotifyIcon(), passing NIM_NEW as the first argument to specify that you're adding a new icon to the tray. The NOTIFYICONDATA structure is, of course, the second argument. The icon will appear in the tray, and any time the mouse goes over or clicks it, the WM_MOUSEMOVE message will be sent to the PictureBox control. And guess what? The control's MouseMove event procedure responds to this message, making it a trivial matter to detect and respond to mouse events on the icon.
There's one more hurdle to overcome. While the control's MouseMove event procedure automatically responds to mouse action on the icon, how can we tell what the mouse has done? The WM_MOUSEMOVE message is sent in response to any mouse action, including movement, left clicks, right clicks, and double-clicks. The answer lies in the MouseMove event procedure's X parameter. In normal use this parameter contains the X coordinate of the mouse cursor's screen location. When the mouse cursor is over the tray, however, we don't need its X coordinate since we already know where it is. Instead, Windows uses the X parameter to pass a value to the event procedure indicating exactly what has happened. The possible values are shown in this table (I obtained these values by placing a debug.print() statement in the MouseMove() event procedure).
| Mouse action | Value passed in X | Hex |
| left button pressed | 7695 | 1E0F |
| left button released | 7710 | 1E1E |
| right button pressed | 7740 | 1E3C |
| right button released | 7755 | 1E4B |
| double left click | 7725 | 1E2D |
| double right click | 7770 | 1E5A |
| movement | 7680 | 1E00 |
Be aware that you'll fire the MouseMove event procedure, with an identifying value in X, for each and every event. Thus, a double-click with the left button results in four events as follows:
It's the third event, where X is equal to 7725, that identifies the double click. This makes it difficult to distinguish a single click from a double click, since every double click by definition starts with a single click. The only way I can think of to make this distinction is to use a timer that is set running when a "button down" followed by a "button up" is detected. The timer interval should be set to a value slightly longer than the double click interval. If the timer times out before the second part of a "double click" event happens, then you know a single click has occurred.
Note that an icon in the tray does not "capture" the mouse. If you press the mouse button on the icon, then move the cursor off the icon before releasing the button, only the "down" event is detected.
Listings 1 and 2 present the code for the tray icon demonstration program that I knocked together to show you some of the possibilities. One nice touch is to have a pop-up menu display when then tray icon is right-clicked. This is accomplished by using the Menu Editor to create the desired menu, and setting its Visible property to False so it does not display in the program's form. Then, the form's PopupMenu() method is used to pop the menu up when the appropriate mouse action is detected by the tray icon. I have also coded the program so that a left-click displays the main program form
This demonstration simulates a process that can be started and stopped by means of commands on the popup menu. When the process is "running" a Timer control is used to animate the icon by switching between two icons at 1 second intervals. For this example I used two of the "moon" icons from the Visual Basic icon collection. If your program performs some activity, such as retrieving data from a network, you can use this sort of animation technique to provide a visible indication that the process is ongoing or is stopped. While not appropriate for most programs, the ability to display and use tray icons can add that final touch of professionalism and user convenience to your Visual Basic projects.
Listing 1. TRAY.BAS, the Basic module for the tray icon demonstration.
Option Explicit
' The API function declaration.
Public Declare Function Shell_NotifyIcon _
Lib "shell32.dll" _
Alias "Shell_NotifyIconA" _
(ByVal dwMessage As Long, _
lpData As NOTIFYICONDATA) As Long
'Data type for icon data.
Public Type NOTIFYICONDATA
cbSize As Long
hWnd As Long
uID As Long
uFlags As Long
uCallbackMessage As Long
hIcon As Long
szTip As String * 64
End Type
'The two icons.
Global Const ICON1 = "moon03.ico"
Global Const ICON2 = "moon06.ico"
Global iconData As NOTIFYICONDATA
Global iconFileName As String
Global Const NIM_ADD = &H0
Global Const NIM_MODIFY = &H1
Global Const NIM_DELETE = &H2
Global Const NIF_MESSAGE = &H1
Global Const NIF_ICON = &H2
Global Const NIF_TIP = &H4
Global Const WM_MOUSEMOVE = &H200
Global Const NIF_RBDOWN = 7740
Global Const NIF_LBDOWN = 7695
Listing 2. TRAY.FRM, the form module for the tray icon demonstration.
Begin VB.Menu mnuTrayIcon
Caption = "Tray Icon Menu"
Visible = 0 'False
Begin VB.Menu mnuStart
Caption = "Start"
End
Begin VB.Menu mnuStop
Caption = "Stop"
Enabled = 0 'False
End
Begin VB.Menu mnuOpen
Caption = "Open"
End
Begin VB.Menu mnuExit
Caption = "Exit"
End
End
Private Sub Command1_Click(Index As Integer)
Select Case Index
Case 0
frmTray.Visible = False
Case 1
Shell_NotifyIcon NIM_DELETE, iconData
End
End Select
End Sub
Private Sub Form_Load()
'Load the desired icon into the PictureBox.
iconFileName = ICON1
Picture1.Picture = LoadPicture(App.Path & "\" & iconFileName)
'Set up the NOTIFYICONDATA structure.
iconData.cbSize = Len(iconData)
iconData.hWnd = Picture1.hWnd
iconData.uID = 1&
iconData.uFlags = NIF_MESSAGE Or NIF_ICON Or NIF_TIP
iconData.uCallbackMessage = WM_MOUSEMOVE
iconData.hIcon = Picture1.Picture
iconData.szTip = "Tray Icon Demo" & Chr$(0)
'Display the icon in the tray.
Shell_NotifyIcon NIM_ADD, iconData
End Sub
Private Sub mnuExit_Click()
'Remove the icon from the tray and end the program.
Shell_NotifyIcon NIM_DELETE, iconData
End
End Sub
Private Sub mnuOpen_Click()
' Make the form visible.
frmTray.Visible = True
End Sub
Private Sub mnuStart_Click()
' Start the timer and set menu command states.
mnuStart.Enabled = False
mnuStop.Enabled = True
Timer1.Enabled = True
End Sub
Private Sub mnuStop_Click()
' When the stop command is selected, disble the Timer
' and set menu command states.
mnuStart.Enabled = True
mnuStop.Enabled = False
Timer1.Enabled = False
End Sub
Private Sub Picture1_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single)
'Messages come here when the tray
'icon detects a mouse event.
'On right click, show the popup menu.
If X = NIF_RBDOWN Then
frmTray.PopupMenu frmTray.mnuTrayIcon
'On left click toggle the form's Visible property.
ElseIf X = NIF_LBDOWN Then
frmTray.Visible = Not frmTray.Visible
End If
End Sub
Private Sub Timer1_Timer()
'Toggle the icon file name.
If iconFileName = ICON1 Then
iconFileName = ICON2
Else
iconFileName = ICON1
End If
'Load the icon.
Picture1.Picture = LoadPicture(App.Path & "\" & iconFileName)
'Display the alternate icon.
iconData.hIcon = Picture1.Picture
Shell_NotifyIcon NIM_MODIFY, iconData
End Sub
While we are still a long way from being able to converse with our computers, recent improvements in both software technology and processor capacity have made possible the development of voice recognition software that is actually useful. I have been using Kurzweil Voice for Windows for a couple of months now, and I am quite favorably impressed. I selected the Kurzweil offering over the other heavy hitter in this arena, from IBM, because the IBM product requires a proprietary sound input/processing card, a hassle that my slot-poor system can do without, while Kurzweil uses any 16 bit Sound Blaster compatible sound card.
Voice offers the choice of either a 30,000 or 60,000 word vocabulary, each of which provides plenty of space for new words that you add. It's a "smart" program that learns from its mistakes. Initial recognition is so-so. After an hour or so of use you are prompted to perform a training session during which you read a long list of words and numbers. After training, recognition improves markedly. Accuracy continues to improve even after training, since each time you correct an incorrectly identified word the program makes note of it and is likely to get it right next time. You can move right along, too, speaking at an almost conversational speed with the sole limitation being the need for discrete speech in which there is a brief pause between words. You can also use Voice to enter certain program commands, such as File Save.
More relevant to this column is the ability to use Voice for programming. The input from Voice goes directly to whatever program is active, just as if it had come from the keyboard. This allows you to "talk" Basic code directly into your Visual Basic programs. Because Voice is initially set up for standard text entry, a bit of extra training is required for programming use. For example, I trained it to recognize "lp" and "rp" (pronounced "ell pee" and "are pee") as left and right parenthesis, and "equal" as the equal sign and not the spelled out word.Variable names can be a problem, and sometimes I find that entering them via the keyboard is easier than training Voice to recognize weird names such as iCount or TempFileName. Fortunately, Voice lets you define different profiles so you can keep your programming vocabulary separate from your standard writing vocabulary.
Even though I'm a pretty good typist, Voice has proven its value in a variety of data entry chores, and is particularly handy when I'm juggling a couple of reference books or a pastrami on rye on my lap. For those with carpal tunnel syndrome or any other disability that makes keyboard use difficult or impossible, it could make an even greater difference.
It used to be that the term "RAD" was a shortening of "radical," usually used in a complementary sense. If someone told you that your new hat was "way rad," then that was, like, cool man. Nowadays RAD more often means rapid application development, and that of course is something we Visual Basic weenies know all about. If you want to put even more zip in your rapid, take a close look at RADBench, a new set of productivity tools from Crescent Software. RADBench integrates into the Visual Basic development environment as an add-in, and provides a variety of tools that speed up program development. Control alignment tools, dialog- and message-box designers, a code manager, and tab order setting are some of the ones I find myself using most often.
In its general approach, RADBench is very similar to Sheridan Software's VBAssist. In fact, many of the tools are found in both products. Which is better? It's a tough call, particularly since I haven't used them both sufficiently to become thoroughly familiar with all of their features. My impression, however, is that VBAssist provides a more comprehensive set of tools, while RADBench is more smoothly integrated into Visual Basic and operates more quickly. The real bottom line is that you don't want to do any serious Visual Basic development without one of these products in your toolbox.