Introduction
Sometimes you are developing an ATL COM server that does not have a GUI, and you would
like an icon in the shell (or "system tray" as it's also known), to display some sort of
runtime information. I needed this for a project I was working on at the time, and was
inspired by the little icon that appears when you connect to the Internet with your modem.
It shows you how long you've been connected, and the number of bytes sent and received.
The icon also changes when there is activity - a nice touch.
I had a look at some of the source code available on Code Project, but none did exactly
what I wanted. For a start most of the ATL servers I was developing did not have a GUI
or a window associated with them. I wanted a utility class that I could derive from when
creating my ATL COM server that worked out of the box, and that handled all the complexities
of creating a hidden window, dealing with menus etc.
So what does this code do? Well this class allows you to display an icon of your choosing
in the shell, which can be changed as often as you like. You can update the tip text
associated with the icon when you like. You can respond to mouse events that are generated
over your tray icon. And, the class will display a popup menu of your choosing on demand.
You might want to check out other articles on Code Project concerning the shell. This
is just a very simple class to get support for the system tray into your ATL COM server
quickly. If nothing else then it's example code. This code was developed with Visual Studio
6.0, I haven't tested it in Visual Studio .NET.
Usage
Compile the Visual C++ project and make sure it registers the COM server that it builds.
This has a minimal COM server with some functions to demonstrate how to use the helper class.
Start the VB demo application ShellTest.exe; you will see an icon appear in the shell area.
You can enable or disable the icon by clicking on the "Visible in shell" checkbox. Type some text
into the tip edit box and click "Update", and the tip text will change.
Check the "animate" button and the icon will change. You can alter how often the icon
animates by entering a valid (say, 100) in the frequency edit box, and then click the
update button.
Right click on the shell icon. You will see a context menu appear. Select "About" and a dialog
will appear.
Using the CShellIconHelper class
The class has a number of functions that you'll want to call to work with the shell:
virtual void SetShellTipText (std::string &TipText)
Call SetShellTipText
to pass in the tip text you want displayed. If you specify more than
64 characters, it will be truncated. This is because the basic version of the common controls only
supports this number of characters.
virtual void SetShellIcon (WORD IconResource)
Pass in the resource ID of the icon (e.g. IDI_ICON1
) that you want displayed in the shell.
virtual void SetShellTimer (bool bEnabled, WORD wTimerDuration)
Call SetShellTimer
to enable a windows timer for your shell icon, if you want to use one. Your
ATL server class will then receive WM_TIMER
messages, which you can do some processing with.
You can also switch off a running timer by calling this with bEnabled = false
. The timer duration is specified
in milliseconds.
virtual void SetShellVisible (bool bVisible = true)
Calling SetShellVisible
with true
displays the icon in the shell, and if you have requested
to use the timer, starts it off. Calling this with false
removes the icon from the shell
and stops any timer.
virtual WORD ShowPopupMenu (WORD PopupMenuResource)
Call ShowPopupMenu
with the resource ID of your menu (e.g. IDR_POPUP_MENU
). It will
return back the menu ID that was selected, or zero if no menu item was selected.
How to use this code in your project.
I'm assuming you used the wizard to create an ATL COM server, and have added an ATL object to your project.
All the changes you'll need to make are to the header file of your ATL object.
Add the file ShellIconHelper.h to your workspace. This contains the class CShellIconHelper
.
In the header file for your COM server, add in the line #include "ShellIconHelper.h"
.
You might also need to change project settings for your COM server so that exceptions are
supported, as the helper class uses STL. Go to project/settings and select "C++ language"
category. Check the "Enable exception handling". If you really don't want exception handling
then you could just change the ShellIconHelper
class to use LPCSTR
instead of std::string
-
quite a trivial change.
Add the class CShellIconHelper
to the list of derived classes of your COM server class.
As it's a templated class you need to specify the name of the class in angle brackets,
in the same fashion as many of the other derived classes listed.
class ATL_NO_VTABLE CMyServer :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CMyServer, &CLSID_MyServer>,
public ISupportErrorInfo,
public IDispatchImpl<IMyServer, &IID_IMyServer,
&LIBID_SYSTEMTRAYDEMOLib>,
public CShellIconHelper<CMyServer>
Add a message map to this class (in the section with the other macro declarations),
so that your COM server handles windows messages correctly. In this case we want to respond
to timer messages and user commands (i.e. when the user moves the mouse over our shell icon).
BEGIN_MSG_MAP(CMyServer)
MESSAGE_HANDLER(WM_TIMER, OnTimer)
MESSAGE_HANDLER(WM_USER, OnUserCommand)
END_MSG_MAP()
If you don't want to use the timer or user commands, just leave out the MESSAGE_HANDLER
lines.
But you must have the BEGIN_MSG_MAP(...)
and END_MSG_MAP()
macros
in your class - otherwise it won't compile.
Override the method OnFinalConstruct
in this class. We'll set up the icon in the system
tray here. Note, you can choose to display the icon at any time once your server is loaded
(or not at all). I chose OnFinalConstruct
as it's a handy place to do this.
public:
HRESULT FinalConstruct()
{
SetShellTipText (std::string("Some tip text"));
SetShellIcon (IDI_ICON1);
SetShellVisible ();
SetShellTimer (true, 1000);
return S_OK;
}
Override the method OnFinalRelease
in this class. The code we add here removes the
icon from the system tray when the application shuts down. Note: You must include this
section of code, with the call to SetShellVisible (false)
otherwise your
application may well get an access violation during unloading.
public:
void FinalRelease ()
{
SetShellVisible (false);
}
Add a handler in for the timer messages. This gets called periodically, at the
frequency you specified in the call to SetShellTimer
. If you want to change
the icon or the text for your system tray icon, you should call SetShellIcon
and SetShellTipText
.
LRESULT OnTimer(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled)
{
SetShellIcon (IDI_ICON1);
SetShellTipText(std::string("Some text"));
return 0;
}
Add a handler in for user commands. This allows you to respond to mouse
events over your system tray icon. For example, you'll probably want to display a
popup menu when the user right clicks your icon, as shown here. The lParam
contains the message
that Windows has passed you. You call the function ShowPopupMenu
, with a Resource
ID of a menu you want displayed. This function returns the ID of the menu option
selected (as defined in the resource files for your project), or 0 if no menu option
is selected.
LRESULT OnUserCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL&bHandled)
{
if (lParam == WM_RBUTTONUP)
{
WORD cmd = ShowPopupMenu (IDR_POPUP_MENU);
}
return 0;
}
Detailed explanation of code
template <typename T>
class CShellIconHelper : public CWindowImpl<T>
The class CShellIconHelper
derives from the ATL class CWindowImpl
- this gives us our hidden window,
that we need to use to display the icon and to receive windows messages. If your ATL object
already derives from CWindowImpl
(maybe it's an ActiveX control), then you can remove this
derivation. CWindowImpl
gives us a public member m_hWnd
- this stores the window handle of our
hidden window.
void ShellNotify (DWORD msg)
{
m_CurrentText = m_CurrentText;
m_CurrentIconResource = m_CurrentIconResource;
NOTIFYICONDATA notifyIconData;
notifyIconData.cbSize = sizeof(notifyIconData);
notifyIconData.hWnd = m_hWnd;
notifyIconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
notifyIconData.uCallbackMessage = WM_USER;
notifyIconData.uID = 0;
notifyIconData.hIcon = ::LoadIcon(_Module.GetResourceInstance),
MAKEINTRESOURCE (m_CurrentIconResource));
::lstrcpyn(notifyIconData.szTip, m_CurrentText.c_str(), 64);
::Shell_NotifyIcon (msg, ¬ifyIconData);
}
ShellNotify
does the actual work of updating the shell icon and tip text. It's really quite
simple - just fill in the NOTIFYICONDATA
structure with the required information and call
Shell_NotifyIcon
. Note that the shell supports "balloon messages" on certain
versions of Windows with the newer version of the common controls.
I decided against supporting these, as I wanted the code to run on as many platforms
as possible.
virtual WORD ShowPopupMenu (WORD PopupMenuResource)
{
HMENU hMenu, hPopup = 0;
hMenu = ::LoadMenu (_Module.GetModuleInstance(),
MAKEINTRESOURCE (PopupMenuResource));
if (hMenu != 0)
{
POINT pt;
::GetCursorPos (&pt);
hPopup = ::GetSubMenu (hMenu, 0);
::SetForegroundWindow (m_hWnd);
WORD cmd = ::TrackPopupMenu (hPopup, TPM_RIGHTBUTTON | TPM_RETURNCMD,
pt.x, pt.y, 0, m_hWnd, NULL);
::PostMessage (m_hWnd, WM_NULL, 0, 0);
::DestroyMenu (hMenu);
return cmd;
}
return 0;
}
The only other noteworthy piece of code is the function ShowPopupMenu
, which displays and then
tracks a popup menu. Note that it gets a "sub menu" from the menu you have defined,
as you cannot display a menu bar. Also it calls SetForegroundWindow
before it tracks the
pop up menu; this is required so that if you click away from the popup menu without
selecting an item it will automatically disappear. You must also post a message to the window after tracking the popup menu;
this behavouir is "by design"! The function returns the menu item
you selected, or zero if you didn't select anything.
Futher development
I tested this project with an ATL DLL and ATL EXE server. I didn't try with a service - there are
other articles on Code Project you might want to look at for services. I also want to get this to work
under Visual Studio .NET. Finally the class should support "Balloon" tips.