Basically Visual

October/November 1996

by Peter G. Aitken 1996

Originally published in Visual Developer magazine


A Tray of Icons, Please

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:

  1. Left button down, X = 7695
  2. Left button up, X = 7710
  3. Left button down again, X = 7725
  4. Left button up, X = 7710

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

 

Talk to Me

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.

Way RAD

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.