February 1996
The Visual Programmer
Joshua Trupin
Joshua Trupin is a software developer specializing in C/C++ and Visual Basic apps for Windows. He can be reached at 75120.0657@compuserve.com or geeknet@ix.netcom.com.
Click to open or copy the VISPROG project files.
Like good versus evil, the ongoing battle between C++ and Visual Basic® rages as strongly as ever. The first battle is easily solved: just ban rap music and all evil will instantly vanish so we can concentrateonreducingthefederaldeficit.AsforWindows®-based programming, almost everything can be done more easily in Visual Basic, but there really are a few things you just can't do without C++. This month I'll reduce that number by presenting a 32-bit callback window. Yes, you need to dip into MFC just a little bit, but I've found that at least one rule really does hold true in programming: purists are eventually losers.
Some Windows functions need to send information back to your program, a process that can be accomplished in more than one way. Back in the days of 16-bit Windows (everything up to this past August for most programmers), these functions accepted a callback address, a pointer to a specific function in your program that would handle any notifications sent from the other process (either a system function or a user-written function).
In Win32, this doesn't work so well. Unless the callback and notifier functions are in the same program (or one is in a DLL), they will each run in a different address space in memory. Unlike 16-bit Windows, processes are now completely separate from one another-there's no conversion between memory addresses. Therefore, a pointer to a function, which defines a specific location in memory on one side, may be completely meaningless to another process, breaking the callback mechanism. The solution is to replace the callback pointer with a window handle/message combination. The calling program tells the called program or API function "when you want to notify me, send message WM_XXX to my window hWnd. I'll be waiting." There are only two problems now, and they can be worked around. First, every process that wants to use a notification-based function has to create a window, even if it's always hidden. Second, you can't add a message handler to an existing Visual Basic 4.0 window. Providing this window for your Visual Basic app is a classic OLE control role.
One Windows function that needs such a window is the Shell_NotifyIcon routine. This is the call that lets you control what goes in that little bunch of icons to the right of your Windows 95 taskbar (see Figure 1), which are a visual representation of currently running but hidden programs. Examples of this might be a power management program, PCMCIA status display, or the Microsoft Plus! system agent. I wrote a generic OLE control to handle callbacks, which you can use with Visual Basic to control your own icon.
Figure 1 Tray icons
If you don't know how to use the Control Wizard with Microsoft® Visual C++™ 2.x or 4.0, it's not that bad. All you do is type in a control's name, choose a couple of options, and you've generated the control's source project. My CBACK callback control creates a window and tells Visual Basic whenever that window receives a Windows message. In Visual C++, you use ClassWizard to add properties and methods as well as events. The only property needed on the control is its hWnd, and this can be chosen as a stock property (one that's handled automatically by the MFC OLE control classes, without further coding).
The simplest functionality requires one method and one event. I've defined an event named Callback. Since the OLE control is written with C++/MFC and not in Visual Basic, you can intercept specific messages sent to its window. To accomplish this, you have to override the control's default WindowProc function, check to see if the message you're waiting for is about to be processed, and then send the message information along to the generic WindowProc function defined in COleControl, the OLE control base class. (Your control is an extension of the basic functionality provided by this COleControl class.) When your message arrives, the control should fire off its Callback event. Calling FireCallback will trigger the Controlxxx_Callback function, if defined, in your Visual Basic project.
There's only one thing missing now. It would be really inefficient to make the control generate a Callback every time it gets a window message, since it will get hundreds and hundreds over its lifetime. It needs a method to let you register specific messages that trigger the event. Therefore, I've added the WatchMsg method. It takes one parameter-an integer representing a message ID. When you call Controlxxx.WatchMsg(401), the control will add 401 as a message for which it will generate the Callback event. It is standard practice to choose a user-defined message number above WM_USER (defined as &H400) for uses like this, by the way. Windows is guaranteed not to use these messages for its own needs (mouse messages, say) so you won't conflict with an existing message.
Normally, a nongraphical control like this wouldn't need to display a window, so you'd set the "invisible at run time" bit in the Control Wizard. But you need an hWnd to receive the messages from the system, so you have to let the control create a window, then automatically hide it whenever necessary. What I've done is put a ShowWindow(SW_HIDE) in the control's OnDraw routine so no one can see it. The control's window is created, immediately hidden, and still receives notification messages. No one's any the wiser.
But back to the Shell_NotifyIcon function. (There has to be some point to all this C++ work!) A program needs to do two things to put its icon down in the "tray" (so nicknamed because of its recessed 3D look.) First, it needs to fill a structure of type NOTIFYICONDATA. This includes the program's hWnd, a message ID, an icon, and a tip string.
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
Second, it passes this structure to the system Shell_NotifyIcon routine. The program should then go away by making its main window invisible.
Since you're making a system call from Visual Basic that will generate future information based on an undetermined future event (a possible mouse message sent to the tray icon), you need a place to receive notification when the event does occur. In this case, whenever the icon on the tray gets a mouse input message, it's passed on to the message queue of the window provided in the initial Shell_NotifyIcon call, with the new message ID. If not for this requirement, you could write the entire program directly in Visual Basic. The CBACK OLE control does this for you so you can put the guts of your program in Visual Basic (see Figure 2).
To fill in NOTIFYICONDATA, you need a few pieces of information. The cbSize member is just the length of the full structure. You can pass it the Len() of the user type:
nid.cbSize = Len(nid)
hWnd is the callback window-CBackControl.hWnd. uID is the ID of the icon on the tray. This can be just about any integer. uFlags tells the call which pieces of data are valid. When you update the display, you don't need to fill in the entire structure each time. uCallbackMessage is the message the CBackControl receives whenever there's been a mouse event on the icon. You should set this to something like &H401 to be safe, then pass it to the control's WatchMsg method. hIcon is the 16 x 16 icon that's displayed down there. Visual Basic handles this quite well for you if you write
nid.hIcon = Form1.Icon
The szTip entry is the string displayed whenever the mouse cursor lingers over the tray icon for a second. It can be changed at any time, a useful feature for a program that continually updates its status, such as a battery power meter. The Icon contains the 16 x 16 icon that's displayed in the tray. This, too, can be changed. Imagine the battery power meter changing to show a half-full battery, or a plug when the machine is running on A/C.
Shell_NotifyIcon can be called three different ways. You can add an icon, remove an icon, or modify an existing icon. In each case, you fill up a NOTIFYICONDATA structure and pass it to Shell_NotifyIcon. To specify what you want Shell_NotifyIcon to do, you pass one of three self-explanatory constants to it as the first parameter: NID_ADD, NID_DELETE, or NID_MODIFY. (The icon is automatically deleted from the tray if the message-recipient hWnd is closed, but not until the user moves a mouse over it or causes a similar callback to be sent to the invalid hWnd. So it's not a terrible situation if the program doesn't clean up after itself, but it's still sloppy. Don't do it.)
So let's quickly review. I've provided a short Visual Basic program (see Figure 3) that lets you create a generic tray icon using the form's default icon (that "slab of granite with a title bar" picture that Visual Basic gives you by default). The tip text is set up correctly when you click the Create button (which then turns into a Remove button for your convenience). I've also provided three more buttons; one lets you change the tray's icon instantaneously, while the other sets the tip text to whatever you've typed in. There's also a button that brings up a File Open common dialog and allows you to choose a new icon. When running properly, it looks like Figure 4, the result of a tray icon click, plus the tip text displayed when the mouse lingers over the icon for a second.
Figure 4 Changing tip text and icons
Using C++ shouldn't be something to shy away from, since there are still parts of Windows that remain uncharted by Visual Basic. If you retain at least a basic working knowledge of MFC and control creation, it will give you a lot of extra firepower when it comes to reaching those awkward corners of the Windows API.
Haveaquestionaboutprogrammingin Visual Basic,VisualFoxPro, Access, Office, or stuff like that? Mail it directly to The Visual Programmer, Microsoft Systems Journal, 825 Eighth Avenue, 18th Floor, New York, New York 10019, or send it to MSJ (re: Visual Programmer) via:
Joshua Trupin
75120,657
geeknet@ix.netcom.com
Eric Maffei
ericm@microsoft.com