Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

CTrayIconPosition - where is my tray icon?

0.00/5 (No votes)
10 Jul 2012 1  
Ever wanted to know position of your tray icon? Windows supplies no API for that. This class is a compact solution that works.

Sample Image - CTrayIconPosition.gif

Introduction

This compact class makes one impossible thing possible - it's able to detect position of tray icon of your application.

The problem

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! 

Two ways to do it (2+)

  1. Direct method: Seems to be perfect if end user do not use a different tray manager than the one that is installed with MS Windows.

    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).

    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.

    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;
    }
  2. Visual scan method: There is also a different approach possible. We can find the rectangle of system tray (we did it in previous method) and then scan this area for our icon manually. The idea is easy but implementation was not. As you may guess, Shell_NotifyIcon while adding your icon to the system tray often does a lot of things with your icon. What is being done depends, for example, on the version of Windows and sometimes on your graphic mode (number of colors). In other words, if you ask Shell_NotifyIcon to add your beautiful 32x32 pixels big and colorful icon to the tray - it could land there with reduced size and number of colors, and it's near not possible to predict how it would look like in the system tray. So it would not be wise to try to seek for your colorful icon.

    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 :)

  3. Using both methods: Direct scan method seems to be perfect - but what if user changed his/her default tray application to some third party software available in the market? It's rather unlikely but if you write an application that has to work on every PC, you should consider it. Second approach (visual scan) has a chance to succeed when first one will fail. Ultimate solution is simple - use both methods - if one fails, just try the second one. The code posted here gives you an easy ability to take this approach.

Usage in your projects

I wrote a compact class CTrayIconPosition. If you want to use it in your project - follow these few simple steps:

  • Add TrayIconPosition.h and TrayIconPosition.cpp to your project.
  • Add #include "TrayIconPosition.h" in files where you plan to use this class.
  • Declare a variable of this class (in my opinion, the best is to make it a member variable of your main dialog window or something like that).
  • Copy IDI_BLANK_BLACK icon from sample project, to your application.
  • Use API described below.

CTrayIconPosition API

  • 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 - it's the ID of the icon you set while adding the icon to tray with Shell_NotifyIcon.
  • 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.

    Please note a_eTrackType parameter: it controls how class should do the tracking. Allowed values are:

    • UseBothTechniquesDirectPrefered - class will try to detect your icon using direct method first; in case of failure, it will do the visual scan of system tray.
    • UseBothTechniquesVisualScanPrefered - similar to UseBothTechniquesDirectPrefered but the order of detection is visual first and direct if visual failed.
    • UseDirectOnly - self explaining.
    • UseVisualScanOnly - self explaining.
  • 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.

  • 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.

    Acceptable values:

    • CTrayIconPosition::Default
    • CTrayIconPosition::High - cached position will expire in 10 seconds
    • CTrayIconPosition::Medium - cached position will expire in 30 seconds
    • CTrayIconPosition::Low - cached position will expire in 60 seconds

    On default, High precision is assumed.

  • void Invalidate();

    This function forces next call of GetTrayIconPosition not to use cached values.

Example of usage

//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

Some remarks

  1. I was testing this class on Win 98, ME, 2000 and XP - on all those systems, it was working fine. If you're concerned about this changing icon to black for a moment (in visual scan method) - I want to say that on most cases, it's not noticeable. Even on slow, overloaded machines, detecting position takes less than a blink of eye. So it should not be a problem.
  2. As you can see, you have to add IDI_BLANK_BLACK icon to your project in order to use this class - I know that it is not the best way it could be done. If you like, you can write your own code that will create plain, black icon in a run time and get rid of this resource. My goal was to present a compact and working class - you're free to improve it!
  3. Some people were asking me why I didn't use balloon tool tips feature of Windows 2000 and XP. If you ask yourself the same question, it means that you don't understand the purpose of this class - it's designed to find out exact position of tray icon - displaying balloon is only an example - you can of course use it in quite a different way.
  4. If you have a proposal to make this class more effective and reliable - I would be grateful for your feedback. Post a comment on this site or mail me at: irekzielinski-DEL_THIS@wp.pl.
  5. Function GetTrayIconPosition returns a CPoint class type data. Some of you could be disappointed why it's not a CRect or RECT structure. Well - it's because I didn't need that - I think that it's rather easy to modify this class to return such a bit more valuable data. In current implementation, GetTrayIconPosition returns not exactly center point of tray icon - it's rather left, top part.
  6. Sorry for my English - it's not my native language :-D

History of updates

22nd of December 2004

  • Added direct scan method as proposed by Neal Andrews here.

28th of May 2004

  • Updated algorithm to detect a black icon (CheckIfColorIsBlackOrNearBlack) as proposed by Harald here.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here