下载本文例子源代码
自从 Windows 95 面市以来,系统托盘应用作为一种极具吸引力的 UI 深受广大用户的喜爱。使用系统托盘 UI 的 Windows 应用程序数不胜数,比如 " 金山词霸 " 、 "Winamp" 、 "RealPlayer" 等等。那么如何编写自己的托盘应用呢?本文是系列文章中的第一篇,这些文章将比较系统地描述托盘应用的编程。并创建自己的 C++ 类来增强系统托盘应用的特性。读完这些文章,再参照例子,相信读者能轻松自如地在自己的程序中应用系统托盘。
大家知道, MFC 框架没有提供任何现成的类应用于系统托盘 UI ,那么如何将表示应用程序的图标添加到任务栏中呢?方法很简单,只用到一个 API 函数,它就是 Shell_NotifyIcon 。这个函数本身也相当容易理解和使用。看看它的原型就知道了:
自从 Windows 95 面市以来,系统托盘应用作为一种极具吸引力的 UI 深受广大用户的喜爱。使用系统托盘 UI 的 Windows 应用程序数不胜数,比如 " 金山词霸 " 、 "Winamp" 、 "RealPlayer" 等等。那么如何编写自己的托盘应用呢?本文是系列文章中的第一篇,这些文章将比较系统地描述托盘应用的编程。并创建自己的 C++ 类来增强系统托盘应用的特性。读完这些文章,再参照例子,相信读者能轻松自如地在自己的程序中应用系统托盘。
大家知道, MFC 框架没有提供任何现成的类应用于系统托盘 UI ,那么如何将表示应用程序的图标添加到任务栏中呢?方法很简单,只用到一个 API 函数,它就是 Shell_NotifyIcon 。这个函数本身也相当容易理解和使用。看看它的原型就知道了:
BOOL Shell_NotifyIcon( DWORD dwMessage, PNOTIFYICONDATA pnid);
第一个参数
dwMessage
类型为
DWORD
,表示要进行的动作,它可以是下面的值之一:
NIM_ADD
: 添加一个图标到任务栏。
NIM_MODIFY
: 修改状态栏区域的图标。
NIM_DELETE
: 删除状态栏区域的图标。
NIM_SETFOCUS
: 将焦点返回到任务栏通知区域。当完成用户界面操作时,任务栏图标必须用此消息。例如,如果任务栏图标正显示上下文菜单,但用户按下"ESCAPE"键取消操作,这时就必须用此消息将焦点返回到任务栏通知区域。
NIM_SETVERSION
:指示任务栏按照相应的动态库版本工作。
第二个参数
pnid
是
NOTIFYICONDATA
结构的地址,其内容视
dwMessage
的值而定。这个结构在
SHELLAPI.H
文件中定义如下:
typedef struct _NOTIFYICONDATA {
DWORD cbSize; //
结构大小(sizeof struct),必须设置
HWND hWnd; //
发送通知消息的窗口句柄
UINT uID; //
图标ID ( 由回调函数的WPARAM 指定)
UINT uFlags;
UINT uCallbackMessage; //
消息被发送到此窗口过程
HICON hIcon; //
图标句柄
CHAR szTip[64]; //
提示文本
}
NOTIFYICONDATA;uFlags
的值:
#define NIF_MESSAGE 0x1 //
表示uCallbackMessage 有效
#define NIF_ICON 0x2 //
表示hIcon 有效
#define NIF_TIP 0x4 //
表示szTip 有效
有关
Shell_NotifyIcon
函数的详细使用细节请参考
MSDN
。
NOTIFYICONDATA
结构中的
hWnd
是
"
拥有
"
图标的窗口句柄。
uID
可以是任何标示托盘图标的
ID
(如果有多个图标),一般使用资源
ID
。
HIcon
可以是任何图标的句柄,包括预定义的系统图标,如
IDI_HAND
、
IDI_QUESTION
、
IDI_EXCLAMATION
、或者
Windows
的徽标
IDI_WINLOGO
。
图标的显示并不难,关键是事件的处理。 当用户将鼠标移到图标上或者在图标上单击鼠标时,为了得到通知消息,你可以将自己的消息 ID 赋给 uCallbackMessage ,并设置 NIF_MESSAGE 标志。当用户在图标上移动或单击鼠标时, Windows 将用 hWnd 指定的窗口句柄调用你建立的窗口过程;消息 ID 在 uCallbackMessage 中指定, uID 的值即为 wParam , lParam 为鼠标事件,如 WM_LBUTTONDOWN 等。
尽管 Shell_NotifyIcon 函数简单实用。但它毕竟是个 Win32 API ,为此我将它封装在了一个 C++ 类中,这个类叫做 CTrayIcon ,有了它,托盘编程会更加轻松自如,因为它隐藏了 NOTIFYICONDATA 、消息代码、标志以及所有那些你必须要看 MSDN 才能搞掂的繁琐细节。 CTrayIcon 的定义以及实现细节请下载源代码参考。 CTrayIcon 为程序员提供了一个更加友好的托盘编程接口,它除了对 Shell_NotifyIcon 函数进行打包之外,它还是一个迷你框架呢!之所以这么说,是因为按照 Windows 系统应用软件界面指南所提倡的原则(这个指南可以在 MSDN 中找到),这个类增强了托盘图标的用户界面行为。以下便是 CTrayIcon 最终实现的 UI 特性:
1 、 托盘图标应该有信息提示,也就是 ToolTips 。
2 、 单击右键应该弹出上下文菜单,这个菜单中应包含打开属性页的命令或者打开与图标相关的其它窗口的命令。
3 、 单击左键应该显示进一步的信息或者控制图标所代表的对象,例如,当左键单击声音图标时进行音量控制。如果没有进一步的信息或控制,则不要有任何动作。
CTrayIcon 对上面的特性进行了全面的封装。为了示范 CTrayIcon 的工作原理,本文提供一个例子程序 TrayTest1 ,图一是运行程序后显示的一个对话框:
图一 TrayTest1 运行后显示的对话框
当把图标安装到系统托盘之后,如果双击托盘图标,程序会弹出一个消息列表窗口,只要你的鼠标在托盘图标上移动或点击(无论是左右键的单击或双击),产生的消息都会显示在这个窗口里,如图二:
图二 消息显示窗口
当鼠标光标移到托盘图标上时,在图标附近会显示提示信息,如图三:
图三 显示 Tooltip
为了正确使用 CTrayIcon ,首先你必须在程序的某个地方实例化 CTrayIcon ,例子程序是在主框架中创建 CTrayIcon 实例的。
图标的显示并不难,关键是事件的处理。 当用户将鼠标移到图标上或者在图标上单击鼠标时,为了得到通知消息,你可以将自己的消息 ID 赋给 uCallbackMessage ,并设置 NIF_MESSAGE 标志。当用户在图标上移动或单击鼠标时, Windows 将用 hWnd 指定的窗口句柄调用你建立的窗口过程;消息 ID 在 uCallbackMessage 中指定, uID 的值即为 wParam , lParam 为鼠标事件,如 WM_LBUTTONDOWN 等。
尽管 Shell_NotifyIcon 函数简单实用。但它毕竟是个 Win32 API ,为此我将它封装在了一个 C++ 类中,这个类叫做 CTrayIcon ,有了它,托盘编程会更加轻松自如,因为它隐藏了 NOTIFYICONDATA 、消息代码、标志以及所有那些你必须要看 MSDN 才能搞掂的繁琐细节。 CTrayIcon 的定义以及实现细节请下载源代码参考。 CTrayIcon 为程序员提供了一个更加友好的托盘编程接口,它除了对 Shell_NotifyIcon 函数进行打包之外,它还是一个迷你框架呢!之所以这么说,是因为按照 Windows 系统应用软件界面指南所提倡的原则(这个指南可以在 MSDN 中找到),这个类增强了托盘图标的用户界面行为。以下便是 CTrayIcon 最终实现的 UI 特性:
1 、 托盘图标应该有信息提示,也就是 ToolTips 。
2 、 单击右键应该弹出上下文菜单,这个菜单中应包含打开属性页的命令或者打开与图标相关的其它窗口的命令。
3 、 单击左键应该显示进一步的信息或者控制图标所代表的对象,例如,当左键单击声音图标时进行音量控制。如果没有进一步的信息或控制,则不要有任何动作。
CTrayIcon 对上面的特性进行了全面的封装。为了示范 CTrayIcon 的工作原理,本文提供一个例子程序 TrayTest1 ,图一是运行程序后显示的一个对话框:
图一 TrayTest1 运行后显示的对话框
当把图标安装到系统托盘之后,如果双击托盘图标,程序会弹出一个消息列表窗口,只要你的鼠标在托盘图标上移动或点击(无论是左右键的单击或双击),产生的消息都会显示在这个窗口里,如图二:
图二 消息显示窗口
当鼠标光标移到托盘图标上时,在图标附近会显示提示信息,如图三:
图三 显示 Tooltip
为了正确使用 CTrayIcon ,首先你必须在程序的某个地方实例化 CTrayIcon ,例子程序是在主框架中创建 CTrayIcon 实例的。
Class MainFrame public CFrameWnd {protected: CTrayIcon m_trayIcon; // my tray icon…….};
然后,你必须提供一个
ID
。这是在图标生命期内的唯一标示,即便以后你修改了要显示的图标。这个
ID
也是鼠标事件发生时你将获得的
ID
。它不一定必须是图标的资源
ID
,例子程序中这个
ID
为
IDR_TRAYICON
,由框架的构造函数
CMainFrame
通过成员初始化列表对
m_trayIcon
进行初始化:
CMainFrame::CMainFrame() : 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)
之外,这些函数都有一个
LPCSTR
类型的可选参数用于指定提示文本。
SetIcon(UINT uID)
使用
ID
与
uID
相同的串资源作为提示文本。例如,
TrayTest1
有一行代码是这样的:
// (
在mainframe.cpp文件中)m_trayIcon.SetIcon(IDI_MYICON);
这行代码也设置了提示信息,因为
TrayTest1
有一个串资源,其
ID
也是
IDI_MYICON
。这在
TRAYTEST.RC
文件中可以看到:
STRINGTABLE PRELOAD DISCARDABLE BEGIN IDI_MYICON "
双击图标激活 TRAYTEST." END
如果你想改变图标,可以用不同的
ID
或者
HICON
再次调用
SetIcon
函数之一。
CTrayTest
便会用
NIM_MODIFY
而不是
NIM_ADD
来改变图标。相同的函数甚至可以用于删除图标,如:
m_trayIcon.SetIcon(0); //
删除图标
CTrayIcon
将此代码解释成
NIM_DELETE
。你已经看到,所有这些表示行为的编码,标志都被一个使用方便的函数所替代:这都归功于
C++
!现在,我们来看看如何处理通知消息以及前面提到的所有
UI
特性。通知消息的处理必须要设置图标之前,但是要在创建窗口之后调用
CTrayIcon::SetNotificationWnd
,做这件事情的最佳场所是在
OnCreate
处理例程中,
TrayTest
就是在这里处理的:
//
注册用于托盘的自定义消息#define WM_MY_TRAY_NOTIFICATION WM_USER+0int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct){…… // 请通知我
m_trayIcon.SetNotificationWnd(this, WM_MY_TRAY_NOTIFICATION);
m_trayIcon.SetIcon(IDI_MYICON);
return 0;
}
消息一旦注册,接下来你便可以用通常的消息映射方式处理托盘通知消息。
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_MESSAGE(WM_MY_TRAY_NOTIFICATION, OnTrayNotification)
// (or ON_REGISTERED_MESSAGE)
END_MESSAGE_MAP()
LRESULT CMainFrame::OnTrayNotification(WPARAM wp, LPARAM lp){
…… //
显示消息……return m_trayIcon.OnTrayNotification(wp, lp);}
当消息处理器得到控制,
WPARAM
的值是在构造
CTrayIcon
时指定的
ID
;
LPARAM
为鼠标事件(如
WM_LBUTTONDOWN
)。当你得到通知消息后,可以做任何想做的的事情;例子程序
TrayTest
此时是显示通知信息,细节请参考源代码。完成消息的处理之后,调用
CTrayIcon::OnTrayNotification
进行缺省处理。此虚拟函数(所以你可以改写)实现我前面提到过的缺省的
UI
行为。尤其是处理
WM_LBUTTONDBLCLK
和
WM_RBUTTONUP
。
CTrayIcon
寻找与图标
ID
相同的某个菜单(如
IDR_TRAYICON
),如果找到,则当用户右键单击图标时
CTrayIcon
显示这个菜单;当用户数双击图标时,
CTrayIcon
执行第一个菜单命令。只有两件事情需要进一步交待:
第一件事情是:在显示菜单之前, CTrayIcon 让第一个菜单项为默认,所以它以黑体显示。但如何用黑体来显示某个菜单项呢?我在 /MSDEV/INCLUDE/*.H 搜索了一番,发现了 Get/SetMenuDefaultItem 。这个函数没有相关的 CMenu 打包类,所以我必须直接调用它们。
第一件事情是:在显示菜单之前, CTrayIcon 让第一个菜单项为默认,所以它以黑体显示。但如何用黑体来显示某个菜单项呢?我在 /MSDEV/INCLUDE/*.H 搜索了一番,发现了 Get/SetMenuDefaultItem 。这个函数没有相关的 CMenu 打包类,所以我必须直接调用它们。
//
让第一个菜单项为默认(黑体):::SetMenuDefaultItem(pSubMenu->m_hMenu, 0, TRUE);
这里
0
表示第一个菜单项,
TRUE
说明用位置表示菜单项的
ID
。为什么
MFC
没有打包
Get/SetMenuDefaultItem
函数呢?微软的家伙们解释那是因为这些函数(其它的还有
::Get/SetMenuItemInfo, ::LoadImage
等)还没有在最新的
Windows
版本中实现。一旦在最新的
Windows
版本中实现了,便会马上添加到
MFC
中。
第二件事情是上下文菜单的显示:
第二件事情是上下文菜单的显示:
::SetForegroundWindow(m_nid.hWnd); ::TrackPopupMenu(pSubMenu->m_hMenu, ...);
为了让
TrackPopupMenu
在托盘的上下文中正确运行,你必须首先调用
SetForegroundWindow
,否则,当用户按下
ESCAPE
键或者在菜单之外单击鼠标时,菜单不会消失。为解决这个问题,我花费了数个小时,最后还是在
MSDN
上找到了解决方法。为了解详情,请参考
MSDN
的
Q135788
。最让我哭笑不得的是我花了那么多时间来关注这个问题,最后微软的这帮家伙在
MSDN
上给你来了一个问题的结论是:
“This behavior is by design.....”
真是气刹人也。
正如你所看到的, CTrayIcon 使得托盘应用的编程变得易如反掌。 TrayTest1 要做的事情不外乎调用 CTrayIcon::OnTrayNotification 实现一个通知消息处理器,提供一个与图标 ID 相同的菜单。就这么简单。
正如你所看到的, CTrayIcon 使得托盘应用的编程变得易如反掌。 TrayTest1 要做的事情不外乎调用 CTrayIcon::OnTrayNotification 实现一个通知消息处理器,提供一个与图标 ID 相同的菜单。就这么简单。
// (TRAYTEST.RC
文件)IDR_TRAYICON MENU DISCARDABLE
BEGIN POPUP "
托盘(&T)"
BEGIN
MENUITEM "
打开(&O)", ID_APP_OPEN
MENUITEM "
关于 TrayTest(&A)...", ID_APP_ABOUT
MENUITEM SEPARATOR
MENUITEM "
退出TrayTest 程序(&S)", ID_APP_SUSPEND
END
END
当用户在托盘图标上单击右键,
CTrayIcon
显示这个菜单,如图四所示。如果用户双击图标,
CTrayIcon
执行第一个菜单命令:
“
打开
”
,此时激活
TrayTest
(正常状态下是隐藏的)。为了终止
TrayTest1
,你必须选择
"Suspend TRAYTEST"
菜单项。如果你从
“
文件
|
退出
”
退出,或者关闭
TrayTest1
主窗口,
TrayTest1
不会真正关闭,它只是将自己隐藏起来。这个行为是
TrayTest1
改写了
CMainframe::OnClose
实现的。
图四 TRAYTEST1 托盘图标菜单
最后,我想说明一个很让人担心的问题,每个人在看到这个小图标后都想尽快的在自己的程序中加入托盘图标。作为程序员,这完全是可以理解的。当自己的程序中成功添加了托盘图标,在朋友们中间炫耀一番,那种感觉确实很好。但是要记住:并不是所有的应用都需要用托盘图标,如果不是必须就不要画蛇添足,否则托盘图标太多必然造成屏幕垃圾,看看下面图五吧:
图五 托盘图标程序 “ 噩梦版 ”
看到这么多的托盘图标对于用户来说简直就是噩梦。
图四 TRAYTEST1 托盘图标菜单
最后,我想说明一个很让人担心的问题,每个人在看到这个小图标后都想尽快的在自己的程序中加入托盘图标。作为程序员,这完全是可以理解的。当自己的程序中成功添加了托盘图标,在朋友们中间炫耀一番,那种感觉确实很好。但是要记住:并不是所有的应用都需要用托盘图标,如果不是必须就不要画蛇添足,否则托盘图标太多必然造成屏幕垃圾,看看下面图五吧:
图五 托盘图标程序 “ 噩梦版 ”
看到这么多的托盘图标对于用户来说简直就是噩梦。
下载本文例子源代码
在本文的 第一部分 ,我们讨论并示范了如何在自己的程序中应用系统托盘图标。通过使用自己创建的一个可重用的 C++ 类 ??CTrayIcon ,我们可以轻松地实现托盘程序。不久以前我用这个类编写了一个程序,开始运行很正常,但是有一次不知什么原因 Windows 资源管理器死掉了,也就是说非正常关闭,重启资源管理器后,发现托盘程序仍然在运行,但托盘图标显示不出来,在任务栏中看不到托盘图标,只有重新启动机器才能重新显示出托盘图标,让人觉得心里很不舒服,有没有什么办法在这个时候不用重启机器而让 Windows 自动找回托盘图标并把它添加到任务栏呢?,也就是让它自动恢复托盘图标程序的用户界面,经过一番研究,终于有所收获。
实际上,如果你用的操作系统是 Windows 98 或者你安装了 IE4.0 的桌面。那么不论什么时候,只要 IE4.0 启动了任务栏,那么它就会向所有最顶层父窗口广播一个注册消息: TaskbarCreated 。有了这个线索,你就可以重新创建图标。如果你用 MFC 编程,那么只要定义一个全程变量保存这个注册消息并实现 ON_REGISTERED_MESSAGE 消息处理例程即可:
在本文的 第一部分 ,我们讨论并示范了如何在自己的程序中应用系统托盘图标。通过使用自己创建的一个可重用的 C++ 类 ??CTrayIcon ,我们可以轻松地实现托盘程序。不久以前我用这个类编写了一个程序,开始运行很正常,但是有一次不知什么原因 Windows 资源管理器死掉了,也就是说非正常关闭,重启资源管理器后,发现托盘程序仍然在运行,但托盘图标显示不出来,在任务栏中看不到托盘图标,只有重新启动机器才能重新显示出托盘图标,让人觉得心里很不舒服,有没有什么办法在这个时候不用重启机器而让 Windows 自动找回托盘图标并把它添加到任务栏呢?,也就是让它自动恢复托盘图标程序的用户界面,经过一番研究,终于有所收获。
实际上,如果你用的操作系统是 Windows 98 或者你安装了 IE4.0 的桌面。那么不论什么时候,只要 IE4.0 启动了任务栏,那么它就会向所有最顶层父窗口广播一个注册消息: TaskbarCreated 。有了这个线索,你就可以重新创建图标。如果你用 MFC 编程,那么只要定义一个全程变量保存这个注册消息并实现 ON_REGISTERED_MESSAGE 消息处理例程即可:
const UINT WM_TASKBARCREATED = ::RegisterWindowMessage(_T("TaskbarCreated"));
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_REGISTERED_MESSAGE(WM_TASKBARCREATED, OnTaskBarCreated)
END_MESSAGE_MAP(CMainFrame, CFrameWnd)
The handler itself should reinstall whatever icons you need.
LRESULT CMainFrame::OnTaskBarCreated(WPARAM wp, LPARAM lp){
VERIFY(InstallIcons()); return 0;}
BOOL CMainFrame::InstallIcons(){
NOTIFYICONDATA nid; //
准备 nid 参数
return Shell_NotifyIcon(NIM_ADD, &nid);
难道就这么容易吗?当然不是,你必须单独实现
InstallIcons
函数,而不是直接从
OnTaskBarCreated
中调用
Shell_NotifyIcon
,因为一般来说,在应用程序启动的时候你也会调用它。
当我搞清楚 TaskbarCreated 的奥秘之后,我便回到本文第一部分中创建的那个 CTrayIcon 类,对它进行了修改和完善,改动不是很大。这个类管理的是单个的托盘图标(如果你有多个,那么为每个图标创建一个单独的实例吧)。使用 CTrayIcon 时,你只要在 CMainFrame 中创建一个实例 m_trayIcon 并象下面这样安装它即可:
当我搞清楚 TaskbarCreated 的奥秘之后,我便回到本文第一部分中创建的那个 CTrayIcon 类,对它进行了修改和完善,改动不是很大。这个类管理的是单个的托盘图标(如果你有多个,那么为每个图标创建一个单独的实例吧)。使用 CTrayIcon 时,你只要在 CMainFrame 中创建一个实例 m_trayIcon 并象下面这样安装它即可:
//
在CMainFrame::OnCreate中
m_trayIcon.SetNotificationWnd(this,WM_MY_TRAY_NOTIFICATION);m_trayIcon.SetIcon(IDI_MYICON);
WM_MY_TRAY_NOTIFICATION
是自己的私有消息,只在有事件发生时,托盘图标才发送此消息,如用户单击图标。对于托盘图标来说,你不用对它做什么处理,
CTrayIcon
全权负责。只是在构造
CTrayIcon
对象时,你给它一个资源
ID
就可以了。
CTrayIcon
用这个
ID
查找上下文菜单,如果找到的话,它便自动处理右键单击托盘图标事件,这时弹出上下文菜单。如果用户双击图标,
CTrayIcon
则执行第一个菜单项命令。所以你要做的全部工作只是创建一个菜单资源,将
ID
传递给构造函数,然后去驱动
CTrayIcon
就可以了。如果你想要一些非标准的例程,那么只需处理
WM_MY_TRAY_NOTIFICATION
即可。有关细节请参考源代码。
为了实现图标自动重建特性,我使用了以前很多文章中都用到的一个类 CSubclassWnd ,这个类在我的编程生涯中几乎无处不在。没有了它,我几乎寸步难行;在我的所有编程诀窍中,它是最有用的一个类。用这个类可以截获 Windows 消息,并将它发送到另外一个窗口消息处理例程中处理,而不是发送到 Windows 自己默认的消息处理函数。 CTrayIcon 用它来截获 TaskbarCreated 消息,所以你就不必为此编写消息处理器 ?? 这个方法非常酷。 CSubclassWnd 通过安装它自己的窗口过程,从而先于 MFC 一步对消息进行处理。
当 CMainFrame 调用 CTrayIcon::SetNotificationWnd 时, CTrayIcon 调用 GetTopLevelParent 以获取顶层父窗口,然后安装 CSubclassWnd 窗口过程,这相当于一个钩子,在消息传到任何顶层窗口之前, CSubclassWnd 的窗口过程先将它处理掉。这里必须明白一点: Windows 只将 TaskbarCreated 消息发送到顶层父窗口。它有点象 WM_QUERYNEWPALETTE 、 WM_SYSCOLORCHANGE 消息以及其它顶层窗口消息。现在当 Windows 发送 TaskbarCreated 时,控制将首先被传递到 CTrayIcon::CTrayHook::WindowProc 。
CTrayIcon::OnTaskBarCreate 是 CTrayIcon 类中新加的一个虚拟函数,其默认实现是在系统托盘中重新安装图标。如果你想做一些其它的事情,可以派生一个新类并改写此默认行为。实际的 WindowProc 代码比我在本文中描述的要稍微复杂一些,因为它还要处理托盘通知以驱动默认的菜单处理例程,以前,当获得 WM_MY_TRAY_NOTIFICATION 消息时,你必须自己调用 OnTrayNotification 。新的 CTrayIcon 实现请参考本文的源代码。
顺便提及一下,你想知道我是如何测试图标自动重建特性的吗?(如果是你,你用什么办法?),很容易。在 Windows 98 中,按下 Ctrl+Alt+Del ,弹出任务管理器的任务清单。选择 " 资源管理器( Explorer ) " ,然后选择 " 结束任务( End Task ) " 。此时会显示关机 / 重启对话框,此时按下 " 取消 " 按钮(不要按 " 关机 " 按钮)。然后,等待几秒钟,你得到消息显示 " 这个任务没有响应 " ,这时你要用 " 结束任务 " 来回复。接下来 Windows 的资源管理器便非正常终止!然后它又会自动起来,这时向所有顶层父窗口广播 TaskbarCreated 消息。如果你的操作系统是 Windows NT/2000/XP ,并安装了 IE4.0 ,如法炮制。当任务栏死掉后,从 " 开始 " 菜单的 " 运行 " 对话框中敲入 "explorer" 启动器源管理器。不管是用什么操作系统,在杀掉任务栏之前,如果 TrayTest 程序处于运行状态,那么当资源管理器自己恢复状态时, TrayTest 程序的托盘图标会自动创新安装。不信的话,你用本文第一部分的例子程序和本文提供的范例程序试一下就知道了。你会发现本文的例子程序会处理 TaskbarCreated 消息,而另一个则完全不会。
为了实现图标自动重建特性,我使用了以前很多文章中都用到的一个类 CSubclassWnd ,这个类在我的编程生涯中几乎无处不在。没有了它,我几乎寸步难行;在我的所有编程诀窍中,它是最有用的一个类。用这个类可以截获 Windows 消息,并将它发送到另外一个窗口消息处理例程中处理,而不是发送到 Windows 自己默认的消息处理函数。 CTrayIcon 用它来截获 TaskbarCreated 消息,所以你就不必为此编写消息处理器 ?? 这个方法非常酷。 CSubclassWnd 通过安装它自己的窗口过程,从而先于 MFC 一步对消息进行处理。
当 CMainFrame 调用 CTrayIcon::SetNotificationWnd 时, CTrayIcon 调用 GetTopLevelParent 以获取顶层父窗口,然后安装 CSubclassWnd 窗口过程,这相当于一个钩子,在消息传到任何顶层窗口之前, CSubclassWnd 的窗口过程先将它处理掉。这里必须明白一点: Windows 只将 TaskbarCreated 消息发送到顶层父窗口。它有点象 WM_QUERYNEWPALETTE 、 WM_SYSCOLORCHANGE 消息以及其它顶层窗口消息。现在当 Windows 发送 TaskbarCreated 时,控制将首先被传递到 CTrayIcon::CTrayHook::WindowProc 。
CTrayIcon::OnTaskBarCreate 是 CTrayIcon 类中新加的一个虚拟函数,其默认实现是在系统托盘中重新安装图标。如果你想做一些其它的事情,可以派生一个新类并改写此默认行为。实际的 WindowProc 代码比我在本文中描述的要稍微复杂一些,因为它还要处理托盘通知以驱动默认的菜单处理例程,以前,当获得 WM_MY_TRAY_NOTIFICATION 消息时,你必须自己调用 OnTrayNotification 。新的 CTrayIcon 实现请参考本文的源代码。
顺便提及一下,你想知道我是如何测试图标自动重建特性的吗?(如果是你,你用什么办法?),很容易。在 Windows 98 中,按下 Ctrl+Alt+Del ,弹出任务管理器的任务清单。选择 " 资源管理器( Explorer ) " ,然后选择 " 结束任务( End Task ) " 。此时会显示关机 / 重启对话框,此时按下 " 取消 " 按钮(不要按 " 关机 " 按钮)。然后,等待几秒钟,你得到消息显示 " 这个任务没有响应 " ,这时你要用 " 结束任务 " 来回复。接下来 Windows 的资源管理器便非正常终止!然后它又会自动起来,这时向所有顶层父窗口广播 TaskbarCreated 消息。如果你的操作系统是 Windows NT/2000/XP ,并安装了 IE4.0 ,如法炮制。当任务栏死掉后,从 " 开始 " 菜单的 " 运行 " 对话框中敲入 "explorer" 启动器源管理器。不管是用什么操作系统,在杀掉任务栏之前,如果 TrayTest 程序处于运行状态,那么当资源管理器自己恢复状态时, TrayTest 程序的托盘图标会自动创新安装。不信的话,你用本文第一部分的例子程序和本文提供的范例程序试一下就知道了。你会发现本文的例子程序会处理 TaskbarCreated 消息,而另一个则完全不会。
下载源代码
托盘程序的信息提示通常是将鼠标光标移到托盘图标上之后, Windows 会发送消息给托盘程序,从而显示提示信息 ??Tooltip 。但在 Windows XP 中我们还看到有些系统托盘程序是自动显示 ToolTips 信息的,也就是说不用将鼠标光标移到托盘图标上便可显示 ToolTips ,在这是怎么实现的呢?本文将示范一种新奇的 ToolTips 风格,它叫做气球提示: Balloon Tips 。
Windows 中与托盘图标相关的提示有两类:一类是传统的信息提示方式,当光标移到图标上时显示;另一类是新式的信息提示即气球提示,它是由你的程序来控制显示。气球提示有点像连环漫画中的文字气球。如图一、图二分别为本文例子程序 TrayTest3 在 Windows 2000 和 Windows XP 中运行的画面:
在 Windows 2000 中运行的气球提示,没有关闭按钮。
图一 在 Windows 2000 运行
在 Windows XP 中运行。
图二 在 Windows XP 中运行的 TrayTest3
气球提示为托盘程序提供了一种非打扰式的方法通知用户发生了某件事情。但是如何让气球提示显示出来呢?所有的托盘图标行为都是通过一个单纯的 API 函数 Shell_NotifyIcon 来操作的。这个函数的一个参数是 NOTIFYICONDATA 结构,你可以利用这个结构来告诉 Windows 你想要做什么。下面是这个结构的定义的最新版本( For IE5.0+ ),其中已经加入了新的成员:
托盘程序的信息提示通常是将鼠标光标移到托盘图标上之后, Windows 会发送消息给托盘程序,从而显示提示信息 ??Tooltip 。但在 Windows XP 中我们还看到有些系统托盘程序是自动显示 ToolTips 信息的,也就是说不用将鼠标光标移到托盘图标上便可显示 ToolTips ,在这是怎么实现的呢?本文将示范一种新奇的 ToolTips 风格,它叫做气球提示: Balloon Tips 。
Windows 中与托盘图标相关的提示有两类:一类是传统的信息提示方式,当光标移到图标上时显示;另一类是新式的信息提示即气球提示,它是由你的程序来控制显示。气球提示有点像连环漫画中的文字气球。如图一、图二分别为本文例子程序 TrayTest3 在 Windows 2000 和 Windows XP 中运行的画面:
在 Windows 2000 中运行的气球提示,没有关闭按钮。
图一 在 Windows 2000 运行
在 Windows XP 中运行。
图二 在 Windows XP 中运行的 TrayTest3
气球提示为托盘程序提供了一种非打扰式的方法通知用户发生了某件事情。但是如何让气球提示显示出来呢?所有的托盘图标行为都是通过一个单纯的 API 函数 Shell_NotifyIcon 来操作的。这个函数的一个参数是 NOTIFYICONDATA 结构,你可以利用这个结构来告诉 Windows 你想要做什么。下面是这个结构的定义的最新版本( For IE5.0+ ),其中已经加入了新的成员:
typedef struct _NOTIFYICONDATA {
DWORD cbSize;
HWND hWnd;
UINT uID;
UINT uFlags;
UINT uCallbackMessage;
HICON hIcon;
#if (_WIN32_IE < 0x0500) WCHAR szTip[64];
#else WCHAR szTip[128];
#endif
#if (_WIN32_IE >= 0x0500)
DWORD dwState;
DWORD dwStateMask;
WCHAR szInfo[256];
union {
UINT uTimeout;
UINT uVersion;
} DUMMYUNIONNAME;
WCHAR szInfoTitle[64];
DWORD dwInfoFlags;
#endif
} NOTIFYICONDATA, *PNOTIFYICONDATA;
有关这个结构的详细信息和用法请参考本文前面的两个部分:
系统托盘编程完全指南(一)
系统托盘编程完全指南(二)
在 NOTIFYICONDATA.uFlags 中的标志之一是 NIF_TIP ,用它来设置传统的信息提示,即鼠标要移动到图标上。新的标志 NIF_INFO (由于 _WIN32_IE >= 0x0500 条件定义,因此在编译时,请注意包含最新版本的头文件 shellapi.h ,并保证链接最新版本的库文件 shell32.lib ,分发程序时用最新版本的运行时动态链接库 shell32.dll )便是为显示气球提示所用的。也就是说,要显示气球提示,那么在调用 Shell_NotifyIcon 函数时必须用 NIF_INFO 标志。提示文本填入 szInfo 域,标题文本填入 szInfoTitle 。你甚至可以在 NOTIFYICONDATA.uTimeout 中设置一个超时时间,当经过指定的毫秒数之后,气球提示自动隐藏。
为了示范气球提示的实现原理,我对本文前面两个部分的例子以及 CTrayIcon 类进行了修改。 CTrayIcon 类中添加了一个新的方法 ShowBalloonTip ,这个方法有两个重载函数,既可以用文本串来调用,也可以用资源 ID 来调用。用资源 ID 时,可以有选择地加载文本串,并调用 ShowBalloonTip 的文本串版本,原型如下:
系统托盘编程完全指南(一)
系统托盘编程完全指南(二)
在 NOTIFYICONDATA.uFlags 中的标志之一是 NIF_TIP ,用它来设置传统的信息提示,即鼠标要移动到图标上。新的标志 NIF_INFO (由于 _WIN32_IE >= 0x0500 条件定义,因此在编译时,请注意包含最新版本的头文件 shellapi.h ,并保证链接最新版本的库文件 shell32.lib ,分发程序时用最新版本的运行时动态链接库 shell32.dll )便是为显示气球提示所用的。也就是说,要显示气球提示,那么在调用 Shell_NotifyIcon 函数时必须用 NIF_INFO 标志。提示文本填入 szInfo 域,标题文本填入 szInfoTitle 。你甚至可以在 NOTIFYICONDATA.uTimeout 中设置一个超时时间,当经过指定的毫秒数之后,气球提示自动隐藏。
为了示范气球提示的实现原理,我对本文前面两个部分的例子以及 CTrayIcon 类进行了修改。 CTrayIcon 类中添加了一个新的方法 ShowBalloonTip ,这个方法有两个重载函数,既可以用文本串来调用,也可以用资源 ID 来调用。用资源 ID 时,可以有选择地加载文本串,并调用 ShowBalloonTip 的文本串版本,原型如下:
BOOL CTrayIcon::ShowBalloonTip(LPCTSTR szMsg, LPCTSTR szTitle,
UINT uTimeout, DWORD dwInfoFlags){
m_nid.cbSize=sizeof(NOTIFYICONDATA);
m_nid.uFlags = NIF_INFO;
m_nid.uTimeout = uTimeout;
m_nid.dwInfoFlags = dwInfoFlags;
strcpy(m_nid.szInfo,szMsg ? szMsg : _T(""));
strcpy(m_nid.szInfoTitle,szTitle ? szTitle : _T(""));
return Shell_NotifyIcon(NIM_MODIFY, &m_nid);
}
这个函数很容易理解,同时也够繁琐的,要把文本串载缓冲里拷来拷去。缺省的
dwInfoFlags
设置为
NIIF_INFO
,在文本旁边显示信息图标;其它可能的标志是
NIIF_ERROR??
表示出错,
NIIF_WARNING??
表示警告,
NIIF_NONE??
没有图标。有关修改后的
CTrayIcon
以及
TrayTest3
源代码请下载本文的例子。
只有一种方法可以显示气球提示( Shell_NotifyIcon ),但终止的方法有多种。用户可以在气球上单击鼠标,也可以单击关闭按钮(在 Windows 2000 里没有关闭按钮,如图一),或者 Windows 用超时机制来终止气球提示。那么是如何知道所发生的事件是什么呢?每当创建托盘图标时,你可以提供一个 HWND 和消息 ID 来接收事件发生的通知。如果用户单击气球提示, Windows 发送 NIN_BALLOONUSERCLICK ;如果超时或者单击关闭按钮, Windows 则发送 NIN_BALLOONTIMEOUT 。就我所知,目前还没有办法区分是超时还是单击了关闭按钮。下表中列出的是所有与气球提示相关的通知消息:
只有一种方法可以显示气球提示( Shell_NotifyIcon ),但终止的方法有多种。用户可以在气球上单击鼠标,也可以单击关闭按钮(在 Windows 2000 里没有关闭按钮,如图一),或者 Windows 用超时机制来终止气球提示。那么是如何知道所发生的事件是什么呢?每当创建托盘图标时,你可以提供一个 HWND 和消息 ID 来接收事件发生的通知。如果用户单击气球提示, Windows 发送 NIN_BALLOONUSERCLICK ;如果超时或者单击关闭按钮, Windows 则发送 NIN_BALLOONTIMEOUT 。就我所知,目前还没有办法区分是超时还是单击了关闭按钮。下表中列出的是所有与气球提示相关的通知消息:
通知消息
|
描述
|
NIN_BALLOONSHOW
|
显示气球提示时发送
|
NIN_BALLOONHIDE
|
气球提示消失时发送;例如,当图标被删除,如果因为超时或是用户单击鼠标气球消失,此消息不会被发送
|
NIN_BALLOONTIMEOUT
|
当由于超时或者用户单击气球上的关闭按钮
(X)
,使气球消失时发送此消息
|
NIN_BALLOONUSERCLICK
|
当用户在气球提示上或托盘图标上单击鼠标(此时气球处于显示状态)时发送此消息
|
在测试过程中,我发现一个奇特的现象:在 Windows XP 中,只要你的托盘程序拥有焦点,气球提示便不会超时。显然,你只有转到其它应用程序,才能启动计时器。在 Windows 2000 里好像没有这个问题。
你可以用 TrayTest3 来看通知消息。实际上, TrayTest3 的功能就是查看通知消息:当托盘通知消息到达时显示它们。查看的方法是先运行 TrayTest3 ,单击初始对话框的 " 确认 " 按钮,然后双击托盘图标调出图一所示的窗口。用 " 查看 | 显示气球提示 " 菜单来让 TrayTest3 显示它的提示,然后当你关闭提示或等待超时的时候,你查看在主窗口中显示的什么通知消息。
最后时一点忠告:请不要滥用气球提示和托盘图标!很多程序员为了好玩和张扬个性而滥用托盘图标。请在只有真正需要它时才去做。不要让用户觉得你的程序很讨厌,这样他们会毫不犹豫地卸载这些屏幕垃圾。如果你真的必需实现托盘图标,至少要给用户一个选项来关掉它。(完)