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).
Figure 2 CRACK OLE Control
CALLBCTL.H
// callbctl.h : Declaration of the CCallBackCtrl OLE control class. // CCallBackCtrl : See callbctl.cpp for implementation. class CCallBackCtrl : public COleControl { DECLARE_DYNCREATE(CCallBackCtrl) // Constructor public: CCallBackCtrl(); CWordArray m_msglist; // List of registered messages // Overrides // We want first crack at the user messages! virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam); // Drawing function virtual void OnDraw(CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid); // Persistence virtual void DoPropExchange(CPropExchange* pPX); // Reset control state virtual void OnResetState(); // Implementation protected: ~CCallBackCtrl(); DECLARE_OLECREATE_EX(CCallBackCtrl) // Class factory and guid DECLARE_OLETYPELIB(CCallBackCtrl) // GetTypeInfo DECLARE_PROPPAGEIDS(CCallBackCtrl) // Property page IDs DECLARE_OLECTLTYPE(CCallBackCtrl) // Type name and misc status // Message maps //{{AFX_MSG(CCallBackCtrl) // NOTE - ClassWizard will add and remove member functions here. // DO NOT EDIT what you see in these blocks of generated code ! //}}AFX_MSG DECLARE_MESSAGE_MAP() // Dispatch maps //{{AFX_DISPATCH(CCallBackCtrl) afx_msg short WatchMsg(short msg); afx_msg long PtrValL(OLE_HANDLE lpPtr); afx_msg short PtrValI(OLE_HANDLE lpPtr); //}}AFX_DISPATCH DECLARE_DISPATCH_MAP() afx_msg void AboutBox(); // Event maps //{{AFX_EVENT(CCallBackCtrl) void FireCallback(short msg, long wParam, long lParam) {FireEvent(eventidCallback,EVENT_PARAM(VTS_I2 VTS_I4 VTS_I4), msg, wParam, lParam);} //}}AFX_EVENT DECLARE_EVENT_MAP() // Dispatch and event IDs public: enum { //{{AFX_DISP_ID(CCallBackCtrl) dispidWatchMsg = 1L, dispidPtrValL = 2L, dispidPtrValI = 4L, eventidCallback = 1L, //}}AFX_DISP_ID }; };
CALLBCTL.CPP
// callbctl.cpp : Implementation of the CCallBackCtrl OLE control class. #include "stdafx.h" #include "cback.h" #include "callbctl.h" #include "callbppg.h" #include "memory.h" #ifdef _DEBUG #undef THIS_FILE static char BASED_CODE THIS_FILE[] = __FILE__; #endif IMPLEMENT_DYNCREATE(CCallBackCtrl, COleControl) // Message map BEGIN_MESSAGE_MAP(CCallBackCtrl, COleControl) //{{AFX_MSG_MAP(CCallBackCtrl) // NOTE - ClassWizard will add and remove message map entries // DO NOT EDIT what you see in these blocks of generated code ! //}}AFX_MSG_MAP ON_OLEVERB(AFX_IDS_VERB_EDIT, OnEdit) ON_OLEVERB(AFX_IDS_VERB_PROPERTIES, OnProperties) END_MESSAGE_MAP() // Dispatch map BEGIN_DISPATCH_MAP(CCallBackCtrl, COleControl) //{{AFX_DISPATCH_MAP(CCallBackCtrl) DISP_FUNCTION(CCallBackCtrl, "WatchMsg", WatchMsg, VT_I2, VTS_I2) DISP_FUNCTION(CCallBackCtrl, "PtrValL", PtrValL, VT_I4, VTS_HANDLE) DISP_FUNCTION(CCallBackCtrl, "PtrValI", PtrValI, VT_I2, VTS_HANDLE) DISP_STOCKPROP_HWND() //}}AFX_DISPATCH_MAP DISP_FUNCTION_ID(CCallBackCtrl, "AboutBox", DISPID_ABOUTBOX, AboutBox, VT_EMPTY, VTS_NONE) END_DISPATCH_MAP() // Event map BEGIN_EVENT_MAP(CCallBackCtrl, COleControl) //{{AFX_EVENT_MAP(CCallBackCtrl) EVENT_CUSTOM("Callback", FireCallback, VTS_I2 VTS_I4 VTS_I4) //}}AFX_EVENT_MAP END_EVENT_MAP() // Property pages // TODO: Add more property pages as needed. Remember to increase the count! BEGIN_PROPPAGEIDS(CCallBackCtrl, 1) PROPPAGEID(CCallBackPropPage::guid) END_PROPPAGEIDS(CCallBackCtrl) // Initialize class factory and guid IMPLEMENT_OLECREATE_EX(CCallBackCtrl, "MSJ.Callback.Control", 0x3d76aaa0, 0x17aa, 0x11cf, 0xa0, 0x18, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0) // Type library ID and version IMPLEMENT_OLETYPELIB(CCallBackCtrl, _tlid, _wVerMajor, _wVerMinor) // Interface IDs const IID BASED_CODE IID_DCallBack = { 0x3d76aaa1, 0x17aa, 0x11cf, { 0xa0, 0x18, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } }; const IID BASED_CODE IID_DCallBackEvents = { 0x3d76aaa2, 0x17aa, 0x11cf, { 0xa0, 0x18, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } }; // Control type information static const DWORD BASED_CODE _dwCallBackOleMisc = OLEMISC_ACTIVATEWHENVISIBLE | OLEMISC_SETCLIENTSITEFIRST | OLEMISC_INSIDEOUT | OLEMISC_CANTLINKINSIDE | OLEMISC_RECOMPOSEONRESIZE; IMPLEMENT_OLECTLTYPE(CCallBackCtrl, IDS_CALLBACK, _dwCallBackOleMisc) // CCallBackCtrl::CCallBackCtrlFactory::UpdateRegistry - // Adds or removes system registry entries for CCallBackCtrl BOOL CCallBackCtrl::CCallBackCtrlFactory::UpdateRegistry(BOOL bRegister) { if (bRegister) return AfxOleRegisterControlClass( AfxGetInstanceHandle(), m_clsid, m_lpszProgID, IDS_CALLBACK, IDB_CALLBACK, TRUE, // Insertable _dwCallBackOleMisc, _tlid, _wVerMajor, _wVerMinor); else return AfxOleUnregisterClass(m_clsid, m_lpszProgID); } // CCallBackCtrl::CCallBackCtrl - Constructor CCallBackCtrl::CCallBackCtrl() { InitializeIIDs(&IID_DCallBack, &IID_DCallBackEvents); } // CCallBackCtrl::~CCallBackCtrl - Destructor CCallBackCtrl::~CCallBackCtrl() { } // CCallBackCtrl::OnDraw - Drawing function void CCallBackCtrl::OnDraw( CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid) { // Always hide the window - we need a handle, but not a displayed // window. if (m_hWnd != NULL) ::ShowWindow(m_hWnd, SW_HIDE); pdc->Rectangle(rcBounds); } // CCallBackCtrl::DoPropExchange - Persistence support void CCallBackCtrl::DoPropExchange(CPropExchange* pPX) { ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor)); COleControl::DoPropExchange(pPX); } // CCallBackCtrl::OnResetState - Reset control to default state void CCallBackCtrl::OnResetState() { COleControl::OnResetState(); // Resets defaults found in DoPropExchange } // CCallBackCtrl::AboutBox - Display an "About" box to the user void CCallBackCtrl::AboutBox() { CDialog dlgAbout(IDD_ABOUTBOX_CALLBACK); dlgAbout.DoModal(); } // CCallBackCtrl message handlers short CCallBackCtrl::WatchMsg(short msg) { // Control method - the user can add a message for the control // to watch for. m_msglist.Add(msg); return msg; } // When we get a registered message, call FireCallback(msg, wParam, lParam) LRESULT CCallBackCtrl::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) { for (int m=0; m < m_msglist.GetSize(); m++) { if (m_msglist[m] == message) FireCallback(message, wParam, lParam); } return COleControl::WindowProc(message, wParam, lParam); } // Special bonus for people who actually read these code samples! // The following functions take a long int pointer value and return // the long integer or short integer it points to. These can be // useful depending on the information the Callback function sends // back to wParam and lParam. long CCallBackCtrl::PtrValL(OLE_HANDLE lpPtr) { return *((LPLONG) lpPtr); } short CCallBackCtrl::PtrValI(OLE_HANDLE lpPtr) { return *((LPINT) lpPtr); }
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 3 VBTRAY
VBTRAY.FRM
. . . '==================================================== ' When the user clicks on the Create button, do this Private Sub btnCreate_Click() Dim tnd As NOTIFYICONDATA ' The Shell_NotifyIcon data structure ' Alternate between Create and Delete handling If btnCreate.Caption = "Create" Then ' Fill in tnd with appropriate values tnd.szTip = edtTip.Text & Chr$(0) ' Flags: the message, icon, and tip are valid and should be ' paid attention to. tnd.uFlags = NIF_MESSAGE + NIF_ICON + NIF_TIP tnd.uID = 100 tnd.cbSize = Len(tnd) ' The window handle of our callback control tnd.hwnd = CBWnd.hwnd ' The message CBWnd will receive when there's an icon event tnd.uCallbackMessage = WM_USER + 1 tnd.hIcon = picIcon.Picture ' Make the callback window wait for our defined message CBWnd.WatchMsg (WM_USER + 1) ' Add the icon to the taskbar tray rc = Shell_NotifyIcon(NIM_ADD, tnd) ' Flip the button's name btnCreate.Caption = "Remove" ElseIf btnCreate.Caption = "Remove" Then ' On remove, we only have to give enough information for Windows ' to locate the icon, then tell the system to delete it. tnd.uID = 100 tnd.cbSize = Len(tnd) tnd.hwnd = CBWnd.hwnd tnd.uCallbackMessage = WM_USER + 1 rc = Shell_NotifyIcon(NIM_DELETE, tnd) ' Ready to create a new one! btnCreate.Caption = "Create" End If End Sub '========================================= ' btnIcon changes the icon in the tray Private Sub btnIcon_Click() Dim tnd As NOTIFYICONDATA ' Fill the data structure with necessary information, including ' the icon we get from the form's picture box tnd.uFlags = NIF_ICON tnd.uID = 100 tnd.cbSize = Len(tnd) tnd.hwnd = CBWnd.hwnd tnd.uCallbackMessage = WM_USER + 1 tnd.hIcon = picIcon.Picture ' Call the modify part of the function to change the elements indicated ' by the uFlags Type member above - in this case, just the icon. rc = Shell_NotifyIcon(NIM_MODIFY, tnd) End Sub '========================================= ' btnLoad shows the Open common dialog, then retrieves the chosen filename ' and tries to load a picture from it. This can be an icon or an appropriately ' bitmap. Private Sub btnLoad_Click() ComDlg.ShowOpen picIcon.Picture = LoadPicture(ComDlg.filename) End Sub '========================================= ' btnTip changes the Tip property of the icon, similar to the btnIcon ' changing the icon. Private Sub btnTip_Click() Dim tnd As NOTIFYICONDATA ' The tip is going to be important tnd.uFlags = NIF_TIP tnd.uID = 100 tnd.cbSize = Len(tnd) tnd.hwnd = CBWnd.hwnd tnd.uCallbackMessage = WM_USER + 1 ' Get the new tip from the onscreen edit box. This has to have a null ' character appended to it or the tip will appear as a string with as ' many trailing spaces as there are in the fixed-length string. tnd.szTip = edtTip.Text & Chr$(0) rc = Shell_NotifyIcon(NIM_MODIFY, tnd) End Sub '========================================= ' CBWnd_CallBack is called by the Cback control any time it receives ' a window message specified by a previous WatchMsg call to it. It ' provides the VB programmer the original msg/wParam/lParam intact. (These ' may at times be pointers to values instead of actual values; it is up to ' the programmer to know when this will occur and adjust his code accordingly. Private Sub CBWnd_CallBack(ByVal msg As Integer, ByVal wParam As Long, ByVal lParam As Long) ' If the user has pressed the mouse button on the icon, pop up a message box If (lParam = WM_LBUTTONDOWN) Then MsgBox "You clicked on the tray icon!", vbOKOnly, "Shell_NotifyIcon callback" End If End Sub '============================================================ ' The basic Form_Load - this provides a default icon and string ' for the VB program to set the tray icon. Private Sub Form_Load() edtTip.Text = "Generic tip string." picIcon.Picture = Me.Icon End Sub
VBTRAY.BAS
Attribute VB_Name = "Module1" Public Const WM_USER = &H400 Public Const NIF_ICON = &H2 Public Const NIF_MESSAGE = &H1 Public Const NIF_TIP = &H4 Public Const NIM_ADD = &H0 Public Const NIM_DELETE = &H2 Public Const NIM_MODIFY = &H1 Public Const WM_MOUSEMOVE = &H200 Public Const WM_LBUTTONUP = &H202 Public Const WM_LBUTTONDOWN = &H201 Public Const WM_LBUTTONDBLCLK = &H203 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 Declare Function Shell_NotifyIcon Lib "shell32.dll" Alias "Shell_NotifyIconA" (ByVal dwMessage As Long, lpData As NOTIFYICONDATA) As Long
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: | |
Internet:
| Joshua Trupin geeknet@ix.netcom.com Eric Maffei |
This article is reproduced from Microsoft Systems Journal. Copyright © 1995 by Miller Freeman, Inc. All rights are reserved. No part of this article may be reproduced in any fashion (except in brief quotations used in critical articles and reviews) without the prior consent of Miller Freeman.
To contact Miller Freeman regarding subscription information, call (800) 666-1084 in the U.S., or (303) 447-9330 in all other countries. For other inquiries, call (415) 358-9500.