Tutorial 23: Tray Icon

In this tutorial, we will learn how to put icons into system tray and how to create/use a popup menu.

Theory:

System tray is the rectangular region in the taskbar where several icons reside. Normally, you'll see at least a digital clock in it. You can also put icons in the system tray too. Below are the steps you have to perform to put an icon into the system tray:
  1. Fill a NOTIFYICONDATA structure which has the following members:
  2. Call Shell_NotifyIcon which is defined in shell32.inc. This function has the following prototype:

  3.  

     

                Shell_NotifyIcon PROTO dwMessage:DWORD ,pnid:DWORD

        dwMessage  is the type of message to send to the shell.
              NIM_ADD Adds an icon to the status area.
             NIM_DELETE Deletes an icon from the status area.
             NIM_MODIFY Modifies an icon in the status area.
        pnid  is the pointer to a NOTIFYICONDATA structure filled with proper values
    If you want to add an icon to the tray, use NIM_ADD message, if you want to remove the icon, use NIM_DELETE.

That's all there is to it. But most of the time, you're not content in just putting an icon there. You need to be able to respond to the mouse events over the tray icon. You can do this by processing the message you specified in uCallbackMessage member of NOTIFYICONDATA structure. This message has the following values in wParam and lParam (special thanks to s__d for the info): Most tray icon, however, displays a popup menu when the user right-click on it. We can implement this feature by creating a popup menu and then call TrackPopupMenu to display it. The steps are described below:
  1. Create a popup menu by calling CreatePopupMenu. This function creates an empty menu. It returns the menu handle in eax if successful.
  2. Add menu items to it with AppendMenu, InsertMenu or InsertMenuItem.
  3. When you want to display the popup menu where the mouse cursor is, call GetCursorPos to obtain the screen coordinate of the cursor and then call TrackPopupMenu to display the menu. When the user selects a menu item from the popup menu, Windows sends WM_COMMAND message to your window procedure just like normal menu selection.
Note: Beware of two annoying behaviors when you use a popup menu with a tray icon:
  1. When the popup menu is displayed, if you click anywhere outside the menu, the popup menu will not disappear immediately as it should be. This behavior occurs because the window that will receive the notifications from the popup menu MUST be the foreground window. Just call SetForegroundWindow will correct it.
  2. After calling SetForegroundWindow, you will find that the first time the popup menu is displayed, it works ok but on the subsequent times, the popup menu will show up and close immediately. This behavior is "intentional", to quote from MSDN. The task switch to the program that is the owner of the tray icon in the near future is necessary. You can force this task switch by posting any message to the window of the program. Just use PostMessage, not SendMessage!

Example:

___________________________________________________________________________________________

; Equates:

[IDI_TRAY 0  IDM_RESTORE 1000 IDM_EXIT 1010]

[WM_SHELLNOTIFY 0405]  ; WM_USER+5
____________________________________________________________________________________________
 

; Data:

[ClassName:     'TrayIconWinClass' 0
 AppName:       'TrayIcon Demo' 0
 RestoreString: '&Restore' 0
 ExitString:    'E&xit Program' 0]
 

[hPopupMenu: 0  WindowHandle: 0]

[WindowClass:
 style: &CS_HREDRAW+&CS_VREDRAW+&CS_DBLCLKS
 lpfnWndProc: MainWindowProc    cbClsExtra: 0  cbWndExtra: 0
 hInstance: 0  hIcon: 0  hCursor: 0  hbrBackground: &COLOR_APPWORKSPACE
 lpszMenuName: 0  lpszClassName: ClassName]
 
