Chinaunix首页 | 论坛 | 博客
  • 博客访问: 536795
  • 博文数量: 576
  • 博客积分: 40000
  • 博客等级: 大将
  • 技术积分: 5020
  • 用 户 组: 普通用户
  • 注册时间: 2008-10-13 14:47
文章分类

全部博文(576)

文章存档

2011年(1)

2008年(575)

我的朋友

分类:

2008-10-14 14:57:08

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


编译/



    自从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实例的。
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执行第一个菜单命令。只有两件事情需要进一步交待:
    第一件事情是:在显示菜单之前,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相同的菜单。就这么简单。
// (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 托盘图标菜单

    最后,我想说明一个很让人担心的问题,每个人在看到这个小图标后都想尽快的在自己的程序中加入托盘图标。作为程序员,这完全是可以理解的。当自己的程序中成功添加了托盘图标,在朋友们中间炫耀一番,那种感觉确实很好。但是要记住:并不是所有的应用都需要用托盘图标,如果不是必须就不要画蛇添足,否则托盘图标太多必然造成屏幕垃圾,看看下面图五吧:


图五 托盘图标程序“噩梦版”

看到这么多的托盘图标对于用户来说简直就是噩梦。(待续)
--------------------next---------------------

我按照上面介绍的步骤去作,为什么在我点击界面右上角的关闭按钮,桌面右下角的图标也一并消失了,整个就是完全退出.但是例子中的不是会仍有图标显示在桌面右下角吗?请指点^_^ ( dahui11 发表于 2004-7-2 9:52:00)
 
可是就算是注销掉也编译ok啊。

但是启动界面和程序界面同时出现,这个问题怎样解决? ( 艾葭 发表于 2003-8-11 9:08:00)
 
把GetSystemWindowsDirectory改成GetSystemDirectory ( liron 发表于 2002-12-25 8:21:00)
 
调试的时候会出错,提示
D:...\StatLink.cpp(179) : error C2065: 'GetSystemWindowsDirectory' : undeclared identifier  Error executing cl.exe.

把GetSystemWindowsDirectory改成GetWindowsDirectory
就ok了。 ( seaboyf 发表于 2002-12-24 16:51:00)
 
.......................................................

--------------------next---------------------

阅读(259) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~