Page 1 of 1

MASM - Displaying a Tray Icon

#1 GunnerInc  Icon User is offline

  • "Hurry up and wait"
  • member icon




Reputation: 858
  • View blog
  • Posts: 2,282
  • Joined: 28-March 11

Posted 24 December 2011 - 06:57 PM

Here I will show how to add an icon to the tray notification area of the taskbar that will display a popup menu when right clicked. The popup menu will have 3 entries:
1. Open the new topics page of Dream In Code in your default webbrowser.
2. Start traffic lights - will cycle through red, yellow and green icons and change the menu to stop traffic lights.
3. Exit
We will create a window, not a "normal" window but a "Message only" window since this sample does not need a window.

Windows 7 (and maybe above and vista) might not show the tray icon, click the up arrow in the tray to show hidden icons and choose customize. Then choose Tray Icon.exe (This app) and select show icons and notifications.

Let's begin! I don't know about you, but I do not want a new icon to show up in the tray each time I "run" my app, so we need a way to see if our app is already running and if it is, don't allow the new app to start. Here is how I do it, You can also use FindWindow or CreateSemaphore.
TrayIcon:
    invoke  CreateMutex, NULL, FALSE, offset szMutex
    mov     hMutex, eax
    
    invoke  GetLastError
    cmp     eax, ERROR_ALREADY_EXISTS
    jne     GoAhead
    invoke  MessageBox, HWND_DESKTOP, offset szRunning, NULL, MB_ICONEXCLAMATION
    jmp     Done
    
GoAhead:
    call    StartUp
    
Done:
    invoke  CloseHandle, hMutex
    invoke  ExitProcess, eax

First thing we do is create a "global" mutex in the 1,2Kernel Object Namespace. CreateMutex always returns a handle to a mutex object even if it already exists. If it exists, GetLastError will return ERROR_ALREADY_EXISTS we check for that and if we get it, we close the Mutex handle that was returned and exit the app, otherwise we continue.
StartUp proc
LOCAL   msg:MSG
LOCAL   wc:WNDCLASSEX

    invoke  memfill, addr wc, sizeof WNDCLASSEX, 0
    invoke  GetModuleHandle, NULL
    mov     hInst, eax
    
    mov     wc.cbSize, sizeof WNDCLASSEX
    mov     wc.hInstance, eax
    mov     wc.lpszClassName, offset szClsTrayTut
    mov     wc.lpfnWndProc, offset WndProc
    invoke  RegisterClassEx, addr wc
    
    xor     ecx, ecx
    invoke  CreateWindowEx, ecx, offset szClsTrayTut, ecx, ecx, ecx, ecx, ecx, ecx, HWND_MESSAGE, ecx, hInst, ecx
   
    .while TRUE
        invoke  GetMessage, addr msg, NULL, 0, 0
        .break .if !eax
        invoke  TranslateMessage, addr msg
        invoke  DispatchMessage, addr msg
    .endw
    mov     eax, msg.message       
    ret
StartUp endp

Ok, so we didn't find our mutex now we have to register a window class to dispatch messages. For this example, we don't want a normal window but a 3"message only window" so I am only interested in filling in 4 members of the WNDCLASSEX structure - cbSize, lpfnWndProc, lpszClassName and hInstance. To make it easier I "zero" out the structure first with memfill.

Next we create our window to dispatch any messages windows will send to our app. Since we don't want a "normal" window all parameters except lpClassName, hWndParent, and hInstance are zero. Since it is a smaller opcode encoding to push a register, I first zero out ecx then "push" ecx instead of zero.
Once the window is created, we enter our message loop to dispatch all messages.

