Workaround Windows Tray Area Item Preference

Reading Time: 4 minutes

Introduction

I’m not an experienced Windows developer.
I had to make it clear 🙂

Today I had the chance to implement kind of a nasty hack on Windows.
I had to make my application tray icon always visibile, at least by default. I swear I honor user preference then. I know this is one of those don’tswhen working with Windows API since it is clearly stated in the docs developers have no control over the notification area. But sometimes you feel it deep in your heart that your application user experience would benefit a lot from making your tiny tray icon visible by default. This was my case, and as I can see on the internet, this is the case of a lot of apps out there.

I just wanted to write this down as an excercise to help me get back on sharing what I code (not always feasible, though).

There’s plenty of information out there, it’s just you won’t find a single pice of it and you’ll have to digg a lot before being able to workaround this limitation over the tray area. At least this was my experience as a non-experienced Windows developer.

Now that my reasons have been stated clear we can go ahead and see some code.

Let’s get started

So, there’s this incredible resource by Geoff Chappell which you should check if you want to know more about some undocumented/private APIs on Windows. It looks he has done a huge amount of work around the notification area documenting well enough how to workaround the default limitations exposed by the documented API.

As he states here explorer.exe exposes an ITrayNotify implementation through COM. This interface can be used to know user preferences and status regarding the notification area items. And you of course can also use it to modify such preferences.

So what we are going to do now is requesting the implementation through the canonical CoCreateInstance passing in the required CLSID information. Such information is retrievable from the docs by Geoff. But you can also look for ITrayNotify through regedit.exe in order to find the needed CLSID.

Bringing a few pieces together, here is a quick recap of the declarations you’ll need to make the CoCreateInstance call succeed.

[sourcecode lang=”cpp”]

#ifndef __ITrayNotify_INTERFACE_DEFINED__
#define __ITrayNotify_INTERFACE_DEFINED__

class __declspec(uuid("FB852B2C-6BAD-4605-9551-F15F87830935")) ITrayNotify : public IUnknown
{
public:
virtual HRESULT __stdcall
RegisterCallback(INotificationCB* callback) = 0;
virtual HRESULT __stdcall
SetPreference(const NOTIFYITEM* notify_item) = 0;
virtual HRESULT __stdcall EnableAutoTray(BOOL enabled) = 0;
};
#endif // #ifndef __ITrayNotify_INTERFACE_DEFINED__

const CLSID CLSID_TrayNotify = {
0x25DEAD04,
0x1EAC,
0x4911,
{0x9E, 0x3A, 0xAD, 0x0A, 0x4A, 0xB5, 0x60, 0xFD}};

[/sourcecode]

This is enough for requesting the instance through CoCreateInstance. Unfortunately, as I discovered testing my own code, this won’t work on Windows 8 where apparently this private API has changed. You know, this is the drawback of using private APIs :).
Anyway, I spent the day looking for the solution and fortunately I found the appropriate interface also for Windows 8. You can find the same information by running OllyDbg against explorer.exe on Windows 8.

[sourcecode lang=”cpp”]
class __declspec(uuid("D133CE13-3537-48BA-93A7-AFCD5D2053B4")) ITrayNotifyWindows8 : public IUnknown
{
public:
virtual HRESULT __stdcall
RegisterCallback(INotificationCB* callback, unsigned long*) = 0;
virtual HRESULT __stdcall UnregisterCallback(unsigned long*) = 0;
virtual HRESULT __stdcall SetPreference(NOTIFYITEM const*) = 0;
virtual HRESULT __stdcall EnableAutoTray(BOOL) = 0;
virtual HRESULT __stdcall DoAction(BOOL) = 0;
};
[/sourcecode]

Getting the instance to the appropriate ITrayNotify interface, though, is not enough. We are going to use another private interface, called INotificationCB, which will help us get the current information regarding our notification item.

So let’s write down our little helper class that will take care of modifying the preferences for our notification item.

[sourcecode lang=”cpp”]
// TinyTrayHelper.h

class TinyTrayHelper : public INotificationCB
{
public:
TinyTrayHelper(NOTIFYICONDATA* nid);
virtual ~TinyTrayHelper();

HRESULT __stdcall Notify(ULONG, NOTIFYITEM *) __override;

bool ensureTrayItemVisible();

ULONG __stdcall AddRef(void) __override;
ULONG __stdcall Release(void) __override;
HRESULT __stdcall QueryInterface(REFIID riid, void **ppvObject) __override;

private:
NOTIFYICONDATA *_nid;
NOTIFYITEM _nit;
wchar_t _exeName[MAX_PATH];
};
[/sourcecode]

