C++/Win32: The System Tray and Balloon Tips
March 2, 2008
Good news! We are going to again break away from the standard Win32 stuff and explore something fun. Actually, two fun things: minimizing to the system tray, and creating balloon tips. Balloons are those little comic book style dialog bubbles that popup in the lower right of the screen telling you information you don’t necessarily need to react to (”There are updates ready to be installed,” “New Hardware Found,” “The program is still running in the system tray,” etc.)
Here’s an image just in case you don’t know what I’m talking about:

Of course, I must admonish you not to just do this in your apps just because you can. Have a reason to do it. If you have an application that needs to keep running, but doesn’t need a lot of attention from the user, this might be a good time to use the system tray. Does our port scanner need to do this? No, not at all. But what if we extended this application, giving it a scheduling system wherein the user could request that a scan across a range of ports on a certain IP address be performed once per hour? Then we’d obviously need to keep the application running, but out of the user’s way. There’s a good enough reason to minimize to the system tray.
Let’s begin.
The DrawAnimatedRects function
Normally, we’d get all bent out of shape if we actually had to animate something, but happily, there is a function that does the window minimization animation for you. All you have to do is pass it two RECT pointers — a to and a from — so that the function knows where to do the animation. This is quite simple. Get the RECT of the window as it is now, calculate the RECT of the window where you want it to be, and call DrawAnimatedRects. Also pass in the HWND of a clipping window and the IDANI_CAPTION constant (meaning animate the caption bar) and that’s it. The end. The simplest way to animate minimizing to the system tray is to call DrawAnimatedRects, passing in the current window and a RECT containing a tiny rectangle near the bottom right of the screen. This isn’t a wonderful solution, but its a good way to see this function in action without getting too complicated too soon. Try this out in your message handler in response to the WM_SYSCOMMAND message with a wParam value of SC_MINIMIZE system command message:
case WM_SYSCOMMAND:
if (wParam == SC_MINIMIZE) {
RECT desktopRect,thisWindowRect;
GetWindowRect(GetDesktopWindow(),&desktopRect);
GetWindowRect(m_hWnd,&thisWindowRect);
// Set the destination rect to the lower right corner of the screen
desktopRect.left = desktopRect.right;
desktopRect.top = desktopRect.bottom;
// Animate
DrawAnimatedRects(m_hWnd,IDANI_CAPTION,&thisWindowRect,&desktopRect);
}
DefWindowProc(m_hWnd,uMsg,wParam,lParam);
break;
Notice we have to call DefWindowProc when we’re done (for now) because otherwise we are preventing the default behavior for all WM_SYSCOMMAND messages from occurring. Not good.
At any rate, you should see an animation that looks like it’s sending the window to the lower right. It doesn’t, of course, and in fact after the animation finishes the window just minimizes as normal — with its own animation, no less. So basically all DrawAnimatedRects does is show a cute little animation — nothing more. We will have to handle the rest.
As a quick note, you should be aware that some users will have Windows configured to disable these animations. You should be polite and check to see if the user has them turned off. The following bit of code will let you know if the user has animations on or off:
ANIMATIONINFO ai; ai.cbSize = sizeof(ai); SystemParametersInfo(SPI_GETANIMATION,sizeof(ai),&ai,0); if (ai.iMinAnimate) // animations are turned ON, go ahead with the animation
So far so good. We really want to do the animation from our window to the specific location of the system tray, though. Why? Because some people don’t have their taskbar at the bottom of the screen. If the user’s task bar is running vertically along the left side of the screen, you’re going to feel pretty foolish when your animation sends the window to the bottom right.
The truth is, you can’t even assume that the user is running EXPLORER.EXE as the shell (and therefore might not even have a system tray). But, come on, yeah you can. Do you know anyone running alternative shells? I don’t. But really you should not assume that the user is. Heh.
We’ll get the actual RECT of the system tray a little later. For now, let’s concentrate on minimizing our window there.
Immediately after doing the animation, the following will minimize the application, and stick our app’s icon into the system tray:
// Hide the window ShowWindow(m_hWnd,SW_HIDE); // Show the notification icon NOTIFYICONDATA nid; ZeroMemory(&nid,sizeof(nid)); nid.cbSize = sizeof(NOTIFYICONDATA); nid.hWnd = m_hWnd; nid.uID = 0; nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; nid.uCallbackMessage = WM_USER; nid.hIcon = LoadIcon(m_hInstance,MAKEINTRESOURCE(IDI_ICON1)); lstrcpy(nid.szTip,”Double-Click To Maximize.”); Shell_NotifyIcon(NIM_ADD,&nid);
Here you see we hide the window (which we’ve seen and done before), and then create a NOTIFYICONDATA structure. We then fill the structure with info about what we want to do, and call Shell_NotifyIcon, passing it our NOTIFYICONDATA structure. As you can probably figure out on your own, we are saying we want an icon in the tray, we want a message to be fired when the user interacts with our icon in the system tray, and we want a tooltip to show when the user hovers his mouse over the icon. We set the tooltip text to “Double-Click To Maximize”, and specify our icon in the hIcon field, and the message we want to receive upon user interaction with the icon in uCallBackMessage. Pretty easy, right?
Wait a minute… what is that WM_USER message? Windows provides a base message constant for use in situations like this. WM_USER is a user message that can be sent when we need to send our own messages, or receive messages for a specific event that we’ve come up with ourselves. You can specify more than one user message by adding a number to WM_USER. So if we needed another user defined message later, we could call the next one WM_USER+1. WM_USER messages are meant for use by private window classes. Similarly, in case you ever need it, your app itself can use user defined messages in the form WM_APP+X. Consult the docs for more information.
Try this out in your app now. You’ll see an animated minimization, and your icon will appear in the system tray. Hooray! I really recommend putting this code in its own function, and calling the function from your message handler. I don’t like messy message handlers, and I really doubt anyone does. In the source code for this lesson, I’ve called the function MinimizeToSysTray.
Now, how do we recover?
There’s a reason we specified the WM_USER message in the above code. It’s this message that we will respond to when we need to return from the system tray. We will check for a double-click associated with the WM_USER message.
One problem is that if we return on a double-click, there will be a WM_LBUTTONUP message waiting for whatever icon moves in to take our icon’s place. So what we really want to do is set a flag on double-click, then check the flag on WM_LBUTTONUP. That way we’ll get the button up out of the queue, preventing the replacing icon from taking its place. This is not necessary, really, but we never know how other apps are written, so we shouldn’t assume that they will handle our stray messages gracefully. If another app returns from the system tray on a WM_LBUTTONUP message, then it will. And who do you think the user will blame? Probably us. So we will need to add a BOOL value to our class called m_bIconDblClicked that we’ll set to true on the double-click message. We will then set it back to false on processing the WM_LBUTTONUP message. Here is the block from the message handler:
case WM_USER:
switch(lParam)
{
case WM_LBUTTONDBLCLK:
m_bIconDblClicked = TRUE;
return true;
case WM_LBUTTONUP:
if (m_bIconDblClicked) {
ReturnFromSysTray();
m_bIconDblClicked = FALSE;
}
return true;
}
break;
And our ReturnFromSysTray function:
void PortScannerWindow::ReturnFromSysTray() {
RECT desktopRect,thisWindowRect;
GetWindowRect(GetDesktopWindow(),&desktopRect);
desktopRect.left = desktopRect.right;
desktopRect.top = desktopRect.bottom;
GetWindowRect(m_hWnd,&thisWindowRect);
// Animate the maximization
DrawAnimatedRects(m_hWnd,IDANI_CAPTION,&desktopRect, &thisWindowRect);
ShowWindow(m_hWnd,SW_SHOW);
SetActiveWindow(m_hWnd);
SetForegroundWindow(m_hWnd);
// Hide the notification icon
NOTIFYICONDATA nid;
ZeroMemory(&nid,sizeof(nid));
nid.cbSize = sizeof(NOTIFYICONDATA);
nid.hWnd = m_hWnd;
nid.uID = 0;
nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
nid.uCallbackMessage = WM_USER;
nid.hIcon = LoadIcon(m_hInstance,MAKEINTRESOURCE(IDI_ICON1));
lstrcpy(nid.szTip,"Double-Click To Maximize.");
Shell_NotifyIcon(NIM_DELETE, &nid);
}
The source for this lesson doesn’t use the m_bIconDblClicked flag, and doesn’t respond to the WM_LBUTTONDBLCLK message at all. It simply responds to the WM_LBUTTONUP message by calling ReturnFromSysTray. Therefore, we return from the system tray on a single click of the tray icon, and the WM_LBUTTONUP message is processed. We still have to handle a possible second click (because users love to double click even when they should single click) so I’ve declared a flag, m_bInSysTray, and we check that flag before calling MinimizeToSysTray or ReturnFromSysTray. This way, double-click/single-click on the icon, either way, and the window returns with no side-effects.
It’s almost perfect. We just need to make one modification: we really want the animation to take us to the RECT of the systray, not the bottom-right corner of the desktop window.
For this I’ve grabbed a function from Matthew Ellis’ CodeProject Demo. It is a clever solution that makes no assumptions about what shell the user is running, or what version for that matter. However, by default it returns a square near the bottom right if it can’t find a system tray. It would seem that if your application is going to use the system tray, you have to assume that a fully EXPLORER.EXE compatible shell exists, otherwise you are going to hide your app without the user having a way to return to close it. But, seriously, come on — your users will be running the EXPLORER.EXE shell. Seriously, now.
Here’s the function, modified only to make it a member of the PortScannerWindow class:
void PortScannerWindow::GetTrayWndRect(LPRECT lpTrayRect) {
// Note: this function was taken verbatim from Matthew Ellis' CodeProject
// Demo. See http://www.codeproject.com/shell/minimizetotray.asp for
// more info.
// First, we'll use a quick hack method. We know that the taskbar is a window
// of class Shell_TrayWnd, and the status tray is a child of this of class
// TrayNotifyWnd. This provides us a window rect to minimize to. Note, however,
// that this is not guaranteed to work on future versions of the shell. If we
// use this method, make sure we have a backup!
HWND hShellTrayWnd=FindWindowEx(NULL,NULL,TEXT("Shell_TrayWnd"),NULL);
if(hShellTrayWnd) {
HWND hTrayNotifyWnd=FindWindowEx(hShellTrayWnd,NULL,TEXT("TrayNotifyWnd"),NULL);
if(hTrayNotifyWnd) {
GetWindowRect(hTrayNotifyWnd,lpTrayRect);
return;
}
}
// OK, we failed to get the rect from the quick hack. Either explorer isn't
// running or it's a new version of the shell with the window class names
// changed (how dare Microsoft change these undocumented class names!) So, we
// try to find out what side of the screen the taskbar is connected to. We
// know that the system tray is either on the right or the bottom of the
// taskbar, so we can make a good guess at where to minimize to
APPBARDATA appBarData;
appBarData.cbSize=sizeof(appBarData);
if(SHAppBarMessage(ABM_GETTASKBARPOS,&appBarData)) {
// We know the edge the taskbar is connected to, so guess the rect of the
// system tray. Use various fudge factor to make it look good
switch(appBarData.uEdge)
{
case ABE_LEFT:
case ABE_RIGHT:
// We want to minimize to the bottom of the taskbar
lpTrayRect->top=appBarData.rc.bottom-100;
lpTrayRect->bottom=appBarData.rc.bottom-16;
lpTrayRect->left=appBarData.rc.left;
lpTrayRect->right=appBarData.rc.right;
break;
case ABE_TOP:
case ABE_BOTTOM:
// We want to minimize to the right of the taskbar
lpTrayRect->top=appBarData.rc.top;
lpTrayRect->bottom=appBarData.rc.bottom;
lpTrayRect->left=appBarData.rc.right-100;
lpTrayRect->right=appBarData.rc.right-16;
break;
}
return;
}
// Blimey, we really aren't in luck. It's possible that a third party shell
// is running instead of explorer. This shell might provide support for the
// system tray, by providing a Shell_TrayWnd window (which receives the
// messages for the icons) So, look for a Shell_TrayWnd window and work out
// the rect from that. Remember that explorer's taskbar is the Shell_TrayWnd,
// and stretches either the width or the height of the screen. We can't rely
// on the 3rd party shell's Shell_TrayWnd doing the same, in fact, we can't
// rely on it being any size. The best we can do is just blindly use the
// window rect, perhaps limiting the width and height to, say 150 square.
// Note that if the 3rd party shell supports the same configuraion as
// explorer (the icons hosted in NotifyTrayWnd, which is a child window of
// Shell_TrayWnd), we would already have caught it above
hShellTrayWnd=FindWindowEx(NULL,NULL,TEXT("Shell_TrayWnd"),NULL);
if(hShellTrayWnd) {
GetWindowRect(hShellTrayWnd,lpTrayRect);
if(lpTrayRect->right-lpTrayRect->left>150)
lpTrayRect->left=lpTrayRect->right-150;
if(lpTrayRect->bottom-lpTrayRect->top>30)
lpTrayRect->top=lpTrayRect->bottom-30;
return;
}
// OK. Haven't found a thing. Provide a default rect based on the current work
// area
SystemParametersInfo(SPI_GETWORKAREA,0,lpTrayRect,0);
lpTrayRect->left=lpTrayRect->right-150;
lpTrayRect->top=lpTrayRect->bottom-30;
}
Right. So now, in a few short steps, we’ve minimized our icon to the system tray, assigned a tooltip to the icon, and returned when the user clicks on the icon. All that’s left is the balloon tip.
Fortunately, when we want to display a balloon tip over a system tray icon, that functionality is pretty much automatic. All we have to do is set a few more fields in the NOTIFYICONDATA structure, telling the system what we want to display, what icon we want associated with it, and how long to keep the balloon alive.
There is one other thing we have to do though. We have to #define a value specifying that we expect certain system files to be in place on the user’s machine. In simplest terms, we need to add the following line to the top of PortScannerWindow.h:
#define _WIN32_IE 0x0501
According to the Win32 API docs, this lets the compiler know that we expect Internet Explorer 5.01, 5.5 or higher on the end user’s system. What Internet Explorer has to do with balloon tips… I’ll leave that mystery to you.
The point is, though, by #defining this value in this way, we open up new fields in the NOTIFYICONDATA structure, notably the following:
uTimeout: Length (in milliseconds) that the balloon should live
dwInfoFlags: flags specifying how the tip should be drawn.
We specify NIIF_INFO to get the info icon for our balloon.
szInfo: The info we want to display in our balloon.
szInfoTitle: The title of the balloon (appears in bold at the top of the balloon).
On dwInfoFlags: there are, of course, other icons you can specify for your balloon:
· NIIF_ERROR: An error icon. · NIIF_INFO: An information icon. · NIIF_NONE: No icon. · NIIF_WARNING: A warning icon.
And you can also ask that the pop sound not be played with the flag NIIF_NOSOUND.
Pretty easy, huh? System tray balloon tips! But we’d still like to know how to make balloon tips pop up whereever we want.
If it surprises you by now that I say “Balloon tips are windows,” give up now. We’re 15 lessons in here, already! Everything is a window!
Ballon tips are also tooltips (duh), and we’ve already done tooltips. All you need to do is specify the TTS_BALLOON style to a tooltip, and it will appear as a balloon.
// CREATE A BALLOON TIP WINDOW FOR THE IP ADDRESS EDIT
HWND hIPAddBalloonTip;
hIPAddBalloonTip = CreateWindowEx(WS_EX_TOPMOST, TOOLTIPS_CLASS, NULL,
WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP | TTS_BALLOON,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
m_hIPAddressEdit, NULL, m_hInstance, NULL);
SetWindowPos(hIPAddBalloonTip, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
// GET COORDINATES OF THE MAIN CLIENT AREA
GetClientRect (m_hIPAddressEdit, &rect);
// struct specifying info about tool in ToolTip control
TOOLINFO ti3;
strcpy(strTT, "Enter an IP Address Here");
lptstr = strTT;
// INITIALIZE MEMBERS OF THE TOOLINFO STRUCTURE
ti3.cbSize = sizeof(TOOLINFO);
ti3.uFlags = TTF_CENTERTIP | TTF_SUBCLASS;
ti3.hwnd = m_hIPAddressEdit;
ti3.hinst = m_hInstance;
ti3.uId = uid;
ti3.lpszText = lptstr;
// ToolTip control will cover the whole window
ti3.rect.left = rect.left;
ti3.rect.top = rect.top;
ti3.rect.right = rect.right;
ti3.rect.bottom = rect.bottom;
// Activate the close button tooltip
SendMessage(hIPAddBalloonTip, TTM_ADDTOOL, 0, (LPARAM) (LPTOOLINFO) &ti3);
The TTF_CENTERTIP flag given to uFlags specifies that we want our ballon to have its dialog arrow in the center of the balloon, as opposed to one of the sides.
If we want the tip to point to a specific point, we will have to send a message to the tip window alerting it of the desired position:
SendMessage(hIPAddBalloonTip, TTM_TRACKPOSITION, 0, (LPARAM) MAKELONG (0, 0));
That’s it, mission accomplished. One more thing you might want to do is handle right-click messages on the icon while it’s in the system tray (trapping WM_RBUTTONUP messages) by popping up a menu with the option to Exit the application.
Next: Working With Files.
Source Code:
win32tut_part15.zip [3.42MB zipped]
Additional Information:
· CodeProject - Minimizing Windows To The System Tray
· TheForger’s Win32 API Tutorial
Further Reading:
· Nitty Gritty Windows Programming with C++ by Henning Hansen.
· Thinking in C++ by Bruce Eckel.




[…] about? And because I felt like being nice I found you a link that shows you exactly how to do it. C++/Win32: The System Tray and Balloon Tips : Stromcode __________________ […]
Hello, i have problem.
i downloaded your source code and compiled program work.(show balloon hint)
But when i try compile its code on VC++ 2008 all is good, but Ballon hint didnt show.
and i dont know what is wrong ,
Someone can tell me ?
Great article, but to be so recent (March 2, 2008) it’s a bit out of date. All of the above is true for older OS versions. But refreshing it for Vista/Server 2008/Windows 7 would be a great help. To Kamil, I complied in VC++ 2008 and the ballon displayed just fine. You won’t get it on IE Ver. = 6.00. The following will produce some animation:
ShowWindow( hWnd, SW_MINIMIZE );
sleep( 200 ); // your own wait for 200 ms function
ShowWindow( hWnd, SW_HIDE );
In addition, lstrcpy() should be replaced with lstrcpy_s(), or better yet, _tcscpy_s().
Thanks for the great article! Keep it up Strom!
Rob Redbeard
I meant IE versions < 5.1, not 6.00.
My point is that DrawAnimatedRects() is a NOP on all new OS versions.
Rob
These are actually from 2004, if you can believe it. They used to be hosted on static pages in ‘04, then moved to PHP-Nuke, then Drupal, then mediaWiki, and finally over to this Wordpress in 2008.
They could use an update, without a doubt.
Unable to download sample code - win32tut_part15.zip. Can you send it to me? Thanks.
- rw
Unable to download sample code - win32tut_part15.zip. Can you send it to me - rwonthego@yahoo.com? Thanks.
- rw