Skip to content

Tray Icon Wrapper

A complete C++ tray-icon wrapper — create, update, and destroy a Shell_NotifyIcon entry with a right-click menu and TaskbarCreated recovery.

Published Dec 21, 1996

Everything you need for a system-tray presence in one self-contained pair of files. LoadTrayIcon registers a hidden message-only window class, creates the popup window that owns the icon, fills out the NOTIFYICONDATA (icon, tooltip, callback message), calls Shell_NotifyIcon(NIM_ADD), builds the About/Exit popup menu, and runs the message loop. The window procedure handles the two classic cases people always miss: right-click shows the menu via TrackPopupMenuEx at the cursor position, and the registered TaskbarCreated broadcast re-adds the icon after Explorer restarts. UpdateTrayCaption/UpdateTrayIcon/UpdateTray cover live changes, and DestroyTrayIcon removes it cleanly on exit.

#ifndef _TRAYCLASS_H
#define _TRAYCLASS_H
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

#define ID_ABOUT       WM_USER + 1
#define ID_EXIT        WM_USER + 2
#define WM_SHELLNOTIFY WM_USER + 5

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

extern char *sTrayClassName;
extern HINSTANCE hInstance;
extern HMENU FileMenu;     
extern HMENU MainMenu;     
extern NOTIFYICONDATA Note; 
extern POINT pt;            
extern UINT uTaskbarCreated;

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

BOOL LoadTrayIcon(WNDPROC WndProc, int iIcon, char *sCaption);
void UpdateTrayCaption(char *sCaption);
void UpdateTrayIcon(int iIcon);
void UpdateTray(int iIcon, char *sCaption);
BOOL DestroyTrayIcon(void);
LRESULT CALLBACK TrayIconProc(HWND xWindowHandle, UINT xMessage, WPARAM wParam, LPARAM lParam);

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#endif

The loader:

#include <Windows.H>
#include <ShellAPI.H>

#include "TrayIcon.H"

char *sTrayClassName = NULL;
HINSTANCE hInstance = NULL;
HMENU FileMenu = NULL;
HMENU MainMenu = NULL;
NOTIFYICONDATA Note;
POINT pt;
UINT uTaskbarCreated = -1;

BOOL LoadTrayIcon(WNDPROC WndProc, int iIcon, char *sCaption)
{
    WNDCLASS Wc;
    MSG Msg;

	if(FindWindow(sTrayClassName, NULL))
	{
		return FALSE;
	}
	
	Wc.style         = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
    Wc.lpfnWndProc   = WndProc;
    Wc.cbClsExtra    = 0;
    Wc.cbWndExtra    = 0;
    Wc.hInstance     = hInstance;
    Wc.hIcon         = LoadIcon(NULL, IDI_WINLOGO);
    Wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    Wc.hbrBackground = GetSysColorBrush(COLOR_BTNFACE);
    Wc.lpszMenuName  = NULL;
    Wc.lpszClassName = sTrayClassName;

    RegisterClass(&Wc);

    HWND MainFORM = CreateWindow(sTrayClassName, sCaption, WS_POPUP|WS_SYSMENU, 0, 0, 0, 0, NULL, (HMENU)NULL, hInstance, NULL);

    Note.cbSize           = sizeof(NOTIFYICONDATA);
    Note.hWnd             = MainFORM;
    Note.hIcon            = LoadIcon(hInstance, MAKEINTRESOURCE(iIcon) );
    Note.uFlags           = NIF_ICON | NIF_TIP | NIF_MESSAGE;
    Note.uCallbackMessage = WM_SHELLNOTIFY;
    strcpy(Note.szTip, sCaption);

    Shell_NotifyIcon(NIM_ADD, &Note);

    MainMenu = CreateMenu();
    FileMenu = CreateMenu();

    AppendMenu(FileMenu, MF_STRING, ID_ABOUT, "&About");
    AppendMenu(FileMenu, MF_STRING, ID_EXIT, "E&xit");
    InsertMenu(MainMenu, 0, MF_POPUP, (UINT)FileMenu, "invisible menu");

    while(GetMessage(&Msg, NULL, 0, 0))
    {
        if( !IsWindow(MainFORM) || !IsDialogMessage(MainFORM, &Msg) )
        {
			TranslateMessage(&Msg);
            DispatchMessage(&Msg);
        }
    }

    return TRUE;
}

void UpdateTrayCaption(char *sCaption)
{
    strcpy(Note.szTip, sCaption);
    Shell_NotifyIcon(NIM_MODIFY, &Note);
}

void UpdateTrayIcon(int iIcon)
{
	Note.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(iIcon));
    Shell_NotifyIcon(NIM_MODIFY, &Note);
}

void UpdateTray(int iIcon, char *sCaption)
{
	Note.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(iIcon));
    strcpy(Note.szTip, sCaption);
    Shell_NotifyIcon(NIM_MODIFY, &Note);
}

BOOL DestroyTrayIcon(void)
{
    return Shell_NotifyIcon(NIM_DELETE, &Note);
}

And the window procedure with the two must-have handlers:

LRESULT CALLBACK TrayIconProc(HWND hHandle, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    if(uMsg == WM_COMMAND)
    {
        if(LOWORD(wParam) == ID_ABOUT)
        {
			MessageBox(hHandle, "This is the about data.", "Tray Icon", 0);
		}

        if(LOWORD(wParam) == ID_EXIT)
        {
            DestroyWindow(hHandle);
        }

    	return TRUE;
    }

    if(uMsg == WM_CREATE)
    {
        uTaskbarCreated = RegisterWindowMessage("TaskbarCreated");
    	return TRUE;
    }

    if(uMsg == WM_SHELLNOTIFY)
    {
        if(lParam == WM_RBUTTONDOWN)
        {
            GetCursorPos(&pt);
            SetForegroundWindow(hHandle);
            TrackPopupMenuEx(FileMenu, TPM_LEFTALIGN| TPM_RIGHTBUTTON, pt.x,pt.y, hHandle, NULL);
            PostMessage(hHandle, WM_NULL, 0, 0);
        }

        if(lParam == WM_LBUTTONDOWN)
        {
			MessageBox(hHandle, "Left Click", "Tray Icon", 0);
        }

    	return TRUE;
    }

    if(uMsg == WM_DESTROY)
    {
        UnregisterClass(sTrayClassName, hInstance);
        Shell_NotifyIconA(NIM_DELETE, &Note);
		PostQuitMessage(0);
		return TRUE;
    }

    if(uMsg == WM_CLOSE)
    {
        DestroyWindow(hHandle);
    	return TRUE;
	}

    if(uMsg == uTaskbarCreated)
    {
        Shell_NotifyIcon(NIM_ADD, &Note);
    	return TRUE;
    }

    return DefWindowProc(hHandle, uMsg, wParam, lParam);
}

Details worth keeping: SetForegroundWindow before TrackPopupMenuEx so the menu dismisses correctly when the user clicks elsewhere, the WM_NULL post to flush the menu state, and the NIM_DELETE in WM_DESTROY so no ghost icon survives process exit.

An unhandled error has occurred. Reload 🗙