Now, the magic happens here in our Window Proc:
WndProc proc uses esi edi hWin:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
LOCAL   pt:POINT

    mov		eax,uMsg
    .if eax==WM_CREATE
        mov     esi, hInst
        mov     dwCurrentIcon, ICON_GREEN
        mov     dwTimerRunning, 0

        invoke  LoadImage, esi, ICON_RED, IMAGE_ICON, 0, 0, NULL
        mov     hRed, eax

        invoke  LoadImage, esi, ICON_YELLOW, IMAGE_ICON, 0, 0, NULL
        mov     hYellow, eax

        invoke  LoadImage, esi, ICON_GREEN, IMAGE_ICON, 0, 0, NULL
        mov     hGreen, eax

        invoke  LoadImage, esi, ICON_DIC, IMAGE_ICON, 0, 0, NULL
        mov     hDiC, eax

        invoke  CreatePopupMenu
        mov     hPopupMenu, eax
        mov     edi, eax
        invoke  AppendMenu, eax, MF_STRING, IDM_DIC_NEW, offset szDic
        invoke  AppendMenu, edi, MF_STRING, IDM_START, offset szStart
        invoke  AppendMenu, edi, MF_STRING, IDM_EXIT, offset szExit
        
        mov     nid.cbSize, sizeof NOTIFYICONDATA
        push    hWin
        pop     nid.hwnd
        mov     nid.uID, 0
        mov     nid.uFlags, NIF_ICON or NIF_MESSAGE or NIF_TIP
        mov     nid.uCallbackMessage, WM_SHELLNOTIFY
        push    hDiC
        pop     nid.hIcon
        invoke  lstrcpy, offset nid.szTip, offset szAppName
        invoke  Shell_NotifyIcon, NIM_ADD, offset nid
        
    .elseif eax==WM_COMMAND
        mov		edx,wParam
        movzx	eax,dx
        shr		edx,16
        .if edx==BN_CLICKED
            .if eax == IDM_DIC_NEW
                invoke	ShellExecute, NULL, offset szOpen, offset szDiCNewTopics, NULL, NULL, SW_SHOWNORMAL or SW_RESTORE
                .if dwTimerRunning == TRUE
                    mov     ax, BN_CLICKED
                    ror     eax, 16
                    mov     ax, IDM_STOP
                    invoke  SendMessage, hWin, WM_COMMAND, eax, NULL           
                    push    hDiC
                    pop     nid.hIcon
                    invoke  Shell_NotifyIcon, NIM_MODIFY, offset nid
                .endif
                   
            .elseif eax == IDM_START
                invoke  ModifyMenu, hPopupMenu, 1, MF_BYPOSITION, IDM_STOP, offset szStop
                invoke  SetTimer, hWin, ID_TIMER, 500, NULL
                mov     dwTimerRunning, TRUE
                
            .elseif eax == IDM_STOP
                invoke  ModifyMenu, hPopupMenu, 1, MF_BYPOSITION, IDM_START, offset szStart
                invoke  KillTimer, hWin, ID_TIMER
                mov     dwTimerRunning, FALSE
                
            .elseif eax==IDM_EXIT
                invoke  Shell_NotifyIcon, NIM_DELETE, offset nid
                invoke  SendMessage, hWin, WM_CLOSE, 0, 0
            .endif
        .endif

    .elseif eax == WM_SHELLNOTIFY
        .if wParam == 0
            .if lParam == WM_RBUTTONDOWN or WM_RBUTTONUP
                invoke  GetCursorPos, ADDR pt
                invoke  SetForegroundWindow, hWin
                invoke  TrackPopupMenuEx, hPopupMenu, TPM_LEFTALIGN or TPM_LEFTBUTTON, pt.x, pt.y, hWin, 0
                invoke  PostMessage, hWin, WM_NULL, 0, 0
            .endif
        .endif		
        
    .elseif eax == WM_TIMER
        .if dwCurrentIcon == ICON_RED
            push    hGreen
            pop     nid.hIcon
            mov     dwCurrentIcon, ICON_GREEN
            
        .elseif dwCurrentIcon == ICON_YELLOW
            push    hRed
            pop     nid.hIcon
            mov     dwCurrentIcon, ICON_RED
            
        .else
            push    hYellow
            pop     nid.hIcon
            mov     dwCurrentIcon, ICON_YELLOW
        .endif
        invoke  Shell_NotifyIcon, NIM_MODIFY, offset nid
       
    .elseif eax==WM_CLOSE 
        invoke  DestroyIcon, hRed
        invoke  DestroyIcon, hYellow
        invoke  DestroyIcon, hGreen
        invoke  DestroyIcon, hDiC
        invoke  DestroyMenu, hPopupMenu
        invoke  KillTimer, hWin, ID_TIMER
        invoke  DestroyWindow, hWin
        
    .elseif eax==WM_DESTROY
        invoke PostQuitMessage,NULL
        
    .else
        invoke DefWindowProc,hWin,uMsg,wParam,lParam
        ret
    .endif
    xor    eax,eax
    ret
