分类: C/C++
2008-04-23 21:39:25
系统托盘编程完全指南(一)
编译/
自从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。
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 0 int 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执行第一个菜单命令。只有两件事情需要进一步交待:
// 让第一个菜单项为默认(黑体): ::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.....”真是气刹人也。
// (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实现的。