[FirstMessage: 0 #7]

____________________________________________________________________________________________
____________________________________________________________________________________________

Main:

    call 'Kernel32.GetModuleHandleA' 0 | mov D§hInstance eax

    call 'User32.LoadIconA'  0  &IDI_APPLICATION | mov D§hIcon eax
 
    call 'User32.LoadCursorA' 0  &IDC_ARROW | mov D§hCursor eax

    call 'User32.RegisterClassA'  WindowClass
 
    call 'User32.CreateWindowExA' &WS_EX_CLIENTEDGE ClassName AppName,
                                 &WS_OVERLAPPEDWINDOW+&WS_VISIBLE,
                                 &CW_USEDEFAULT &CW_USEDEFAULT 350 200,
                                 0  D§hInstance  0
      mov D§WindowHandle eax, D§NID_hwnd eax
 
    call 'User32.ShowWindow'  D§WindowHandle &SW_SHOW
    call 'User32.UpdateWindow'  D§WindowHandle

    add D§NID_cbSize 64
 
L1: call 'User32.GetMessageA' FirstMessage 0 0 0 | cmp eax 0 | je L9>
        call 'User32.TranslateMessage'  FirstMessage
        call 'User32.DispatchMessageA'  FirstMessage
    jmp L1<

L9: call 'Kernel32.ExitProcess' 0

_____________________________________________________________________________________

[POINT:  Point_X: 0  Point_Y: 0]

[RECT:  Rect_Left: 0  Rect_Top: 0  Rect_Right: 0  Rect_Bottom: 0]

[NOTIFYICONDATA:
 NID_cbSize: len
 NID_hwnd: 0
 NID_uID: IDI_TRAY
 NID_uFlags: &NIF_ICON+&NIF_MESSAGE+&NIF_TIP
 NID_uCallbackMessage: WM_SHELLNOTIFY
 NID_hIcon: 0] [NID_szTip: B§ 0 #64]
____________________________________________________________________________________________
____________________________________________________________________________________________

Proc MainWindowProc:
    Arguments @Adressee, @Message, @wParam, @lParam

    pushad

    .If D@Message = &WM_CREATE
        call 'USER32.CreatePopupMenu'
            mov D§hPopupMenu eax
            call 'USER32.AppendMenuA' D§hPopupMenu &MF_STRING IDM_RESTORE  RestoreString
            call 'USER32.AppendMenuA' D§hPopupMenu &MF_STRING IDM_EXIT  ExitString
    .Else_If D@Message = &WM_DESTROY
        call 'USER32.DestroyMenu' D§hPopupMenu
        call 'USER32.PostQuitMessage' &NULL
    .Else_If D@Message = &WM_SIZE
        ..If D@wParam = &SIZE_MINIMIZED
            move D§NID_hwnd D@Adressee
            call 'User32.LoadIconA' &NULL &IDI_WINLOGO
            mov D§NID_hIcon eax, esi AppName, edi NID_szTip
            While B§esi a 0
                movsb
            End_While
 
            call 'User32.ShowWindow' D@Adressee &SW_HIDE
            call 'Shell32.Shell_NotifyIconA' &NIM_ADD  NOTIFYICONDATA
 
        ..End_If
    .Else_If D@Message = &WM_COMMAND
        ..If D@lParam = 0
            call 'Shell32.Shell_NotifyIconA' &NIM_DELETE  NOTIFYICONDATA
            mov eax D@wParam
            If ax = IDM_RESTORE
                call 'User32.ShowWindow' D@Adressee &SW_RESTORE
            Else
                call 'User32.DestroyWindow' D@Adressee
            End_If
       ..End_If
    .Else_If D@Message = WM_SHELLNOTIFY
       ..If D@wParam = IDI_TRAY
            If D@lParam = &WM_RBUTTONDOWN
                call 'User32.GetCursorPos' POINT
                call 'User32.SetForegroundWindow' D@Adressee
                call 'User32.TrackPopupMenu' D§hPopupMenu &TPM_RIGHTALIGN+&TPM_RIGHTBUTTON,
                                             D§Point_X  D§Point_Y  &NULL  D@Adressee &NULL
                call 'User32.PostMessageA' D@Adressee &NULL 0 0
            Else_If D@lParam = &WM_LBUTTONDBLCLK
                call 'User32.SendMessageA' D@Adressee &WM_COMMAND IDM_RESTORE 0
            End_If
       ..End_If
    .Else
        popad
        call 'User32.DefWindowProcA' D@Adressee D@Message D@Wparam D@Lparam
        Exit
 
    .End_If
 
    popad | mov eax &FALSE
EndP
 

Analysis:

The program will display a simple window. When you press the minimize button, it will hide itself and put an icon into the system tray. When you double-click on the icon, the program will restore itself and remove the icon from the system tray. When you right-click on it, a popup menu is displayed. You can choose to restore the program or exit it.
 

    .If D@Message = &WM_CREATE
        call 'USER32.CreatePopupMenu'
            mov D§hPopupMenu eax
            call 'USER32.AppendMenuA' D§hPopupMenu &MF_STRING IDM_RESTORE  RestoreString
            call 'USER32.AppendMenuA' D§hPopupMenu &MF_STRING IDM_EXIT  ExitString

When the main window is created, it creates a popup menu and append two menu items. AppendMenu has the following syntax:
 

AppendMenu PROTO hMenu:DWORD, uFlags:DWORD, uIDNewItem:DWORD, lpNewItem:DWORD
 
After the popup menu is created, the main window waits patiently for the user to press minimize button.
When a window is minimized, it receives WM_SIZE message with SIZE_MINIMIZED value in wParam.
 

    .Else_If D@Message = &WM_SIZE
        ..If D@wParam = &SIZE_MINIMIZED
            move D§NID_hwnd D@Adressee
            call 'User32.LoadIconA' &NULL &IDI_WINLOGO
            mov D§NID_hIcon eax, esi AppName, edi NID_szTip
            While B§esi a 0
                movsb
            End_While
 
            call 'User32.ShowWindow' D@Adressee &SW_HIDE
            call 'Shell32.Shell_NotifyIconA' &NIM_ADD  NOTIFYICONDATA
 
        ..End_If

We use this opportunity to fill NOTIFYICONDATA structure. IDI_TRAY is just a constant defined at the beginning of the source code. You can set it to any value you like. It's not important because you have only one tray icon. But if you will put several icons into the system tray, you need unique IDs for each tray icon. We specify all flags in uFlags member because we specify an icon (NIF_ICON), we specify a custom message (NIF_MESSAGE) and we specify the tooltip text (NIF_TIP). WM_SHELLNOTIFY is just a custom message defined as WM_USER+5. The actual value is not important so long as it's unique. I use the winlogo icon as the tray icon here but you can use any icon in your program. Just load it from the resource with LoadIcon and put the returned handle in hIcon member. Lastly, we fill the szTip with the text we want the shell to display when the mouse is over the icon.
We hide the main window to give the illusion of "minimizing-to-tray-icon" appearance.
Next we call Shell_NotifyIcon  with NIM_ADD message to add the icon to the system tray.

Now our main window is hidden and the icon is in the system tray. If you move the mouse over it, you will see a tooltip that displays the text we put into szTip member. Next, if you double-click at the icon, the main window will reappear and the tray icon is gone.
 

    .Else_If D@Message = WM_SHELLNOTIFY
       ..If D@wParam = IDI_TRAY
            If D@lParam = &WM_RBUTTONDOWN
                call 'User32.GetCursorPos' POINT
                call 'User32.SetForegroundWindow' D@Adressee
                call 'User32.TrackPopupMenu' D§hPopupMenu &TPM_RIGHTALIGN+&TPM_RIGHTBUTTON,
                                             D§Point_X  D§Point_Y  &NULL  D@Adressee &NULL
                call 'User32.PostMessageA' D@Adressee &NULL 0 0
            Else_If D@lParam = &WM_LBUTTONDBLCLK
                call 'User32.SendMessageA' D@Adressee &WM_COMMAND IDM_RESTORE 0
            End_If
       ..End_If

When a mouse event occurs over the tray icon, your window receives WM_SHELLNOTIFY message which is the custom message you specified in uCallbackMessage member. Recall that on receiving this message, wParam contains the tray icon's ID and lParam contains the actual mouse message. In the code above, we check first if this message comes from the tray icon we are interested in. If it does, we check the actual mouse message. Since we are only interested in right mouse click and double-left-click, we process only WM_RBUTTONDOWN and WM_LBUTTONDBLCLK messages.
If the mouse message is WM_RBUTTONDOWN, we call GetCursorPos to obtain the current screen coordinate of the mouse cursor. When the function returns, the POINT structure is filled with the screen coordinate of the mouse cursor. By screen coordinate, I mean the coordinate of the entire screen without regarding to any window boundary. For example, if the screen resolution is 640*480, the right-lower corner of the screen is x==639 and y==479. If you want to convert the screen coordinate to window coordinate, use ScreenToClient function.
However, for our purpose, we want to display the popup menu at the current mouse cursor position with TrackPopupMenu call and it requires screen coordinates, we can use the coordinates filled by GetCursorPos directly.
TrackPopupMenu has the following syntax:
 

When the user double-clicks at the tray icon, we send WM_COMMAND message to our own window specifying IDM_RESTORE to emulate the user selects Restore menu item in the popup menu thereby restoring the main window and removing the icon from the system tray. In order to be able to receive double click message, the main window must have CS_DBLCLKS style.
 
 

            call 'Shell32.Shell_NotifyIconA' &NIM_DELETE  NOTIFYICONDATA
            mov eax D@wParam
            If ax = IDM_RESTORE
                call 'User32.ShowWindow' D@Adressee &SW_RESTORE
            Else
                call 'User32.DestroyWindow' D@Adressee
            End_If

When the user selects Restore menu item, we remove the tray icon by calling Shell_NotifyIcon again, this time we specify NIM_DELETE as the message. Next, we restore the main window to its original state. If the user selects Exit menu item, we also remove the icon from the tray and destroy the main window by calling DestroyWindow.


[Iczelion's Win32 Assembly Homepage]