This is a short, boring Windows programming writeup with a long, boring Windows program source listing tacked onto the end. It's straight Win32 SDK programming in C, just like mother used to make.

The "system tray" in Windows is that little area on the "taskbar" where the clock lives. You usually see some other icons there, too, and you can usually right click on them and get a popup menu. How do they get there? That's our topic for the day.

A Windows program asks the system to put an icon in the tray. It provides a few bits of information when it does that:

  • A handle to an icon.

  • An arbitrary integer ID. This is passed back when you get notifications from the system tray: If you have more than one tray icon owned by the same window, you can tell them apart by giving them different IDs.

  • Some text for the "tooltip" that pops up when the user hovers the mouse over the icon in the tray.

  • A "owner" window handle that will receive notification messages when the user interacts with the tray icon.

  • A "callback message" integer that you define: This is the message ID that will be sent to the "owner" window handle. The LPARAM will specify a standard mouse message, but you won't get the usual arguments for it because the LPARAM is already being used.

It's trivial, but there's a "gotcha": If you pop up a menu when the user clicks on the icon, the program must work some special, semi-documented mojo in handling that. Otherwise, the menu will not "cancel" properly. In Windows, when a menu is showing, it should always vanish if the user clicks on something other than the menu itself. If you're showing the menu in response to a tray icon message, that doesn't happen. This is damned weird: The parent of the menu is your own window, right? What's different here? Why should it behave like that? Good question. I haven't given it much thought: I just looked up a workaround and got on with my life. A simpler workaround might be to forward yourself the tray icon message with PostMessage(), so the original notification will be out of the message queue by the time you act on it.

I'm truly sorry about the Loathsome Hungarian Cruft Notation, but Petzold uses it and a lot of Windows programmers seem to expect it, since it's such an epidemic.

//-----------------------------------------------------------------------------
//  trayicon.c
//
//  Here, we demonstrate how to put an icon in the Windows system tray and how 
//  to interact with it.
//
//  Build: Just compile it. Remember to link to user32.lib and shell32.lib
//
//  With MSVC, specifically (you DID tell the installer to set you up for 
//  using the command line compiler, RIGHT?)
//      > vcvars32
//      > cl /nologo trayicon.c user32.lib shell32.lib
//
//  by wharfinger 8/05/2001
//
//  This software is in the public domain.  
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
#include <windows.h>

//-----------------------------------------------------------------------------
#define HELP_ABOUT "\
trayicon\n\
by wharfinger\n\
\n\
This software is in the public domain.\n\
"

#define THIS_CLASSNAME      "E2 Tray Icon Demo Window"
#define THIS_TITLE          "E2 Tray Icon Demo"

//  Here we keep track of whether we're showing a message box or not.
static BOOL g_bModalState       = FALSE;


//-----------------------------------------------------------------------------
enum {
    //  Tray icon crap
    ID_TRAYICON         = 1,

    APPWM_TRAYICON      = WM_APP,
    APPWM_NOP           = WM_APP + 1,

    //  Our commands
    ID_ABOUT            = 2000,
    ID_EXIT,
};


//-----------------------------------------------------------------------------
//  Prototypes
void    AddTrayIcon( HWND hWnd, UINT uID, UINT uCallbackMsg, UINT uIcon, 
                     LPSTR pszToolTip );
void    RemoveTrayIcon( HWND hWnd, UINT uID);
void    ModifyTrayIcon( HWND hWnd, UINT uID, UINT uIcon, LPSTR pszToolTip );

HICON   LoadSmallIcon( HINSTANCE hInstance, UINT uID );

BOOL    ShowPopupMenu( HWND hWnd, POINT *curpos, int wDefaultItem );
void    OnInitMenuPopup( HWND hWnd, HMENU hMenu, UINT uID );

BOOL    OnCommand( HWND hWnd, WORD wID, HWND hCtl );

void    OnTrayIconMouseMove( HWND hWnd );
void    OnTrayIconRBtnUp( HWND hWnd );
void    OnTrayIconLBtnDblClick( HWND hWnd );

