This compact class makes one impossible thing possible - it's able to detect position of tray icon of your application.
I'm the author of the popular application Tray Helper. This application takes advantage of Shell_NotifyIcon WinAPI function and puts its own icon into system tray. For a long time, I didn't have any problems with this feature until.... I found a great class by Joshua Heyer (Shog9) - CBalloonHelp. Shog9's class is able to display cool balloon messages and I decided that my application will display those balloons coming from my tray icon. After reading MSDN and many WWW sites, I found that there is no way to determine where exactly your tray icon is! There is no API at all for that!
Shell_NotifyIcon
This method was proposed by Neal Andrews and I ported it from VB (source code) to C++. The main idea behind this method is that system tray uses an ordinary toolbar control to display icons (if you don't believe me - check it out with Spy++ application). It's also an easy thing to find a handle of this control and ask it directly for the rectangle of our icon. There are two things that need to be implemented. First, we need to find a handle to toolbar control. It can be done by enumerating all windows in a system and finding the one with Shell_TrayWnd class name (this is a main window for a system tray). Then we enumerate all the child windows of the tray to find a toolbar (ToolbarWindow32 class name).
Shell_TrayWnd
ToolbarWindow32
Once we have a handle to a toolbar, we can query it for the number of icons it currently posseses:
//now we check how many buttons is there - should be more than 0 int iButtonsCount = SendMessage(hWndTray, TB_BUTTONCOUNT, 0, 0);
If number of icons seems to be fine (is greater than 0), ee can start thinking how to ask this control for our icon. If toolbar would be a part of our application, we could just send it TB_GETBUTTON and TB_GETITEMRECT messages. It could look like:
for(int iButton=0; iButton<iButtonsCount; iButton++) { TBBUTTON buttonData; //this structure will be filled with data about button SendMessage(hWndTray, TB_GETBUTTON, iButton, (LPARAM)&buttonData); }
But in our code, such message would fail or even raise a General Protection Fault error! The main reason is that we can't pass pointer to locally allocated TBBUTTON structure to another process (process of Windows tray application). To solve this problem, we need to allocate TBBUTTON structure inside tray application process. Then we can send message to a toolbar with a pointer to that allocated memory, and at the end - we can read this block of memory back to our application.
TBBUTTON
Code sample (error checking was skipped for easier reading):
BOOL FindOutPositionOfIconDirectly(const HWND a_hWndOwner, const int a_iButtonID, CRect& a_rcIcon) { HWND hWndTray = GetTrayToolbarControl(); //now we have to get an ID of the parent process for system tray DWORD dwTrayProcessID = -1; GetWindowThreadProcessId(hWndTray, &dwTrayProcessID); //here we get a handle to tray application process HANDLE hTrayProc = OpenProcess(PROCESS_ALL_ACCESS, 0, dwTrayProcessID); //now we check how many buttons is there - should be more than 0 int iButtonsCount = SendMessage(hWndTray, TB_BUTTONCOUNT, 0, 0); //We want to get data from another process - it's not possible //to just send messages like TB_GETBUTTON with a locally //allocated buffer for return data. Pointer to locally allocated //data has no usefull meaning in a context of another //process (since Win95) - so we need //to allocate some memory inside Tray process. //We allocate sizeof(TBBUTTON) bytes of memory - //because TBBUTTON is the biggest structure we will fetch. //But this buffer will be also used to get smaller //pieces of data like RECT structures. LPVOID lpData = VirtualAllocEx(hTrayProc, NULL, sizeof(TBBUTTON), MEM_COMMIT, PAGE_READWRITE); BOOL bIconFound = FALSE; for(int iButton=0; iButton<iButtonsCount; iButton++) { //first let's read TBUTTON information //about each button in a task bar of tray DWORD dwBytesRead = -1; TBBUTTON buttonData; SendMessage(hWndTray, TB_GETBUTTON, iButton, (LPARAM)lpData); //we filled lpData with details of iButton icon of toolbar //- now let's copy this data from tray application //back to our process ReadProcessMemory(hTrayProc, lpData, &buttonData, sizeof(TBBUTTON), &dwBytesRead); //let's read extra data of each button: //there will be a HWND of the window that //created an icon and icon ID DWORD dwExtraData[2] = { 0,0 }; ReadProcessMemory(hTrayProc, (LPVOID)buttonData.dwData, dwExtraData, sizeof(dwExtraData), &dwBytesRead); HWND hWndOfIconOwner = (HWND) dwExtraData[0]; int iIconId = (int) dwExtraData[1]; if(hWndOfIconOwner != a_hWndOwner || iIconId != a_iButtonID) { continue; } //we found our icon - in WinXP it could be hidden - let's check it: if( buttonData.fsState & TBSTATE_HIDDEN ) { break; } //now just ask a tool bar of rectangle of our icon RECT rcPosition = {0,0}; SendMessage(hWndTray, TB_GETITEMRECT, iButton, (LPARAM)lpData); ReadProcessMemory(hTrayProc, lpData, &rcPosition, sizeof(RECT), &dwBytesRead); MapWindowPoints(hWndTray, NULL, (LPPOINT)&rcPosition, 2); a_rcIcon = rcPosition; bIconFound = TRUE; break; } VirtualFreeEx(hTrayProc, lpData, NULL, MEM_RELEASE); CloseHandle(hTrayProc); return bIconFound; }
But there is an easy and reliable solution (it really works on nearly all machines!). What about changing your default icon to plain-black one, seek for black rectangle in system tray, and after that restore the icon of your application? Not convinced? Well, I was skeptic also - but it just works fine :)
I wrote a compact class CTrayIconPosition. If you want to use it in your project - follow these few simple steps:
CTrayIconPosition
#include "TrayIconPosition.h"
IDI_BLANK_BLACK
void InitializePositionTracking(HWND hwndOfIconOwner, int iIconID);
Before calling this function, you should already have your icon in system tray. This function initializes tracking mechanism.
int iIconID
BOOL GetTrayIconPosition(TrackType a_eTrackType = UseBothTechniquesDirectPrefered, Precision a_ePrec = Default);
This function calculates position of tray icon, and returns TRUE if icon was found and FALSE if it was not found. But even if return value is FALSE - you can use point value - since it most likely will contain useful data. For example, under Windows XP, your tray icon can be hidden - then the return value of this function will be FALSE. But point will contain left, middle part of system tray (in WinXP, it's hide/unhide icons button). Remember that call of this function can change your tray icon to black - call RestoreTrayIcon if you want to undo this effect.
TRUE
FALSE
RestoreTrayIcon
Please note a_eTrackType parameter: it controls how class should do the tracking. Allowed values are:
a_eTrackType
UseBothTechniquesDirectPrefered
UseBothTechniquesVisualScanPrefered
UseDirectOnly
UseVisualScanOnly
void RestoreTrayIcon(HICON icon);
Restores black icon set by GetTrayIconPosition. Since icon changes quite often in my Tray Helper application, I implemented restoring icon in a separate function call. If your application has static, always the same icon, it could be convenient to change this class to auto call this restore function.
GetTrayIconPosition
void SetDefaultPrecision(Precision newPrecision);
Let me explain meaning of this function on example:
You call GetTrayIconPostion member function many times a second. Since this function sets black icon in tray - such numerous calls in short period of time could look quite bad (flickering). But usually, position of tray icon doesn't change that often. That's why CTrayIconPosition keeps a cache of last calculated position and if you call GetTrayIconPosition - it is able to return cached value instead of checking it over and over again. Cached value is valid only for some time - and using this function, you can set it if you want more accurate results or less accurate with less flickering.
GetTrayIconPostion
Acceptable values:
CTrayIconPosition::Default
CTrayIconPosition::High
CTrayIconPosition::Medium
CTrayIconPosition::Low
On default, High precision is assumed.
High
void Invalidate();
This function forces next call of GetTrayIconPosition not to use cached values.
//let's add icon to system tray first NOTIFYICONDATA nid; nid.cbSize = sizeof(nid); nid.hWnd = m_hWnd; //ID of icon - you have to pass this //value to InitializePositionTracking nid.uID = 1; nid.uFlags = NIF_ICON; nid.hIcon = AfxGetApp()->LoadIcon(IDI_YOUR_ICON); Shell_NotifyIcon(NIM_ADD, &nid); //let's initialize tray icon position tracking //second argument it's ID of icon (nid.uID) m_tipPosition.InitializePositionTracking(m_hWnd,1); //ok now let's find out the position of our tray icon: //use m_tipPosition.Invalidate(); //if you want to avoid few-seconds position cashing CPoint ptIcon; BOOL bIconFound = m_tipPosition.GetTrayIconPosition(ptIcon, CTrayIconPosition::UseBothTechniquesDirectPrefered); //GetTrayIconPosition in order to find out position //can (unless UseDirectOnly method is used) //sets a black icon in tray - let's restore it now m_tipPosition.RestoreTrayIcon(AfxGetApp()->LoadIcon(IDI_YOUR_ICON)); //use returned CPoint value here :-D
CPoint
CRect
RECT
CheckIfColorIsBlackOrNearBlack
This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)
General News Suggestion Question Bug Answer Joke Rant Admin
Math Primers for Programmers