24 December 2008

VB.Net enumerate the system tray icons/buttons

Another msdn post. This one asked how to get the paths to the icons in the system tray. This sounds pretty simple, but isn't. You can start off by finding the window with Spy++, and then getting a handle to it. Then SendMessage with TB_BUTTONCOUNT to get the number of buttons. (TBBUTTON = TaskBarButton). Then you send TB_GETBUTTON repeatedly and get back TBBUTTON structures with information about the buttons. (See the last blog post for info on the x86/x64 mess)

typedef struct _TBBUTTON {    int        iBitmap;
    int        idCommand;
    BYTE     fsState;
    BYTE     fsStyle;
#ifdef _WIN64
    BYTE     bReserved[6]     // padding for alignment
#elif defined(_WIN32)
    BYTE     bReserved[2]     // padding for alignment
#endif
   DWORD_PTR   dwData;
    INT_PTR          iString;
} TBBUTTON, NEAR *PTBBUTTON *LPTBBUTTON;

The dwData field points to a NOTIFYICONDATA structure with some more gobbledegook:

typedef struct _NOTIFYICONDATA {
    DWORD cbSize;
    HWND hWnd;
    UINT uID;
    UINT uFlags;
    UINT uCallbackMessage;
    HICON hIcon;
    TCHAR szTip[64];
    DWORD dwState;
    DWORD dwStateMask;
    TCHAR szInfo[256];
    union {
        UINT uTimeout;
        UINT uVersion;
    };
    TCHAR szInfoTitle[64];
    DWORD dwInfoFlags;
    GUID guidItem;
    HICON hBalloonIcon;
} NOTIFYICONDATA, *PNOTIFYICONDATA;

We don't need all that, so we could trim it down. Earlier versions of windows used a smaller version anyway.

The difficulty lies in the fact that some of the pointers don't point anywhere useful. Say my process has a button at address 100. That address is local to my process, it has its own virtual memory space. If another process reads that pointer, then the value - 100 - will not mean anything as the other process has its own virtual memory space that maps to a different block of real memory. In our program, we SendMessage to the process hosting the tray buttons asking it to stick a TBBUTTON structure into some memory that is allocated in our home process. Unfortunately the pointer to the memory has no meaning to the other process, so the button goes astray.

To get around this, we ask the other process to create a blob of memory big enough for a tray button using VirtualAllocEx. Then we SendMessage, asking the other process to put the button into this blob of memory. Finally we read the contents of that memory with ReadProcessMemory:

Dim tbb As New TBBUTTON32
Dim friendlyTB As New TrayButton
friendlyTB.SetTrayIndex(index)
Dim pButton As IntPtr = IntPtr.Zero
Try
    ' Create memory in the explorer process to store a TBBUTTON strucutre:
    pButton = VirtualAllocEx(hProcess, Nothing, tbb.Size, MEM_COMMIT, PAGE_READWRITE)
    ' Use SendMessage to request that the memory is filled with the TBBUTTON @ index.
    Dim bResult As Boolean = SendMessage(hwndTrayToolbar, TB_GETBUTTON, index, pButton)
    If bResult = False Then Throw New Win32Exception
    ' And read the memory from the other process:
    Dim bytesReturned As Integer
    bResult = ReadProcessMemory(hProcess, pButton, tbb, tbb.Size, bytesReturned)
    If bResult = False Then Throw New Win32Exception

And here's an app that doesn't work too well as there are many complications to do with animated icons, OSes, x86/x64 that I haven't touched...

1 comment: