Win32: Hide to system tray – Part 3
In Part 1 of the series, we have seen how we can hide the application and display an icon in system tray. In Part 2 we have implemented a redisplay of application window upon double click on system tray icon. In Part 3, we will see how we can display a popup menu on left and right click on system tray icon without disturbing double click behaviour.
Step 1 – Create a new menu
Double click on myprojectname.rc file in your Solution explorer (it is located in folder Resource Files). A Resource View will open with several folders. Open folder Menu. There is one item in there already. That is your main menu. Right click on folder and select Add resource…, choose Menu and click New button. New, empty menu will be created. I suggest a rename (in my case IDR_TRAYMENU), but it is not necessary.
Open the menu and create a menu without a title with Show and Exit sub menu items. Show item will redisplay the application window (much like double click) and Exit will close the application all together. Again, I suggest changing the default IDs of each item to something more obvious (in my case IDM_TRAY_SHOW and IDM_TRAY_EXIT).
Step 2 – Display a popup menu on right click
So basically, what you want is that when you right click application’s tray icon, you need to see a popup near the click. Thus, you need to get mouse cursor position at the time of the click and display a sub menu item at that position. You can also tie it to position of an icon, but it is easier to just get cursor position, so that’s what we’ll use. Then, we need to handle right click event message and make it display a popup menu.
So, tray.h and tray.cpp files will need some tinkering. I created a TrayLoadPopupMenu function, which will take care of displaying a sub menu:
void TrayLoadPopupMenu(HWND hWnd);
// // FUNCTION: TrayLoadPopupMenu(HWND) // // PURPOSE: Load tray specific popup menu // // void TrayLoadPopupMenu(HWND hWnd) { POINT cursor; HMENU hMenu; GetCursorPos(&cursor); hMenu = (HMENU) GetSubMenu(LoadMenu(hInst, MAKEINTRESOURCE(IDR_TRAYMENU)), 0); SetMenuDefaultItem(hMenu, IDM_TRAY_SHOW, false); TrackPopupMenu(hMenu, TPM_LEFTALIGN, cursor.x, cursor.y, 0, hWnd, NULL); }
We get mouse cursor position by calling GetCursorPos function. Then, we load our popup menu and store a handle to it. We set a default item to Show, resulting in Show being written in bold. And lastly, we call TrackPopupMenu function to which we pass popup menu handle, alignment of popup menu, a position of a menu and a window handler. That’s pretty much it.
Now, we need to alter our main message handling function in myprojectname.cpp to handle right mouse click event. But beware, we only need to follow a right button click on a tray icon. Meaning, we need to alter our WM_TRAYMESSAGE handler like so:
case WM_TRAYMESSAGE: switch(lParam) { case WM_LBUTTONDBLCLK: TrayDeleteIcon(hWnd); ShowWindow(hWnd, SW_SHOW); break; case WM_RBUTTONUP: TrayLoadPopupMenu(hWnd); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break;
We are handling WM_RBUTTONUP event because we want a user to release the button and then display a menu.We could just as easily handle WM_RBUTTONDOWN event, but that would be fired each time a user would press a button, but would not release it yet. So, it really depends what works for you.
That is it. Now, if we build and debug our app, a popup menu will show whenever we right click on app’s tray icon.
Step 3 – Handle sub menu item events
Obviously, we also want our popup menu to do something. Thus, we need to handle our popup menu item events. We want our Show item to redisplay an app and our Exit item to close it all together. We need to alter our WM_COMMAND handler to look something like this:
case WM_COMMAND: wmId = LOWORD(wParam); wmEvent = HIWORD(wParam); // Parse the menu selections: switch (wmId) { case IDM_ABOUT: DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About); break; case IDM_TRAY_SHOW: TrayDeleteIcon(hWnd); ShowWindow(hWnd, SW_SHOW); break; case IDM_TRAY_EXIT: case IDM_EXIT: TrayDeleteIcon(hWnd); DestroyWindow(hWnd); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break;
This is where that ID renaming came in good use. We can look at this code and see that IDM_TRAY_SHOW represents Show item and that IDM_TRAY_EXIT represents exit item.
Step 4 – Plot thickens a.k.a. displaying menu on left mouse click
This is where things get tricky. Not because handling left mouse click would be that more difficult than right mouse click, but because we are handling double click event as well. Now, the problem is that double mouse click is a sequence of following events: WM_LBUTTONDOWN, WM_LBUTTONUP, WM_LBUTTON_DOWN and WM_LBUTTONUP, which means that while handling double click a single click event will be triggered. Twice. Not good. There are couple of ways to work around this. The easiest and most compelling is to use a boolean variable to track weather it was a double click or a single click. Don’t go with that, as it can lead to race conditions. Instead, I like a method where you run a timer for the time just a bit longer than double click is and if double click occurs, kill the timer and redisplay an app or, if there is no double click, display a menu after the timer ran out. On a negative side this means a click will not be as responsive. On a plus side, it will work every time in every scenario.
But, how do we know how long a double click is? Win32 already has a function called GetDoubleClickTime that returns time set for double click in miliseconds. With that knowledge gathered, we can now create methods that will create and kill timer and a callback method that will be run on timer interval.
First things first. Killing a timer is a repetitive task and it does not depend on nature of timer or task. Thus, we will create a utils.h and utils.cpp file that will contain code to kill a timer.
#pragma once #include "resource.h" BOOL UtilKillTimer(HWND hWnd, INT_PTR pnTimer);
#include "stdafx.h" #include "utils.h" // // FUNCTION: UtilKillTimer(HWND, INT_PTR) // // PURPOSE: Kills specified timer // // BOOL UtilKillTimer(HWND hWnd, INT_PTR pnTimer) { BOOL fTimerKilled = FALSE; if (NULL != pnTimer) { fTimerKilled = KillTimer(hWnd, pnTimer); pnTimer = NULL; } return fTimerKilled; }
This code is pretty straight forward. We check if timer still exists and if it does, we kill it using Win32 native KillTimer function.
Next, we need to create functions that will Start and Kill tray delay timer and create a callback method which timer will invoke upon each interval. We add the following:
#define TRAY_ID_TIMER_CLICK 100 #define TRAY_CLICK_DELAY 10 void TrayStartClickDelayTimer(HWND hWnd); void TrayKillClickDelayTimer(HWND hWnd); void CALLBACK TrayOnTimerIconClick(HWND hWnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime);
INT_PTR pTimerTrayClick = NULL; // tray click timer id // // FUNCTION: TrayStartClickDelayTimer(HWND) // // PURPOSE: Starts click delay timer which displays a popup menu // // COMMENTS: Needed due to event bubbling when double click occurs // // void TrayStartClickDelayTimer(HWND hWnd) { pTimerTrayClick = SetTimer(hWnd, TRAY_ID_TIMER_CLICK, GetDoubleClickTime() + TRAY_CLICK_DELAY, TrayOnTimerIconClick); } // // FUNCTION: TrayKillClickDelayTimer(HWND) // // PURPOSE: Kills click delay timer // // void TrayKillClickDelayTimer(HWND hWnd) { UtilKillTimer(hWnd, pTimerTrayClick); } // // FUNCTION: TrayOnTimerIconClick(HWND, UINT, UINT_PTR, DWORD) // // PURPOSE: Click delay timer callback function which displays tray specific popup menu // // void CALLBACK TrayOnTimerIconClick(HWND hWnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) { UtilKillTimer(hWnd, pTimerTrayClick); TrayLoadPopupMenu(hWnd); }
Code itself is no rocket science, but let’s browse through it.
- Function TrayStartClickDelayTimer starts a delay timer. This is done via Win32 native function SetTimer. We pass it a window handle, a timer id (which is a custom number, but must be unique for each timer), a time interval (which must be greater than double click event time or we solved nothing) and pass a pointer to our callback function.
- Function TrayKillClickDelayTimer which makes sure that the right timer gets killed when calling UtilKillTimer function.
- Callback function TrayOnTimerIconClick, which kills the timer that triggered it (or it would run in a loop) and then calls TrayLoadPopupMenu function to actually display a popup menu.
All we are now left to do is to handle left button message in myprojectname.cpp:
case WM_TRAYMESSAGE: switch(lParam) { case WM_LBUTTONDBLCLK: TrayKillClickDelayTimer(hWnd); TrayDeleteIcon(hWnd); ShowWindow(hWnd, SW_SHOW); break; case WM_LBUTTONUP: case WM_RBUTTONUP: TrayStartClickDelayTimer(hWnd); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break;
So what we are doing here is we are telling the application that on right or left mouse click on tray icon, it should start a delay timer. If double click occurs (which always happens before delay timer runs out), kill the timer and do what your double click does. Otherwise do what is specified in timer’s callback function.
Now you are fully capable of hiding an app in a system tray and redisplay it with double click or popup menu.
With this we conclude our tray icon series.
Leave a Reply