Now let’s see the actual implementation for our helper.

[sourcecode lang=”cpp”]
#include "trayhelper.h"

#include <sdkddkver.h>
#include <VersionHelpers.h>

#include <stdio.h>

static void* CreateTrayNotify(bool win8);

TinyTrayHelper::TinyTrayHelper(NOTIFYICONDATA *nid) :
_nid(nid),
_win8(false)
{
CoInitialize(NULL);

::GetModuleFileName(NULL, _exeName, MAX_PATH);

// here we prepare the NOTIFYITEM instance
// that is required to change settings
_nit.exe_name = _exeName;
_nit.guid = _nid->guidItem;
_nit.hwnd = _nid->hWnd;
_nit.icon = _nid->hIcon;
}

TinyTrayHelper::~TinyTrayHelper()
{
}

HRESULT __stdcall TinyTrayHelper::Notify(ULONG, NOTIFYITEM *item)
{
if (item->hwnd != _nid->hWnd || item->guid != _nid->guidItem) {
// this is a notification about an item that is not ours
// so let’s just ignore it
return S_OK;
}

_nit = NOTIFYITEM(*item);

return S_OK;
}

ULONG __stdcall TinyTrayHelper::AddRef(void)
{
return 1;
}

ULONG __stdcall TinyTrayHelper::Release(void)
{
return 1;
}

HRESULT __stdcall TinyTrayHelper::QueryInterface(REFIID riid, void **ppvObject)
{
if (ppvObject == NULL) return E_POINTER;

if (riid == __uuidof(INotificationCB)) {
*ppvObject = (INotificationCB*)this;
} else if (riid == IID_IUnknown) {
*ppvObject = (IUnknown *) this;
} else {
return E_NOINTERFACE;
}

AddRef();
return S_OK;
}

bool TinyTrayHelper::ensureTrayItemVisible()
{
const bool win8 = IsWindows8OrGreater();
void *trayNotify = CreateTrayNotify();
if (!trayNotify) {
return false;
}

HRESULT hr;
if (win8) {
auto *win8TrayNotify = static_cast<ITrayNotifyWin8*>(trayNotify);
unsigned long callback_id = 0;
// this is synchronous
hr = win8TrayNotify->RegisterCallback(static_cast<INotificationCB*>(this), &callback_id);
hr = win8TrayNotify->UnregisterCallback(&callback_id);
} else {
hr = ((ITrayNotify*)trayNotify)->RegisterCallback(static_cast<INotificationCB*>(this));
hr = ((ITrayNotify*)trayNotify)->RegisterCallback(NULL);
}

if (FAILED(hr)) {
((IUnknown*)trayNotify)->Release();
return false;
}

// now we should have an up-to-date information
// about our notification icon item

if (_nit.preference != 0x01) { // this means always hide, so we honor user preference
_nit.preference = 0x02;

if (_win8) {
((ITrayNotifyWin8*)trayNotify)->SetPreference(&_nit);
} else {
((ITrayNotify*)trayNotify)->SetPreference(&_nit);
}
}
((IUnknown*)trayNotify)->Release();
}

static void* CreateTrayNotify(bool win8)
{
CLSID iTrayNotifyCLSID;
if (win8) {
iTrayNotifyCLSID = __uuidof(ITrayNotifyWindows8); // the interface we defined previously
} else {
iTrayNotifyCLSID = __uuidof(ITrayNotify);
}

void *trayNotify;
HRESULT hr = CoCreateInstance (
CLSID_TrayNotify,
NULL,
CLSCTX_LOCAL_SERVER,
iTrayNotifyCLSID,
(PVOID *) &trayNotify);

if (hr == S_OK) {
return trayNotify;
} else {
printf("Cannot get reference to ITrayNotify instance\n");
}

return NULL;
}

[/sourcecode]

I see what you did there

So, TinyTrayHelper basically does 4 things here:

  1. Creates a NOTIFYITEM instance based on a NOTIFICATIONDATA instance
  2. Chooses the appropriate ITrayNotify instance based on the current OS
  3. Registers itself as an instace of INotificationCB to receive the relevant information inside the Notify method
  4. Finally calls SetPreference to change the preference regarding the notification area item

What now?

What you need now is just to create an instance of our TinyTrayHelper and pass in your NOTIFICATIONDATA instance reference. Then call ensureTrayIconVisible to change the notification area preference regarding your item.

Please note that I adapted a more complex code to build this example so I didn’t test this code specifically. Use at your own risk.

I hope this will be useful to you. Please, let me know if I made tremendous mistakes here, I’ll try to fix!

Cheers.


Posted

in

,

by

Comments

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.