Creating a Complete Tray Icon Program

By: David Pankhurst

Abstract: Discussion about programming all aspects of a tray icon program - left/right mouse handling, hiding taskbar icon, and so forth.

Tray Icon programs are those apps you see with the icon near the clock, such as the volume control, or DeskMenu. While putting the icon in the try is fairly easy, the details of putting together a program that works properly is not. This article looks at those details.

Adding To The Shell

The example code that accompanies this article illustrates how to use the Windows API call Shell_NotifyIcon() to do three things:
  • Add an icon to the tray, using the NIM_ADD flag with Shell_NotifyIcon().
  • Modify the icon of tool tip, using the NIM_MODIFY flag.
  • Remove the icon from the tray with the NIM_DELETE flag.

In each case, there are two parameters to the call; the appropriate flag, and a NOTIFYICONDATA structure you fill in:

  struct NOTIFYICONDATA 
  { 
    DWORD cbSize; 
    HWND hWnd; 
    UINT uID; 
    UINT uFlags; 
    UINT uCallbackMessage; 
    HICON hIcon; 
    char szTip[64]; 
  }; 

The good news is that not all the items have to be set each time. However as a general rule, always clear out the structure with memset() for consistent results.

The first call needed is the settings required for adding an icon to a tray. The sample program has a wrapper call which illustrates this called TrayIcon_Create:
  void TForm1:: TrayIcon_Create (const char *tipText,HICON hIcon)
  {
    // add the program's icon to the tray
    NOTIFYICONDATA info;
    memset(&info,0,sizeof(info));
    info.cbSize          =sizeof(info) ;
    info.hWnd            =Handle; // form handle gets notification message
    info.uID             =WM_TRAY_ICON_ID; // id of icon - passed back in wParam of message
    info.uCallbackMessage=WM_TRAY_ICON_MESSAGE; // our notification message
    info.hIcon           =hIcon; // icon to display
    strncpy (info.szTip,tipText,sizeof(info.szTip)); // set tool tip text
    info.szTip[sizeof(info.szTip)-1]='0';
    info.uFlags          =NIF_MESSAGE | NIF_ICON | NIF_TIP ; // indicate modifications
    Shell_NotifyIcon(NIM_ADD,&info) ; // add it
  }

For flexibility, we pass in the tip text and the display icon handle. At its simplest, you could use Application->Icon->Handle, which would make the tray icon the same as your program's. Custom icons can be added by dropping a TImage on the form, placing an icon in it, and passing the {Timage}->Picture->Icon->Handle property (don't forget to turn off the image's Visible property).

Tips and Icons

Tool tip text can be quite useful. When users place the mouse over your icon, this text is displayed, allowing you to provide status, small messages, and the like. At the very least, it should display the program name. Our program starts off displaying a generic message, but we then use a timer to adjust the text.

The icon handle and tool tip text are placed in the hIcon and szTip fields. The uID is our own identification value, which must be the same for each Shell_NotifyIcon() call - here we've defined a value in the header for it. Another define is the value passed to the uCallbackMessage variable, which identifies the message sent via the tray icon.

For the proper running of a tray icon program, it needs to do more than display an icon and show a tool tip - it needs to respond to mouse messages. If you look at a typical tray icon, you'll see that left and right mouse buttons often produce actions. We need to respond to the mouse via our own message, and place a define for it in the header:
  #define WM_TRAY_ICON_MESSAGE (WM_USER+1997)

This value is also placed in the uCallbackMessage field, and of course your form (the handle passed as the hWnd param) needs to detect this new message. We do this with the message mapping macro:
  BEGIN_MESSAGE_MAP     
    MESSAGE_HANDLER(WM_TRAY_ICON_MESSAGE,TMessage,TrayIcon_HandleMessage)
  END_MESSAGE_MAP(TForm)

This will funnel our custom message to our handler routine, TrayIcon_HandleMessage().

Now to finish off the structure before calling Shell_NotifyIcon(), we need to fill in one more variable, uFlags. This contains flags indicating which fields are valid for our current call. In the case of creating our tray icon, all must be filled in and valid, so the flags are combined.

This flag is the primary difference in the four routines in the example code. It identifies which parts of the structure are valid for the current call. The other wrapper functions (TrayIcon_ModifyIcon() to change just the icon, and TrayIcon_ModifyTip() for the tool tip) implement one of these flags each, and just some of the parameters.

