This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


March 1996

Microsoft Systems Journal Homepage

C++ Q&A

Paul DiLascia is a freelance software consultant specializing in training and software development in C++ and Windows. He is the author of Windows++: Writing Reusable Code in C++ (Addison-Wesley, 1992).

Click to open or copy the TRAYTEST project files.

Click to open or copy the TRACEWIN project files.

Click to open or copy the UPDTENMD project files.

QI noticed that some programs install an icon in the Windows® 95 task bar, on the right edge near the clock. For example, the System Agent that comes with the Windows 95 Plus! pack installs an icon. Does MFC have a class that will let me add my own icon to the task bar?

Bruce Eddington

ANo, but it's easy to write your own. Putting your own icon in the Windows 95 "system tray" is pretty straightforward. (In fact, you can do it from Visual Basic, as Josh Trupin detailed in last month's Visual Programmer column.) I'll teach you C++ folks how to do it this go-round. One function, Shell_NotifyIcon, is all you need. Shell_NotifyIcon is pretty simple. The first argument is a code (NIM_ADD, NIM_MODIFY, or NIM_DELETE) that says whether you want to add, modify, or delete an icon from the tray. The second and only other argument is a pointer to a NOTIFYICONDATA struct.

 // (From SHELLAPI.H)
typedef struct _NOTIFYICONDATA {
DWORD cbSize; // sizeofstruct,youmustset
HWND hWnd; // HWND sending notification
UINT uID; // IDoficon(callbackWPARAM)
UINT uFlags; // see below
UINT uCallbackMessage; // sent to your wndproc
HICON hIcon; // handle of icon
CHAR szTip[64]; // tip text
} NOTIFYICONDATA;

// uFlags
#define NIF_MESSAGE 0x1 // uCallbackMessage is valid
#define NIF_ICON 0x2 // hIcon is valid
#define NIF_TIP 0x4 // szTip is valid

hWnd is a handle to your window that "owns" the icon. uID can be any ID you like that identifies your tray icon (in case you have more than one). Typically, you'll use its resource ID. hIcon can be a handle to any icon, including predefined system icons like IDI_HAND, IDI_QUESTION, IDI_EXCLAMATION, or IDI_WINLOGO, the Windows logo.

Displaying icons is nice, but what's really fun are events. To receive notification when the user moves the mouse over or clicks on your tray icon, you can set uCallbackMessage to your very own message ID, and set the NIF_MESSAGE flag. When the user moves or clicks the mouse over the icon, Windows will call your window proc with hWnd equal to your window handle, specified in hWnd; messageID is the value you specified in uCallbackMessage; wParam is the value you specified as uID; and lParam is a mouse event (such as WM_LBUTTONDOWN).

Shell_NotifyIcon is short, simple, and sweet. But like most Windows API functions, it's a little clunky and assemblerish. So I encapsulated it in a C++ class, CTrayIcon (see Figure 1). CTrayIcon hides NOTIFYICONDATA, message codes, flags, and all that rot. It presents a more programmer-friendly interface to tray icons. But CTrayIcon is more than just a wrapper for Shell_NotifyIcon-it's a miniframework. It enforces the correct user interface behavior for tray icons, as per the Windows Interface Guidelines for Software (available on MSDN). Here's the executive sumary:

TRAYTEST.H

CTrayIcon encapsulates all but the last of these rules. To show how it works, I wrote a little program. When you run TRAYTEST, it displays the dialog in Figure 2, then installs an icon in the system tray and goes into hiding. If you double-click the tray icon, TRAYTEST appears with a window that displays tray notifications as you move or click the mouse in the tray icon (see Figure 3).

Figure 2 TRAYTEST

Figure 3 TRAYTEST

To use CTrayIcon, the first thing you have to do is instantiate a CTrayIcon someplace where it'll live for the lifetime of the icon. TRAYTEST does it in its frame window.

 class CMainFrame : public CFrameWnd {
protected:
CTrayIcon m_trayIcon;
// my tray icon . . . };

When you instantiate a CTrayIcon, you must supply an ID. This is the one-and-only ID used for the lifetime of the icon, even if you later change the actual icon displayed. This ID is the one you'll get when mouse events happen. It need not be the resource ID of the icon; for TRAYTEST, it's IDR_TRAYICON, initialized by the CMainFrame constructor.

 CMainFrame::CMainFrame() : m_trayIcon(IDR_TRAYICON)
{ . . . }

To add the icon, call one of the overloaded SetIcon functions:

 m_trayIcon.SetIcon(IDI_MYICON);         //resource ID
m_trayIcon.SetIcon("myicon"); //resourcename
m_trayIcon.SetIcon(hicon); //HICON
m_trayIcon.SetStandardIcon(IDI_WINLOGO);//system icon

All these functions take an optional LPCSTR argument to use as the tip text, except for SetIcon(UINT uID) which looks for a string resource with the same uID as the tip. For example, TRAYTEST contains the line,

 // (In mainframe.cpp)
m_trayIcon.SetIcon(IDI_MYICON);

which also sets the tip, because TRAYTEST has a string with the same ID:

 // (In TRAYTEST.RC)
STRINGTABLE PRELOAD DISCARDABLE
BEGIN
IDI_MYICON"Double-clickthebananatoactivateTRAYTEST."
END

If you want to change the icon, you can call one of the SetIcon functions again with a different ID or HICON. CTrayTest will know to do NIM_MODIFY instead of NIM_ADD. The same function even works to remove the icon:

 m_trayIcon.SetIcon(0);//removeicon

CTrayIcon will translate this into NIM_DELETE. All those codes, all those flags replaced with a single overloaded function: isn't C++ great? Now, what about notifications and all that UI stuff I mentioned? To handle tray notifications, call CTrayIcon::SetNotificationWnd sometime before you set the icon, but after your window is created. The perfect place is in your OnCreate handler, which is where TRAYTEST does it.

 // Private message used for tray notifications
#define WM_MY_TRAY_NOTIFICATION WM_USER+0
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{ . . . // Notify me, please m_trayIcon.SetNotificationWnd(this,
WM_MY_TRAY_NOTIFICATION);
m_trayIcon.SetIcon(IDI_MYICON);
return 0;
}

