系统托盘编程完全指南(二)

2016-07-03

在本文的第一部分,我们讨论并示范了如何在自己的程序中应用系统托盘图标。通过使用自己创建的一个可重用的 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并象下面这样安装它即可:

// 在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消息,而另一个则完全不会。(待续)