Tutorial 22: SuperClassing Tutorial 23: Tray Icon Tutorial 24: Windows Hooks

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

Download the example here.

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:
    • cbsize The size of this structure.
    • hwnd Handle of the window that will receive notification when a mouse event occurs over the tray icon.
    • uid A constant that is used as the icon's identifier. You are the one who decides on this value. In case you have more than one tray icons, you will be able to check from what tray icon the mouse notification is from.
    • uflags Specify which members of this structure are valid.
      • nif_icon The hIcon member is valid.
      • nif_message The uCallbackMessage member is valid.
      • nif_tip The szTip member is valid.
    • ucallbackmessage The custom message that Windows will send to the window specified by the hwnd member when mouse events occur over the tray icon. You create this message yourself.
    • hicon The handle of the icon you want to put into the system tray.
    • sztip A 64-byte array that contains the string that will be used as the tooltip text when the mouse hovers over the tray icon.
  2. Call shell_notifyicon which is defined in shell32.inc. This function has the following prototype: 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):
  • wParam contains the ID of the icon. This is the same value you put into uID member of NOTIFYICONDATA structure.
  • lParam The low word contains the mouse message. For example, if the user right-clicked at the icon, lParam will contain WM_RBUTTONDOWN.
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:

.386 .model FLAT,STDCALL OPTION casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\include\shell32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\shell32.lib WM_SHELLNOTIFY equ WM_USER+5 IDI_TRAY equ 0 IDM_RESTORE equ 1000 IDM_EXIT equ 1010 WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD .data ClassName db "TrayIconWinClass",0 AppName db "TrayIcon Demo",0 RestoreString db "&;Restore",0 ExitString db "E&;xit Program",0 .data? hInstance dd ? note NOTIFYICONDATA <;> hPopupMenu dd ? .code start: invoke GetModuleHandle, NULL mov hInstance,eax invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT invoke ExitProcess,eax WinMain PROC hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR, CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:HWND mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW or CS_DBLCLKS mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_APPWORKSPACE mov wc.lpszMenuName,NULL mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, ADDR wc invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName, \ ADDR AppName, \ WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU \ +WS_MINIMIZEBOX+WS_MAXIMIZEBOX \ +WS_VISIBLE, \ CW_USEDEFAULT,CW_USEDEFAULT, \ 350,200, \ NULL,NULL,hInst,NULL mov hwnd,eax .WHILE TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .ENDW mov eax,msg.wParam ret WinMain ENDP WndProc PROC hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL pt:POINT .IF uMsg==WM_CREATE invoke CreatePopupMenu mov hPopupMenu,eax invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE, \ ADDR RestoreString invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,ADDR ExitString .ELSEIF uMsg==WM_DESTROY invoke DestroyMenu,hPopupMenu invoke PostQuitMessage,NULL .ELSEIF uMsg==WM_SIZE .IF wParam==SIZE_MINIMIZED mov note.cbSize,sizeof NOTIFYICONDATA push hWnd pop note.hwnd mov note.uID,IDI_TRAY mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP mov note.uCallbackMessage,WM_SHELLNOTIFY invoke LoadIcon,NULL,IDI_WINLOGO mov note.hIcon,eax invoke lstrcpy,ADDR note.szTip,ADDR AppName invoke ShowWindow,hWnd,SW_HIDE invoke Shell_NotifyIcon,NIM_ADD,ADDR note .ENDIF .ELSEIF uMsg==WM_COMMAND .IF lParam==0 invoke Shell_NotifyIcon,NIM_DELETE,ADDR note mov eax,wParam .IF ax==IDM_RESTORE invoke ShowWindow,hWnd,SW_RESTORE .ELSE invoke DestroyWindow,hWnd .ENDIF .ENDIF .ELSEIF uMsg==WM_SHELLNOTIFY .IF wParam==IDI_TRAY .IF lParam==WM_RBUTTONDOWN invoke GetCursorPos,ADDR pt invoke SetForegroundWindow,hWnd invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y, \ NULL,hWnd,NULL invoke PostMessage,hWnd,WM_NULL,0,0 .ELSEIF lParam==WM_LBUTTONDBLCLK invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0 .ENDIF .ENDIF .ELSE invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .ENDIF xor eax,eax ret WndProc ENDP END start

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 uMsg==WM_CREATE invoke CreatePopupMenu mov hPopupMenu,eax invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE, \ ADDR RestoreString invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT, \ ADDR 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
  • hMenu is the handle of the menu you want to append the item to.
  • uFlags tells Windows about the menu item to be appended to the menu whether it is a bitmap or a string or an owner-draw item, enabled, grayed or disable etc. You can get the complete list from win32 api reference. In our example, we use MF_STRING which means the menu item is a string.
  • uIDNewItem is the ID of the menu item. This is a user-defined value that is used to represent the menu item.
  • lpNewItem specifies the content of the menu item, depending on what you specify in uFlags member. Since we specify MF_STRING in uFlags member, lpNewItem must contain the pointer to the string to be displayed in the popup menu.

After the popup menu is created, the main window waits patiently for the user to press minimize button.
Whena window is minimized, it receives WM_SIZE message with SIZE_MINIMIZED value in wParam. .ELSEIF uMsg==WM_SIZE .IF wParam==SIZE_MINIMIZED mov note.cbSize,sizeof NOTIFYICONDATA push hWnd pop note.hwnd mov note.uID,IDI_TRAY mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP mov note.uCallbackMessage,WM_SHELLNOTIFY invoke LoadIcon,NULL,IDI_WINLOGO mov note.hIcon,eax invoke lstrcpy,ADDR note.szTip,ADDR AppName invoke ShowWindow,hWnd,SW_HIDE invoke Shell_NotifyIcon,NIM_ADD,ADDR note .ENDIF

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. .ELSEIF uMsg==WM_SHELLNOTIFY .IF wParam==IDI_TRAY .IF lParam==WM_RBUTTONDOWN invoke GetCursorPos,ADDR pt invoke SetForegroundWindow,hWnd invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y, \ NULL,hWnd,NULL invoke PostMessage,hWnd,WM_NULL,0,0 .ELSEIF lParam==WM_LBUTTONDBLCLK invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0 .ENDIF .ENDIF

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: TrackPopupMenu PROTO hMenu:DWORD, uFlags:DWORD, x:DWORD, y:DWORD, \ nReserved:DWORD, hWnd:DWORD, prcRect:DWORD
  • hMenu is the handle of the popup menu to be displayed.
  • uFlags specifies the options of the function. Like where to position the menu relative to the coordinates specified later and which mouse button will be used to track the menu. In our example, we use TPM_RIGHTALIGN to position the popup menu to the left of the coordinates.
  • x and
    y specify the location of the menu in screen coordinates.
  • nReserved must be NULL
  • hWnd is the handle of the window that will receive the messages from the menu.
  • is the rectangle in the screen where it is possible to click without dismissing the menu. Normally we put NULL here so when the user clicks anywhere outside the popup menu, the menu is dismissed.

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.

invoke Shell_NotifyIcon,NIM_DELETE,ADDR note mov eax,wParam .IF ax==IDM_RESTORE invoke ShowWindow,hWnd,SW_RESTORE .ELSE invoke DestroyWindow,hWnd .ENDIF

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.


Tutorial 22: SuperClassing Overview Tutorial 24: Windows Hooks
Software » wiki » Assembler » X86 » icz » uk » tute023.html