Once you've registered yourself, you handle tray notifications in the normal message map manner.

 BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_MESSAGE(WM_MY_TRAY_NOTIFICATION,
OnTrayNotification)
// (or ON_REGISTERED_MESSAGE)
END_MESSAGE_MAP()

LRESULT
CMainFrame::OnTrayNotification(WPARAM wp, LPARAM lp)
{ . . // display message . return m_trayIcon.OnTrayNotification(wp, lp); }

When your handler gets control, WPARAM is the ID you specified when you constructed the CTrayIcon; LPARAM is the mouse event (for example, WM_LBUTTONDOWN). You can do whatever you like when you get the notification; TRAYTEST displays information about the notification (see Figure 1, in MAINFRM.CPP, for details). When you're finished, call CTrayIcon::OnTrayNotification to perform default processing. This virtual (so you can override it) function implements the default UI behavior I mentioned earlier. In particular, it handles WM_LBUTTONDBLCLK and WM_RBUTTONUP. CTrayIcon looks for a menu with the same ID as the icon (for example, IDR_TRAYICON). If a menu with this ID exists, CTrayIcon displays it when the user right-clicks the icon; when the user double-clicks, CTrayIcon executes the first command in the menu. Only two things require further explanation.

Before displaying the menu, CTrayIcon makes the first item the default, so it appears bold. But how do you make a menu item bold? After several minutes of grep'ing in \MSDEV\INCLUDE\*.H, I discovered Get/SetMenuDefaultItem. There are no CMenu wrappers for these (as of MFC 4.0), so I had to call them directly.

 // Make first menu item the default (bold font)
::SetMenuDefaultItem(pSubMenu->m_hMenu, 0, TRUE);

Here 0 identifies the first menu item, and the TRUE specifies that the item ID is by position, not ID. How come there's no MFC wrapper for Get/SetMenuDefaultItem? The folks in Redmond explained that it's because these functions (and several others, like ::Get/SetMenuItemInfo, ::LoadImage, and so on) are not yet implemented in Windows NT(asof3.51).As soon as Windows NT gets its facelift, wrappers will be added to MFC.

The second item of interest in CTrayIcon::OnTrayNotification is what it does to display the context menu:

 ::SetForegroundWindow(m_nid.hWnd);  
::TrackPopupMenu(pSubMenu->m_hMenu, ...);

To make TrackPopupMenu work properly in the context of a tray, you must first call SetForegroundWindow on the window that owns the popup. Otherwise, the menu will not disappear when the user presses Escape or clicks the mouse outside the menu. This totally obscure behavior caused me hours of grief until I finally uncovered a problem report about it on MSDN. To find out more, search for Q135788 in MSDN. What I love most is that after describing the problem and workaround at great length, the PRB concludes by stating "This behavior is by design."

As you can see, CTrayIcon makes tray icons almost trivial. All TRAYTEST does to make its tray menu work are implement a notification handler that calls CTrayIcon::OnTrayNotification, and provide a menu with the same ID as the CTrayIcon.

 // (In TRAYTEST.RC)
IDR_TRAYICON MENU DISCARDABLE
BEGIN
POPUP "&Tray"
BEGIN
MENUITEM "&Open", ID_APP_OPEN
MENUITEM "&About TRAYTEST...",ID_APP_ABOUT
MENUITEM SEPARATOR
MENUITEM "&Suspend TRAYTEST", ID_APP_SUSPEND
END
END

When the user right-clicks the tray icon, CTrayIcon displays this menu (see Figure 4). And if the user double-clicks, CTrayIcon executes the first item: Open, which activates TRAYTEST (normally, it's hidden). To terminate TRAYTEST, you must select Suspend TRAYTEST from the tray menu. If you do File Exit or close the TRAYTEST main window, TRAYTEST doesn't really close, it merely hides itself.TRAYTESToverrides CMainframe::OnClose to provide this behavior (see Figure 1 MAINFRM.CPP).

Figure 4 TRAYTEST menu

Before leaving you with CTrayIcon, some words of advice. I was almost afraid to answer this question, because I know everyone is going to run out and implement a tray icon as soon as they find out how. (I did.) It's just one of those things about being a programmer: as soon as there's some new little graphic gizmo, you have to try it out. Go ahead, do it. Add a tray icon to your app, stare at it, feel good, show it to your friends, have a party. Then take it out. Because most apps have no need for them. Unless you're writing some sort of system add-in like a replacement shell or improved print spooler or fontware DLL that loads invisibly, all tray icons will do is contribute to screen pollution. Figure 5 shows my nightmare vision of tray icons gone amok.

Figure 5 Tray icon abbundanza

Update

A few issues back (December 1995), someone wrote asking how to save and restore the position of an MDI child window in a document. The answer I gave focused primarily on the mechanics of changing the frame window position at the appropriate time during loading. I showed how to override CDocTemplate::InitialUpdateFrame to get the saved position from the document, and then move the frame. As far as the actual moving was concerned, I did the obvious thing: GetWindowRect to get the position of the window for saving it, and MoveWindow to restore the saved position.

I recently noticed a problem with my TRACEWIN program (October 1995), which also uses GetWindowRect/MoveWindow to save and restore the window position across user sessions. Sometimes, when I ran TRACEWIN, it would come up invisible. TRACEWIN would appear in the Windows 95 task bar, but when I clicked to activate it, there was no window anywhere I could see. Moreover, I'd noticed the same behavior with a commercial app I have, but it wasn't reproducible so I ignored it.

A little investigation revealed what was going on. When I looked at the registry entries for my saved window position, the top left corner was at (x,y) coordinates (3000,3000). On an 800 x 600 display, that's somewhere roughly near Saturn. Likewise with my commercial app: the window was at (3000, 3000). How were these windows getting moved to outer space?

Well! As anyone knows who's ever used it, Windows 95 doesn't minimize windows the same way Windows 3.1 did. Instead, Windows 95 moves your window off the screen. When I shut down my computer while TRACEWIN was minimized, TRACEWIN saved these bogus coordinates and restored them the next time it ran. Once I figured out what was going on, I was able to consistently reproduce the bug by minimizing TRACEWIN, then closing it from the task bar without restoring it first. Sure enough, the next time I ran TRACEWIN, it came up in outer space.

I thought, OK, I won't save the position if the window is minimized. But I quickly realized that wouldn't do: what if the user changed the window size before minimizing it? The new size wouldn't get saved. So what was I supposed to do, save the window position every time the user minimized or maximized the window? Sheesh. And if this remember-the-position feature was really what it claimed to be, TRACEWIN should come up minimized if that's how it was the last time it was used. Suddenly I was contemplating several lines of code just to save the measly window position.

At this point I let out a long groan, because I suddenly remembered how you're supposed to do this stuff. There's a pair of little-known but really useful Windows API functions whose only roles in life are to manage the saving and restoring of window positions: GetWindowPlacement and SetWindowPlacement. Placement refers to the size and position of the window when it's in restored state (neither minimized nor maximized), whether it's minimized or not, and whether activating it should go to maximized state instead of restored state. This last situation arises when the user maximizes a window, then minimizes it. Activating the window should restore it to maximized state, not restored state-but the window should still remember its restored position in case the user clicks the restore button in the title bar. It's all very complicated, but GetSetWindowPlacement make a molehill out of the mountain. All you have to do is call GetWindowPlacement to get the placement, then call SetWindowPlacement to restore it.

GetWindowPlacement returns everything you could ever want to know about your window's placement in a struct called WINDOWPLACEMENT. You can save this information in the registry, then read it back when your program starts up, and call SetWindowPlacement to restore the window to the exact same placement as before, including all the bizarre minimize/maximize/restore semantics. It all works just like it should. The only thing Get/SetWindowPlacement doesn't do is read and write the information to your app's profile (registry key or INI file). Since that seems like such a natural thing to do, I wrote a little class that does it.

Figure 6 shows how I implemented CWindowPlacement, and Figure 7 shows how I modified TRACEWIN to use it. The implementation is brainless-the hardest part was thinking up registry key names for the items in WINDOWPLACEMENT. I could've used the MFC functions Get/WriteProfileBinary to save the whole struct in one fell swoop, but I wanted something more readable than hex, so if something ever goes wrong, I can always manually edit my profile with REGEDIT (see Figure 8). The only thing CWindowPlacement does that could be considered remotely clever is check that the restored position is in fact visible before restoring it, in case the user changes display resolution from something like 1024x68 to SVGA's lowly 800x600 (which is what I use, so I'll be laughing when I'm old and all my programmer friends have gone alexic from staring at too many tiny pixels).

Figure 8 Manually editing profiles with REGEDIT

Have a question about programming in C or C++? You can mail it directly to C/C++ Q&A, Microsoft Systems Journal, 825 Eighth Avenue, 18th Floor, New York, New York 10019, or send it to MSJ (re: C/C++ Q&A) via:

Internet:

Paul DiLascia
72400.2702@compuserve.com

Eric Maffei
ericm@microsoft.com

From the March 1996 issue of Microsoft Systems Journal.