void    OnClose( HWND hWnd );

void    RegisterMainWndClass( HINSTANCE hInstance );
void    UnregisterMainWndClass( HINSTANCE hInstance );

//-----------------------------------------------------------------------------
//  Entry point
int WINAPI WinMain( HINSTANCE hInst, HINSTANCE prev, LPSTR cmdline, int show )
{
    HMENU   hSysMenu    = NULL;
    HWND    hWnd        = NULL;
    HWND    hPrev       = NULL;
    MSG     msg;

    //  Detect previous instance, and bail if there is one.  
    if ( hPrev = FindWindow( THIS_CLASSNAME, THIS_TITLE ) )
        return 0;//SendMessage( hPrev, WM_CLOSE, 0, 0 );

    //  We have to have a window, even though we never show it.  This is 
    //  because the tray icon uses window messages to send notifications to 
    //  its owner.  Starting with Windows 2000, you can make some kind of 
    //  "message target" window that just has a message queue and nothing
    //  much else, but we'll be backwardly compatible here.
    RegisterMainWndClass( hInst );

    hWnd = CreateWindow( THIS_CLASSNAME, THIS_TITLE,
                         0, 0, 0, 100, 100, NULL, NULL, hInst, NULL );

    if ( ! hWnd ) {
        MessageBox( NULL, "Ack! I can't create the window!", THIS_TITLE, 
                    MB_ICONERROR | MB_OK | MB_TOPMOST );
        return 1;
    }

    //  Message loop
    while ( GetMessage( &msg, NULL, 0, 0 ) ) {
        TranslateMessage( &msg );
        DispatchMessage( &msg );
    }

    UnregisterMainWndClass( hInst );

    return msg.wParam;
}


//-----------------------------------------------------------------------------
//  Add an icon to the system tray.
void AddTrayIcon( HWND hWnd, UINT uID, UINT uCallbackMsg, UINT uIcon, 
                  LPSTR pszToolTip )
{
    NOTIFYICONDATA  nid;

    memset( &nid, 0, sizeof( nid ) );

    nid.cbSize              = sizeof( nid );
    nid.hWnd                = hWnd;
    nid.uID                 = uID;
    nid.uFlags              = NIF_ICON | NIF_MESSAGE | NIF_TIP;
    nid.uCallbackMessage    = uCallbackMsg;
    //  Uncomment this if you've got your own icon.  GetModuleHandle( NULL )
    //  gives us our HINSTANCE.  I hate globals.
//  nid.hIcon               = LoadSmallIcon( GetModuleHandle( NULL ), uIcon );

    //  Comment this if you've got your own icon.
    {
    char    szIconFile[512];

    GetSystemDirectory( szIconFile, sizeof( szIconFile ) );
    if ( szIconFile[ strlen( szIconFile ) - 1 ] != '\\' )
        strcat( szIconFile, "\\" );
    strcat( szIconFile, "shell32.dll" );
    //  Icon #23 (0-indexed) in shell32.dll is a "help" icon.
    ExtractIconEx( szIconFile, 23, NULL, &(nid.hIcon), 1 );
    }

    strcpy( nid.szTip, pszToolTip );

    Shell_NotifyIcon( NIM_ADD, &nid );
}


//-----------------------------------------------------------------------------
void ModifyTrayIcon( HWND hWnd, UINT uID, UINT uIcon, LPSTR pszToolTip )
{
    NOTIFYICONDATA  nid;

    memset( &nid, 0, sizeof( nid ) );

    nid.cbSize  = sizeof( nid );
    nid.hWnd    = hWnd;
    nid.uID     = uID;

    if ( uIcon != (UINT)-1 ) {
        nid.hIcon   = LoadSmallIcon( GetModuleHandle( NULL ), uIcon );
        nid.uFlags  |= NIF_ICON;
    }

    if ( pszToolTip ) {
        strcpy( nid.szTip, pszToolTip );
        nid.uFlags  |= NIF_TIP;
    }

    if ( uIcon != (UINT)-1 || pszToolTip )
        Shell_NotifyIcon( NIM_MODIFY, &nid );
}