WndProc endp

WM_CREATE
When our window is created, we set a "flag" for our starting icon to ICON_GREEN, and our Timer is not running. Then we load 4 icons from our resource section and save their handles.
Create a popup menu and save that handle.
Next we add 3 menu items to the popup menu with AppendMenu.

Next we fill out the NOTIFYICONDATA structure
cbSize = size of structure
hwnd = the handle to the window to receive messages from our tray icon
uID = for our example, we have only 1 icon in the tray so set to zero. If you had more than one icon in the tray, you would give each icon a unique number to check which one was clicked.
uFlags = the fields we are filling in - NIF_ICON, NIF_MESSAGE and NIF_TIP
uCallbackMessage = User defined message that the shell will send to our app when a user clicks our icon in the tray
hIcon = handle to the icon to be displayed in the tray
szTip = pointer to a zero termiated string to be dispalyed as the tooltip when user hovers over our tray icon. Max 64 characters including the NULL. (version 5 of shell32.dll max lenght is 128 characters)
Once the NOTIFYICONDATA structure is filled, we call Shell_NotifyIcon with the message NIM_ADD and the address of the NOTIFYICONDATA structure.
That's it! We now have a Dream In Code icon in the tray area! Eh, what good is it if it does nothing? Let's handle the user interaction (mouse right click and menu display/selection).

WM_SHELLNOTIFY
This is the message we told windows to send to use when something happens to our icon, we are interested when the the user right clicks our icon then we will display a popup menu.
    .elseif eax == WM_SHELLNOTIFY
        .if wParam == 0
            .if lParam == WM_RBUTTONDOWN or WM_RBUTTONUP
                invoke  GetCursorPos, ADDR pt
                invoke  SetForegroundWindow, hWin
                invoke  TrackPopupMenuEx, hPopupMenu, TPM_LEFTALIGN or TPM_LEFTBUTTON, pt.x, pt.y, hWin, 0
                invoke  PostMessage, hWin, WM_NULL, 0, 0
            .endif
        .endif

We get the position of the cursor and display our popup menu to the left of where the cursor is. We want to respond to a left mouse button click, so we pass TPM_LEFTBUTTON to the function TrackPopupMenu. Now when
user right clicks our tray icon, a popup menu will be displayed and will only respond to left mouse clicks.
Caveat
There are 2 "bugs" that occur with a popup menu with a tray icon.
1. The menu will not dissapear if you click "outside" of the menu. We fix this by making our "window" the foreground window with SetForegroundWindow right before we display the popup menu BUT this causes bug number 2 to surface.
2. After calling SetForegroundWindow - first time popup menu is displayed, everything is fine, everytime after that the popup menu will display and close right away. This is an "intentional feature" according to Microsoft.
To fix this, we need to "task switch" to our program by posting a dummy message to our window, we use PostMessage for this with a WM_NULL message.

