现在比较流行的软件都会在系统托盘上显示一个图标,左键点击该图标时可以显示/隐藏软件主界面,节省任务栏的空间;右键点击该图标会出现一个菜单,显示一些常用的功能。那么这是怎么实现的呢?其实很简单,只需要一个API函数就搞定了,跟我一起看吧~
    首先看一个结构体:
    typedef struct _NOTIFYICONDATA {
        DWORD cbSize;
        HWND hWnd;
        UINT uID;
        UINT uFlags;
        UINT uCallbackMessage;
        HICON hIcon;
        TCHAR szTip[64];
        DWORD dwState; //Version 5.0
        DWORD dwStateMask; //Version 5.0
        TCHAR szInfo[256]; //Version 5.0
        union {
            UINT  uTimeout; //Version 5.0
            UINT  uVersion; //Version 5.0
        } DUMMYUNIONNAME;
        TCHAR szInfoTitle[64]; //Version 5.0
        DWORD dwInfoFlags; //Version 5.0
    } NOTIFYICONDATA, *PNOTIFYICONDATA;

    使用该结构指定托盘图标的相关信息,其中Version 5.0的部分表示的是Shell DLL的版本号,可以用DllGetVersion获得当前系统的DLL版本。出于兼容的考虑,该部分很少使用。
    cbSize: NOTIFYICONDATA结构的大小,sizeof(NOTIFYICONDATA)
    hWnd:接收托盘通知的窗口句柄
    uID:托盘图标的ID号,该ID与hWnd一起唯一标识了一个托盘图标的身份,因此该ID号不能重复,一般使用与该图标相关联的菜单的ID
    uFlags:为下列值的组合
    NIF_ICON:表示结构中的hIcon有效 
    NIF_MESSAGE:表示结构中的uCallbackMessage有效
    NIF_TIP:表示结构中的szTip有效
    NIF_STATE:表示结构中的dwState和dwStateMask有效
    NIF_INFO:使用气球型的托盘提示代替传统的方框型托盘提示.结构中的szInfo, uTimeout, szInfoTitle,和dwInfoFlags有效
    uCallbackMessage:托盘消息,当托盘区域有鼠标事件(如鼠标移动,单击等)产生时,会向接收窗口发送该消息,进行相应的处理
    hIcon:托盘图标句柄
    szTip:托盘提示字符


    好了,就介绍这么多,下面接收这个API函数Shell_NotifyIcon:
    BOOL Shell_NotifyIcon(
        DWORD dwMessage,
        PNOTIFYICONDATA pnid
    );

    dwMessage:为下列值之一
        NIM_ADD:添加托盘图标
        NIM_DELETE:删除图盘图标
        NIM_MODIFY:修改托盘图标
        NIM_SETFOCUS:Version 5.0
        NIM_SETVERSION:Version 5.0
        pnid:NOTIFYICONDATA结构的指针

    好了,下面言归正传,开始着手编程了~
    我们分为以下几步逐步分析:
        初始化
        添加/修改/移除图标
        添加托盘消息响应函数
        添加菜单消息处理函数
    (1)初始化
    首先要声明一个NOTIFYICONDATA成员变量:
    NOTIFYICONDATA m_nid;
    然后在资源面板中添加一个菜单资源IDR_TRAYMENU和一个图标资源IDI_RED