To round out our wrapper functions, TrayIcon_Delete() uses the NIM_DELETE flag to remove the tray icon. Obviously, this should be called before the program ends, or you'll slowly accumulate little icons without parents on the tray (expect this to happen often while debugging).

The sample program illustrates these wrapper functions by adding an icon at the start, and removing it at the end of the program's run. Buttons let you play around with the options, and are checked to lock out invalid choices (like creating two icons), but the code is simple enough that you should easily see what is going on.

Messaging

With the icon active, we can now look at the messages. Called from our message map, TrayIcon_HandleMessage() processes any tray icon mouse activity:
void TForm1::TrayIcon_HandleMessage(TMessage &Message)
{
  switch (Message.LParam)
  {
    case WM_LBUTTONDBLCLK:
      // handle left mouse button double click
      break;
    case WM_RBUTTONDOWN:
      // handle right mouse button click
      break;
    
    // ... and so on ...

    case WM_RBUTTONDBLCLK:
    case WM_MOUSEMOVE:
    case WM_LBUTTONDOWN:
      break;
  }
}

This is not your typical message loop. Since the Message parameter is always our custom message, the actual mouse message is moved to LParm, and the WParam is used for the uID value. The result is that we don't get the mouse position values from the message, and need to generate them ourselves (GetCursorPos() is ideal for this).

Trying out the various tray icons on your own computer, it's obvious how each message should be handled. As well, Microsoft specifies 'official' guidelines for icon responses:
  • When the user clicks the icon with the left mouse button, your application should display a popup with additional information.
  • When the user clicks the icon with the right mouse button, your application should display the shortcut menu.
  • When the user double-clicks the icon (left button), your application should execute the default shortcut menu command.
This means responding to the WM_LBUTTONDOWN, WM_LBUTTONDBLCLK, and WM_RBUTTONDOWN messages in your message handler. Although this is good, I offer one recommendation: you'll notice few non-Microsoft programs implement both WM_LBUTTONDOWN and WM_LBUTTONDBLCLK. The reason probably is that a WM_LBUTTONDOWN is always sent before a WM_LBUTTONDBLCLK, making it hard to know whether the left click is to be ignored because of a future double click or not. Although it can be handled (for example, setting a timer using the call GetDoubleClickTime(), and assuming a single click if no double click message arrives in that time), I find few programs that implement both, and suspect no-one will be confused if you implement just the left mouse button double-click, as the example program shows.

In contrast, the right click handler is straightforward, and should display a popup of options. In Builder we drop a TPopupMenu object on the form, set up the menu item handlers, and be done with it. Display is trivial by adding the following code to the TrayIcon_HandleMessage():
  case WM_RBUTTONDOWN:
  {
    POINT point;
    ::GetCursorPos(&point);
    PopupMenu1->Popup(point.x,point.y);
  }

Because the popup is smart enough to always position itself fully onscreen, no matter where the task bar is, we can pass the point value to it unaltered.

Finally, we deal with bringing up the program and sending it away. Hiding the main form is the key to removing its icon from the taskbar, and is so simple we don't even need to deal with minimizing and maximizing. We place a hide button on our program, and use the following code in our handler to restore the window:
  case WM_LBUTTONDBLCLK:
    if (!Visible)
      Show();
    else // already visible, so move it forward
      Application->BringToFront();
    break;

The BringToFront() call is a nicety for users - if the program is not visible, they will assume that the double-click brings it up. If it's behind another window, they may not realize that, so we use this code to make the window pop up front and center, no matter how it existed before.

All Done

Together with the other calls, the sample program implements a demonstration of the wrapper routines, including a timer to repeatedly change the tool tip (making it a clock) and cycle the icon using the TrayIcon_ModifyTip() and TrayIcon_ModifyIcon() calls. The icons are placed on the form, and are 16x16, which is the display size of the tray icons. Although you can provide 32x32 icons, you're probably better off shrinking the image down yourself, rather than settling for the operating system's interpretation of your icon.

Taken together, the code illustrates the various aspects of a complete tray icon program - setting up the icon is only the beginning of the project, but with Builder, it's very simple to manage the remainder.

Server Response from: ETNASC02