//-----------------------------------------------------------------------------
//  Remove an icon from the system tray.
void RemoveTrayIcon( HWND hWnd, UINT uID )
{
    NOTIFYICONDATA  nid;

    memset( &nid, 0, sizeof( nid ) );

    nid.cbSize  = sizeof( nid );
    nid.hWnd    = hWnd;
    nid.uID     = uID;

    Shell_NotifyIcon( NIM_DELETE, &nid );
}


//-----------------------------------------------------------------------------
//  OUR NERVE CENTER.
static LRESULT CALLBACK WindowProc( HWND hWnd, UINT uMsg, WPARAM wParam, 
                                    LPARAM lParam )
{
    switch ( uMsg ) {
        case WM_CREATE:
            AddTrayIcon( hWnd, ID_TRAYICON, APPWM_TRAYICON, 0, THIS_TITLE );
            return 0;

        case APPWM_NOP:
            //  There's a long comment in OnTrayIconRBtnUp() which explains 
            //  what we're doing here.
            return 0;

        //  This is the message which brings tidings of mouse events involving 
        //  our tray icon.  We defined it ourselves.  See AddTrayIcon() for 
        //  details of how we told Windows about it.
        case APPWM_TRAYICON:
            SetForegroundWindow( hWnd );

            switch ( lParam ) {
                case WM_MOUSEMOVE:
                    OnTrayIconMouseMove( hWnd );
                    return 0;

                case WM_RBUTTONUP:
                    //  There's a long comment in OnTrayIconRBtnUp() which 
                    //  explains what we're doing here.
                    OnTrayIconRBtnUp( hWnd );
                    return 0;

                case WM_LBUTTONDBLCLK:
                    OnTrayIconLBtnDblClick( hWnd );
                    return 0;
            }
            return 0;

        case WM_COMMAND:
            return OnCommand( hWnd, LOWORD(wParam), (HWND)lParam );

        case WM_INITMENUPOPUP:
            OnInitMenuPopup( hWnd, (HMENU)wParam, lParam );
            return 0;

        case WM_CLOSE:
            OnClose( hWnd );
            return DefWindowProc( hWnd, uMsg, wParam, lParam );

        default:
            return DefWindowProc( hWnd, uMsg, wParam, lParam );
    }
}


//-----------------------------------------------------------------------------
void OnClose( HWND hWnd )
{
    //  Remove icon from system tray.
    RemoveTrayIcon( hWnd, ID_TRAYICON );

    PostQuitMessage( 0 );
}


//-----------------------------------------------------------------------------
//  Create and display our little popupmenu when the user right-clicks on the 
//  system tray.
BOOL ShowPopupMenu( HWND hWnd, POINT *curpos, int wDefaultItem )
{
    HMENU   hPop        = NULL;
    int     i           = 0;
    WORD    cmd;
    POINT   pt;

    if ( g_bModalState )
        return FALSE;

    hPop = CreatePopupMenu();

    if ( ! curpos ) {
        GetCursorPos( &pt );
        curpos = &pt;
    }

    InsertMenu( hPop, i++, MF_BYPOSITION | MF_STRING, ID_ABOUT, "About..." );
    InsertMenu( hPop, i++, MF_BYPOSITION | MF_STRING, ID_EXIT, "Exit" );

    SetMenuDefaultItem( hPop, ID_ABOUT, FALSE );

    SetFocus( hWnd );

    SendMessage( hWnd, WM_INITMENUPOPUP, (WPARAM)hPop, 0 );

    cmd = TrackPopupMenu( hPop, TPM_LEFTALIGN | TPM_RIGHTBUTTON 
                            | TPM_RETURNCMD | TPM_NONOTIFY,
                          curpos->x, curpos->y, 0, hWnd, NULL );

    SendMessage( hWnd, WM_COMMAND, cmd, 0 );

    DestroyMenu( hPop );

    return cmd;
}