由于我们要处理鼠标消息,因此先定义一个用户自定义消息:
    #define UM_ TRAYNOTIFICATION (WM_USER+100)
    然后对该结构的成员进行初始化:
    void InitTray()
    {
          //初始化m_nid
          m_nid.cbSize = sizeof(NOTIFYICONDATA);
          m_nid.hWnd = this->m_hWnd;
          m_nid.uID = IDR_TRAYMENU;
          m_nid.uFlags = NIF_ICON|NIF_TIP|NIF_MESSAGE;
          m_nid.hIcon = AfxGetApp()->LoadIcon(IDI_RED);
          strcpy (m_nid.szTip, "我的托盘听我的");
          m_nid.uCallbackMessage = UM_TRAYNOTIFICATION;
    }

    上面的程序我就不多说了,相信大家都能看懂。
    (2)添加/修改/移除图标
    这个更简单,直接调用Shell_NotifyIcon函数就可以了:
    //向托盘添加图标
    void AddTray()
    {
          Shell_NotifyIcon(NIM_ADD, &m_nid);
    }

    //移除托盘图标,在程序退出时一定要记得调用,否则图标会残留在托盘上
    void RemoveTray()
    {
          Shell_NotifyIcon(NIM_DELETE, &m_nid);
    }

    //修改图标,QQ登陆时托盘上的动态效果不用我教了吧
    void ModifyTray(UINT uId)//参数为要显示的Icon的ID号
    {
          m_nid.hIcon = AfxGetApp()->LoadIcon(uId);
          Shell_NotifyIcon(NIM_MODIFY, &m_nid);
    }

    (3)添加托盘消息响应函数
    这个其实就是响应自定义消息,忘了?好,那我们一起复习一下吧~
    首先定义消息标识符(第1步已经定义过了):
    #define UM_ TRAYNOTIFICATION (WM_USER+100)
    然后在头文件的DECLARE_MESSAGE_MAP()之前定义消息响应函数:
    afx_msg LRESULT OnTrayNotification(WPARAM wId, LPARAM lEvent);
    其中两个参数分别为图标ID号和鼠标事件
    最后在cpp文件里实现该函数:
    LRESULT CXXXDlg::OnTrayNotification(WPARAM wId, LPARAM lEvent)
    {
          if(wId!=m_nid.uID
           || (lEvent!=WM_LBUTTONUP && lEvent!=WM_RBUTTONUP))
           return 0;

          //加载菜单
          CMenu menu;
          if(!menu.LoadMenu(wId))
               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; 
    }
    下面逐句解释上面这段代码:
    首先判断发送该消息的图标的ID是否等于m_nid中的ID号,再判断接收到的鼠标消息是否为左键单击消息WM_LBUTTONUP或右键单击消息WM_RBUTTONUP,如果不是则不对该消息进行响应。
    然后加载菜单,右键单击时显示该菜单。先用LoadMenu()加载顶层菜单,再用GetSubMenu()获得一级子菜单。
当鼠标事件为右键单击时,显示菜单,否则(即左键单击时)用::SendMessage()向接收窗口发送命令消息,进行相应的处理。::SetMenuDefaultItem()用于设置菜单的默认选择项,以粗体显示。最后用::TrackPopupMenu()函数显示右键菜单。该菜单可能不会像通常那样马上消失,这是因为从弹出菜单接收消息的窗口必须是前景窗口。调用::SetForegroundWindow()函数就可以纠正该错误。
    (4)添加菜单消息处理函数
    在右键菜单中选择一项后,就会发送命令消息WM_COMMAND,在ClassWizard里添加WM_COMMAND的消息处理函数:
    BOOL CZYJReportDlg::OnCommand(WPARAM wParam, LPARAM lParam)
    {
          // TODO: Add your specialized code here and/or call the base class
          switch(LOWORD(wParam))
          {
          case ID_SHOWHIDE:
                ShowWindow(m_bShow?SW_HIDE:SW_SHOW);
                m_bShow = !m_bShow;
                break;
          case ID_EXIT:
                RemoveTray();
                PostQuitMessage(0);
                break;
          case ID_ABOUT:
                CAboutDlg about;
                about.DoModal();
                break;
          }

          return CDialog::OnCommand(wParam, lParam);
    }
    其中第一个参数wParam的低字节LOWORD(wParam)标识了所选子菜单的ID,根据所选的菜单项执行相应的程序。
     OK,以上介绍了写托盘程序的完整过程,是不是很简单呢?当然托盘编程的内容还远不止这些,比如Version 5.0的部分,还可以添加很多漂亮的效果,欢迎大家跟我一起探讨!