WM_COMMAND
This is where we will respond to the menu selection.
    .elseif eax==WM_COMMAND
        mov		edx,wParam
        movzx	eax,dx
        shr		edx,16
        .if edx==BN_CLICKED
            .if eax == IDM_DIC_NEW
                invoke	ShellExecute, NULL, offset szOpen, offset szDiCNewTopics, NULL, NULL, SW_SHOWNORMAL or SW_RESTORE
                .if dwTimerRunning == TRUE
                    mov     ax, BN_CLICKED
                    ror     eax, 16
                    mov     ax, IDM_STOP
                    invoke  SendMessage, hWin, WM_COMMAND, eax, NULL           
                    push    hDiC
                    pop     nid.hIcon
                    invoke  Shell_NotifyIcon, NIM_MODIFY, offset nid
                .endif
                   
            .elseif eax == IDM_START
                invoke  ModifyMenu, hPopupMenu, 1, MF_BYPOSITION, IDM_STOP, offset szStop
                invoke  SetTimer, hWin, ID_TIMER, 500, NULL
                mov     dwTimerRunning, TRUE
                
            .elseif eax == IDM_STOP
                invoke  ModifyMenu, hPopupMenu, 1, MF_BYPOSITION, IDM_START, offset szStart
                invoke  KillTimer, hWin, ID_TIMER
                mov     dwTimerRunning, FALSE
                
            .elseif eax==IDM_EXIT
                invoke  Shell_NotifyIcon, NIM_DELETE, offset nid
                invoke  SendMessage, hWin, WM_CLOSE, 0, 0
            .endif
        .endif

IDM_DIC_NEW
We get this when user selects our menu - DiC New Topics. We call ShellExecute to open/bring to front the users default webbrowser and display new topics.
We check to see if our timer is active, and if it is we "click" the Stop Timer menu through code with SendMessage and change the tray icon to the DiC icon.
we chage the handle of the icon pointed to in nid.hIcon to our DiC icon and tell the Shell to modify the icon with Shell_NotifyIcon and the message NIM_NOTIFY.

IDM_START
We receive this when user selects the Start Traffic Light menu.
First thing we do is change the menu text from Start Traffic Lights to Stop Traffic Lights and change the menu ID to IDM_STOP, we do this with ModifyMenu
Next we activate our timer to "fire" every half second (we use the timer to change the icons), then we set our flag that our timer is running.

IDM_STOP
We receive this when user select the Stop Traffic Light menu.
We change the menu text from Stop Traffic Light back to Start Traffic Light, and change the menu ID back to IDM_START
Stop the timer and our timer is running flag to false

IDM_EXIT
We receive this when user select exit from menu
First we tell the shell to remove our icon from the tray notification area, then close our app.

WM_TIMER
We receive this message everytime our timer fires.
        .if dwCurrentIcon == ICON_RED
            push    hGreen
            pop     nid.hIcon
            mov     dwCurrentIcon, ICON_GREEN
            
        .elseif dwCurrentIcon == ICON_YELLOW
            push    hRed
            pop     nid.hIcon
            mov     dwCurrentIcon, ICON_RED
            
        .else
            push    hYellow
            pop     nid.hIcon
            mov     dwCurrentIcon, ICON_YELLOW
        .endif
        invoke  Shell_NotifyIcon, NIM_MODIFY, offset nid

Here we check to see which icon is being displayed and change the tray icon accordingly

WM_CLOSE
We receive this when user selects the Exit menu
    .elseif eax==WM_CLOSE 
        invoke  DestroyIcon, hRed
        invoke  DestroyIcon, hYellow
        invoke  DestroyIcon, hGreen
        invoke  DestroyIcon, hDiC
        invoke  DestroyMenu, hPopupMenu
        invoke  KillTimer, hWin, ID_TIMER
        invoke  DestroyWindow, hWin

Here we are being good programmers and destroying everything we created.

Thats it! Modify and experiment - have fun!!!
App, icons and source attatched.

1CreateMutex
2Kernel Object Namespace
3Message Only Window

Attached File(s)


This post has been edited by GunnerInc: 25 December 2011 - 07:12 PM


Is This A Good Question/Topic? 0
  • +

Page 1 of 1