//-----------------------------------------------------------------------------
BOOL OnCommand( HWND hWnd, WORD wID, HWND hCtl )
{
    if ( g_bModalState )
        return 1;

    //  Have a look at the command and act accordingly
    switch ( wID ) {
        case ID_ABOUT:
            g_bModalState = TRUE;
            MessageBox( hWnd, HELP_ABOUT, THIS_TITLE, 
                        MB_ICONINFORMATION | MB_OK );
            g_bModalState = FALSE;
            return 0;

        case ID_EXIT:
            PostMessage( hWnd, WM_CLOSE, 0, 0 );
            return 0;
    }

}


//-----------------------------------------------------------------------------
//  When the mouse pointer drifts over the tray icon, a "tooltip" will be 
//  displayed.  Before that happens, we get notified about the movement, so we 
//  get a chance to set the "tooltip" text to be something useful.  
void OnTrayIconMouseMove( HWND hWnd )
{
    //  stub
}


//-----------------------------------------------------------------------------
//  Right-click on tray icon displays menu.
void OnTrayIconRBtnUp( HWND hWnd )
{
    /*
    This SetForegroundWindow() and PostMessage( , APPWM_NOP, , ) are 
    recommended in some MSDN sample code; apparently there's a bug in most if 
    not all versions of Windows: Tray icon menus don't vanish properly when 
    cancelled unless you provide special coddling.  

    In MSDN, see: "PRB: Menus for Notification Icons Don't Work Correctly", 
    Q135788:

        mk:@ivt:kb/Source/win32sdk/q135788.htm

    Example code:

        mk:@ivt:pdref/good/code/graphics/gdi/setdisp/c3447_7lbt.htm

    Both of these pseudo-URL's are from the July 1998 MSDN.  Those geniuses 
    have since completely re-broken MSDN at least once, so the pseudo-URL's 
    are useless with more recent MSDN's.

    In the April 2000 MSDN, you can search titles for "Menus for 
    Notification Icons"; "Don't" in the title has been changed to "Do Not", 
    and searching for either complete title doesn't work anyway.  Good old 
    MSDN.
    */

    SetForegroundWindow( hWnd );

    ShowPopupMenu( hWnd, NULL, -1 );

    PostMessage( hWnd, APPWM_NOP, 0, 0 );
}


//-----------------------------------------------------------------------------
void OnTrayIconLBtnDblClick( HWND hWnd )
{
    SendMessage( hWnd, WM_COMMAND, ID_ABOUT, 0 );
}


//-----------------------------------------------------------------------------
void OnInitMenuPopup( HWND hWnd, HMENU hPop, UINT uID )
{
    //  stub
}


//-----------------------------------------------------------------------------
void RegisterMainWndClass( HINSTANCE hInstance )
{
    WNDCLASSEX wclx;
    memset( &wclx, 0, sizeof( wclx ) );

    wclx.cbSize         = sizeof( wclx );
    wclx.style          = 0;
    wclx.lpfnWndProc    = &WindowProc;
    wclx.cbClsExtra     = 0;
    wclx.cbWndExtra     = 0;
    wclx.hInstance      = hInstance;
    //wclx.hIcon        = LoadIcon( hInstance, MAKEINTRESOURCE( IDI_TRAYICON ) );
    //wclx.hIconSm      = LoadSmallIcon( hInstance, IDI_TRAYICON );
    wclx.hCursor        = LoadCursor( NULL, IDC_ARROW );
    wclx.hbrBackground  = (HBRUSH)( COLOR_BTNFACE + 1 );    //  COLOR_* + 1 is
                                                            //  special magic.
    wclx.lpszMenuName   = NULL;
    wclx.lpszClassName  = THIS_CLASSNAME;

    RegisterClassEx( &wclx );
}


//-----------------------------------------------------------------------------
void UnregisterMainWndClass( HINSTANCE hInstance )
{
    UnregisterClass( THIS_CLASSNAME, hInstance );
}


//-----------------------------------------------------------------------------
HICON LoadSmallIcon( HINSTANCE hInstance, UINT uID )
{
    return LoadImage( hInstance, MAKEINTRESOURCE( uID ), IMAGE_ICON, 
                      16, 16, 0 );
}


//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------