西安二炮工程学院 俞俊军 张 毅
摘要
本文对如何将应用程序的图标加入到Windows的系统托盘中做了较为详细的介绍,
然后给出了一个C++类以方便的实现该功能,并在VC++6.0中给出了一个应用程序
实例来体现其具体实现过程。同时该应用程序实例还讲解了如何在托盘中实现动
画图标以及在程序中关闭计算机的技术。
关键词:系统托盘 动画图标
Windows98桌面的系统托盘位于任务栏的右侧,即Windows98桌面的右下方。它常
用来显示一些系统的状态。如:系统时间,音量控制以及其它的一些图标(依个
人机器安装的软件而不定),如下图为笔者的Windows98系统托盘。(图略)
常常能见到一些优秀的软件在运行后会将其应用程序图标加入到系统托盘中,如
金山词霸。如果能将自己编写的应用程序的图标也加入到系统托盘中,将会使你
的程序显得很有专业水准。
其实这并不困难,与系统托盘通信的函数只有一个:
Shell_NotifyIcon (UINT message, NOTIFYICONDATA &m_nid);
首先看一下该函数的两个参数。
第一个参数message可以取以下值:
NIM_ADD 向托盘中加入一个图标;
NIM_MODIFY 修改托盘中的图标
NIM_DELETE 从托盘中删除一个图标
第二个参数m_nid是NOTIFYICONDATA结构的一个引用。该结构的原型如下:
typedef struct _NOTIFYICONDATA
{
DWORD cbSize;// 结构的大小,必须在程序中给出
HWND hWnd;
//是你程序中将要接收托盘消息的窗口句柄
UINT uID;
// 应用程序中定义的托盘图标ID,此参数用作标识
UINT uFlags;
//设置属性,低三位有意义,0--7,如下:
//第一位//#define NIF_MESSAGE 0x1
// uCallbackMessage参数有效
//第二位//#define NIF_ICON 0x2 // hIcon参数有效
//第三位//#define NIF_TIP 0x4 // szTip参数有效
UINT uCallbackMessage;
// 自定义的消息ID值,一定不要与以有的消息ID相重。
HICON hIcon;
//显示在系统托盘上的Icon的句柄,可以为系统的 IDI_WINLOGO等
CHAR szTip[64]; // 用于图标显示的提示字符串
} NOTIFYICONDATA;
为了接收到来自托盘的通知消息你可以将uCallbackMessage设定为你所定义的消息
ID值,同时设定NIF_MESSAGE标志。这样当用户在你的托盘图标上移动或按下鼠标
时,Windows将发出消息:该消息的 messageID是你在uCallbackMessage中定义的
值;wParam是你定义的uID值;而lParam是鼠标事件(如WM_LBUTTONDOWN),这样你
的应用程序就能响应该事件了。
因此,为了将自己的应用程序加入到系统托盘中,首先得建立一处理托盘通知消息
的窗口对象,然后将窗口对象与你自己的托盘通知消息联系起来并建立相应的托盘
通知消息映射机制,以便你的窗口对象能处理相应的事件。
可以看到结构体NOTIFYICONDATA中,其成员变量hWnd,uID,uFlags均用于在窗口对
象与你自己的托盘通知消息之间建立联系,而成员变量uCallbackMessage则必须是
对应于你的窗口对象的托盘通知消息ID值。
于是要完成的工作有:
(1)建立一处理托盘通知消息的窗口对象;
(2)建立一结构体NOTIFYICONDATA变量,并给变量的相应域赋值以在托盘通知消
息与窗口对象之间建立联系;
(3)建立相应的托盘通知消息映射机制;
(4)调用Shell_NotifyIcon函数以在系统托盘中加入、修改或删除图标;
(5)当然别忘了在你的窗口对象中编写相应的事件响应函数。
因此,可以编写一C++类来实现以上功能以简化编程同时提高代码的可重用性。以
下为该类代码:
class CTrayIcon : public CCmdTarget {
protected:
DECLARE_DYNAMIC(CTrayIcon)
NOTIFYICONDATA m_nid;
// Shell_NotifyIcon 函数中的结构参数
public:
CTrayIcon(UINT uID);
~CTrayIcon();
// 通过调用该成员函数来接收托盘通知消息
void SetNotificationWnd(CWnd* pNotifyWnd,
UINT uCbMsg);
// SetIcon 函数用来在系统托盘中加入、改变及删除图标。
//要删除图标这样调用:SetIcon(0)
BOOL SetIcon(UINT uID);
BOOL SetIcon(HICON hicon, LPCSTR lpTip);
BOOL SetIcon(LPCTSTR lpResName, LPCSTR lpTip)
{
return SetIcon(lpResName ?
AfxGetApp()->LoadIcon(lpResName):NULL,lpTip);
}
BOOL SetStandardIcon(LPCTSTR lpszIconName,LPCSTR lpTip)
{
return SetIcon(::LoadIcon(NULL,lpszIconName),lpTip);
}
virtual LRESULT OnTrayNotification(WPARAM uID, LPARAM lEvent);
};
CTrayIcon::CTrayIcon(UINT uID)
{
//初始化NOTIFYICONDATA结构变量
memset(&m_nid, 0 , sizeof(m_nid));
m_nid.cbSize = sizeof(m_nid);
m_nid.uID = uID;
AfxLoadString(uID, m_nid.szTip, sizeof
(m_nid.szTip));
}
CTrayIcon::~CTrayIcon()
{
SetIcon(0); // 从系统托盘中删除图标
}
// 设定通知窗口,该窗口必须已被创建
void CTrayIcon::SetNotificationWnd(CWnd* pNotifyWnd, UINT uCbMsg)
{
ASSERT(pNotifyWnd==NULL || ::IsWindow(pNotifyWnd->GetSafeHwnd()));
m_nid.hWnd = pNotifyWnd->GetSafeHwnd();
ASSERT(uCbMsg==0 || uCbMsg>=WM_USER);
m_nid.uCallbackMessage = uCbMsg;
}
BOOL CTrayIcon::SetIcon(UINT uID)
{
HICON hicon=NULL;
if (uID) {
AfxLoadString(uID, m_nid.szTip, sizeof(m_nid.szTip));
hicon = AfxGetApp()->LoadIcon(uID);
}
return SetIcon(hicon, NULL);
}
//////////////////
//
BOOL CTrayIcon::SetIcon(HICON hicon, LPCSTR lpTip)
{
UINT msg;
m_nid.uFlags = 0;
// 设定图标
if (hicon) {
// 判断是要在系统托盘中增加还是要删除图标
msg = m_nid.hIcon ? NIM_MODIFY : NIM_ADD;
m_nid.hIcon = hicon;
m_nid.uFlags |= NIF_ICON;
} else { // 删除图标
if (m_nid.hIcon==NULL)
return TRUE; //已被删除
msg = NIM_DELETE;
}
if (lpTip)
strncpy(m_nid.szTip, lpTip, sizeof(m_nid.szTip));
if (m_nid.szTip[0])
m_nid.uFlags |= NIF_TIP;
if (m_nid.uCallbackMessage && m_nid.hWnd)
m_nid.uFlags |= NIF_MESSAGE;
BOOL bRet = Shell_NotifyIcon(msg, &m_nid);
if (msg==NIM_DELETE || !bRet)
m_nid.hIcon = NULL;
return bRet;
}
// 缺省事件处理程序,该程序处理鼠标右击及双击事件。
LRESULT CTrayIcon::OnTrayNotification(WPARAM wID,
LPARAM lEvent)
{
if (wID!=m_nid.uID ||
(lEvent!=WM_RBUTTONUP && lEvent!=WM_LBUTTONDBLCLK))
return 0;
// 使用与托盘图标拥有同样ID号的菜单作为右键弹出菜单
// 并将菜单上的第一项作为缺省命令使用,
// 缺省命令在WM_LBUTTONDBLCLK事件发生时被击发
//
CMenu menu;
if (!menu.LoadMenu(m_nid.uID))
return 0;
CMenu* pSubMenu = menu.GetSubMenu(0);
if (!pSubMenu)
return 0;
if (lEvent==WM_RBUTTONUP) {
//使菜单第一项为缺省项 (表现为粗体)
::SetMenuDefaultItem(pSubMenu->m_hMenu, 0, TRUE);
// 在鼠标的当前位置弹出菜单。
CPoint mouse;
GetCursorPos(&mouse);
::SetForegroundWindow(m_nid.hWnd);
::TrackPopupMenu(pSubMenu->m_hMenu,
0,
mouse.x,
mouse.y,
0,
m_nid.hWnd,
NULL);
} else // 双击事件: 执行菜单第一项
::SendMessage(m_nid.hWnd, WM_COMMAND, pSubMenu->
GetMenuItemID(0), 0);
return 1; // 表示事件已被处理
}
以下以在VC++6.0中具体实现的程序为例。该程序将拥有以下功能:程序被执行
后,首先显示一对话框表示程序开始执行,然后该对话框消失。接着程序图标
被加入到系统托盘中,可以看到,该图标将是一动画图标。当鼠标在该系统托
盘上右击时,将弹出一菜单。如图所示(略)。其第一项为缺省项命令,单击
将显示应用程序。为简化编程,该应用程序只是显示一应用程序主窗口。而单击
菜单第二项将关闭机器,单击菜单第三项将结束本程序。当并且当用户双击时,
CTrayIcon将执行菜单上的第一项:显示服务程序,这将击活(显示)TrayDemo
(正常情况下,它是隐藏的)。而要终止TrayDemo,你得选择结束本程序。当你
执行File Exit或关掉TrayDemo主窗口时,TrayDemo并没有真正的关掉,它只不过
隐藏起来了而已。TrayDemo 重载了Cmainframe::OnClose函数以执行该项功能。
首先在VC++6.0中生成用应用程序向导生成一单文档工程TrayDemo,然后在工程中
加入以上的CTrayIcon类。
要使用CTrayIcon类,你首先得实例化一个CTrayIcon类对象,TrayDemo在视图中
完成此项工作。以下是对应代码:
class CTrayDemoView : public CView {
protected: CTrayIcon m_trayIcon;
// my tray icon
.
.
.
};
当你实例化一个CTrayIcon类对象之后,你必须分配给其一个ID号。该ID号是此图
标在其生命周期内使用的唯一一个ID号,即使在以后你改变了实际显示的图标。此
ID号是当鼠标事件发生时你获得的ID。它可以不必是图标的资源ID;在TrayDemo
中,其值是IDR_TRAYICON,由CTrayDemoView构造函数所初始化。
CTrayDemoView::CTrayDemoView() :
m_trayIcon(IDR_TRAYICON){
.
.
.
}
要增加图标,可调用SetIcon重载函数之一
m_trayIcon.SetIcon(IDI_MYICON); //参数为资源ID
m_trayIcon.SetIcon("myicon"); //参数为资源名
m_trayIcon.SetIcon(hicon); //参数为HICON句柄
m_trayIcon.SetStandardIcon(IDI_WINLOGO);
//加入系统图标
除了SetIcon(UINT uID)函数需要一个同样拥有uID号的字符串资源作为提示字符串
以外,所有这些函数都有一个可选的指向提示字符串的LPCSTR参数。例如,在
TRAYTEST中有以下行:
// (In TrayDemoView.cpp)
m_trayIcon.SetIcon(IDI_RED);
该语句在增加图标的同时同样设定了提示字符串,因为TrayDemo有一个同样ID的字
符串:如果你想改变图标,只需再次调用其中的一个SetIcon函数,只不过需要不
同的ID或HICON。CTrayIcon类知道响应NIM_MODIFY消息而不是NIM_ADD消息。同样
的函数甚至可以去掉图标:
m_trayIcon.SetIcon(0);//removeicon
CtrayIcon类会将其解释为NIM_DELETE事件。这么多的代码和标志只用一个简单的
重载函数就予以完成,这是C++的伟大之处。
如果要显示动画图标,只需设置一定时器,然后在定时器的响应事件中调用
SetIcon成员函数就可以了。如:
int CTrayDemoView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
m_timerID = this->SetTimer(99,200,NULL);
…
}
void CTrayDemoView::OnTimer(UINT nIDEvent)
{
uChangeIcon++;
if(uChangeIcon-IDI_RED>2)
uChangeIcon=IDI_RED;
m_trayIcon.SetIcon(uChangeIcon);
CView::OnTimer(nIDEvent);
}
在示例程序中,有3个图标,其ID为IDI_RED,IDI_YELLO,IDI_GREEN,且其ID值是相
连的,因而UINT型变量uChangeIcon用来依次轮换三个图标。这样程序执行以后,你
将会看到红、黄、绿三个交通指示灯依次闪烁。
那么怎样处理托盘通知呢?
要处理托盘通知,需要在你设定图标之前调用CTrayIcon::SetNotificationWnd函
数,当然你必须已经创建了窗口。最适当的地方是在OnCreate函数中,在TrayDemo
中也是这样做的。用ClassWizard在CtrayDemoView类中加入WM_CREATE消息响应函
数OnCreate(),并加入以下代码:
// Private message used for tray notifications
#define WM_MY_TRAY_NOTIFICATION WM_USER+0
int CTrayDemoView::OnCreate(LPCREATESTRUCT lpCreateStruct)
.
.
.
m_trayIcon.SetNotificationWnd(this,WM_MY_TRAY_NOTIFICATION);
m_trayIcon.SetIcon(IDI_RED);
return 0;
}
然后进行消息注册(REGISTER),一旦注册以后,你就可以用正常的消息映射方式
处理托盘通知。
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_MESSAGE(WM_MY_TRAY_NOTIFICATION,OnTrayNotification)
// (or ON_REGISTERED_MESSAGE)
END_MESSAGE_MAP()
当然不要忘了在TrayDemoView.h中加入以下语句:
afx_msg LRESULT OnTrayNotification(WPARAM wp, LPARAM lp);
当你的处理程序得到在托盘图标上的鼠标事件的控制以后,WPARAM参数是你在创建
CTrayIcon类时定义的ID;LPARAM是鼠标事件(如,WM_LBUTTONDOWN)。当捕获到
通知后你可以做任何你想做的事情;记得最后要调用
CTrayIcon::OnTrayNotification函数以完成一些缺省的处理。该虚函数完成前面
所提到的一些缸省的UI行为。特别的,它处理WM_LBUTTONDBLCLK和WM-RBUTTONUP事
件。CTrayIcon类寻找与图标拥有同样ID的菜单(如,IDR_TRAYICON)。如果拥有
该ID的菜单存在,CTrayIcon类将在用户右击图标的时候显示此菜单;而当用户双
击时,CTrayIcon将执行菜单上的第一个命令。
LRESULT CTrayDemoView::OnTrayNotification(WPARAM wp, LPARAM lp)
{
return m_trayIcon.OnTrayNotification(wp, lp);
}
只有两件事需要进一步解释。在显示菜单之前,CTrayIcon类使得第一项为缸省项,
因此它看起来是大写的。但怎样使得一个菜单项大写呢?使用函数
GSetMenuDefaultItem。
// Make first menu item the default (bold font)
::SetMenuDefaultItem(pSubMenu->m_hMenu, 0, TRUE);
这里的0便指定了第一个菜单项,TRUE表示通过位置而不是ID来确定菜单项。
对CTrayIcon::OnTrayNotification,我们关心的第二项是为了显示相关菜单,它干
了些什么?
::SetForegroundWindow(m_nid.hWnd);
::TrackPopupMenu(pSubMenu->m_hMenu, ...);
为了使TrackPopupMenu函数在托盘环境中工作正常,你必须首先在拥有该弹出菜单
的窗口中调用SetForegroundWindow函数。否则,当用户按下Esc键或在菜单以外单
击鼠标时该菜单将不会消失。正如你看到的那样,CTrayIcon类使得托盘图标的编
程很简单。为了使托盘菜单生效,在TrayDemo中所做的只是实现一个通知程序,在
该程序中调用了CTrayIcon::OnTrayNotification,对了别忘了还要提供一个与
CTrayIcon类拥有同样ID的菜单。TrayDemo程序中是在菜单编辑器内加入一ID为
IDR_TRAYICON的如下菜单:
然后,用ClassWizard在视图类中分别为三个菜单命令加入如下的响应函数:
void CTrayDemoView::OnDisplayProgram()
{
CWnd* pWnd;
pWnd=AfxGetApp()->m_pMainWnd;
pWnd->ShowWindow(SW_NORMAL);
pWnd->SetForegroundWindow();
}
void CTrayDemoView::OnCloseProgram()
{
m_bShutdown = TRUE; // really exit
CWnd* pWnd;
pWnd=AfxGetApp()->m_pMainWnd;
pWnd->SendMessage(WM_CLOSE);
}
void CTrayDemoView::OnShutoff()
{
ExitWindowsEx(EWX_SHUTDOWN,0);
}
其中,在OnShutoff函数中,ExitWindowsEx(EWX_SHUTDOWN,0)用来关闭计算机。限
于篇幅,这里不作详细介绍,读者可以查看MSDN来获得更详细的资料。
最后,还要重载Cmainframe::OnClose函数如下:
void CMainFrame::OnClose()
{
CTrayDemoView *pView =
(CTrayDemoView *)GetActiveView();
if (pView->m_bShutdown)
CFrameWnd::OnClose();
else
ShowWindow(SW_HIDE);
}
提醒一点,为使框架程序识别视图类,还要在MainFrm.cpp中加入如下两句:
#include "TrayDemoDoc.h"
#include "TrayDemoView.h"
如果有兴趣,还可以对将本程序继续扩充,使之可以监视系统的状态:当鼠标和键
盘在超过一设定的时间后,仍没有动作,则程序将自动执行关机命令。
以上程序在Windows98,VC++6.0中调试通过。