原文:http://www.ccw.com.cn/htm/app/aprog/01_6_25_4.asp
相当不错的一篇文章,写得很详细。
信息产业部电子第二十二研究所青岛分所 郎锐
--------------------------------------------------------------------------------
一、 引言
Windows 95/98/NT/2000中,任务栏的右边(托盘)常驻有几个图标,如输入法切换图标、音量控制图标等系统图标,而一些在后台执行实时监控或其他任务的软件如金山词霸、瑞星杀毒软件等也只是在托盘上放一个小小的图标,几乎不占用本来就拥挤不堪的桌面,而且必要时我们还可以通过用鼠标点击图标对其进行菜单操作或激活其主窗口。如果在我们编制的一些类似的在后台工作的实时监控软件里加入上述功能,无疑会使程序显得很有专业水准。虽然有不少介绍系统托盘方面编程的文章,但大多是讲述如何在SDK下用Windows API编写的,这样做显然是非常烦琐的,本文以一个SDI(单文档界面)程序为例,讲述了在MFC框架程序中此类程序的实现过程。
二、 程序的设计思路
在编制此类程序时,为了不干扰前台程序的运行界面和不显示不必要的窗口,应使其运行时的主窗口不可见。同时,又要让用户知道该程序正在运行,并且能达到与用户进行交互的目的。将一个图标显示在任务栏右端系统托盘区中并响应用户的鼠标动作是当前非常流行的方法。要使程序的主窗口不可见,并且不在任务栏上出现任务按钮,需要分别设置主边框窗口的风格和扩展风格;另外,利用系统函数Shell_NotifyIcon可以将一个图标显示在任务栏的通告区中。剩下的任务就是如何通过这个图标来响应用户的操作,可以象其他软件如WinAmp 一样通过弹出式菜单来完成同用户的交互。
三、 程序的具体实现
(一) 主界面的隐藏
前面已经提过,要求程序运行开始时主窗口不可见,同时也不出现在任务栏中,在SDK下是通过修改入口函数WinMain中的API函数CreatWindow的参数来实现的,在MFC下已经对其进行了封装,使我们无法直接对WinMain函数内部进行访问与修改,不过可以通过MFC提供的另外一个接口--在主框架类的预创建窗口函数中设定主框架窗口的风格和扩展风格来实现之:
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
if( !CFrameWnd::PreCreateWindow(cs) )
return FALSE;
cs.style=WS_POPUP;
cs.dwExStyle |=WS_EX_TOOLWINDOW;
return TRUE;
}
其中,CREATESTRUCT结构中的style成员变量设定了主框架窗口的风格,在此设定为WS_POPUP;而dwExStyle成员变量则对主框架窗口的扩展风格做了描述,只需在原有基础上为其再增添一种WS_EX_TOOLWINDOW扩展属性即可使其不会出现在任务条上。
(二) 将图标放置到托盘区
将图标放到托盘之前必须先将图标装载到内存中,可用API函数LoadIcon实现。之后就可以用系统函数Shell_NotifyIcon将一个图标显示到任务栏的托盘区中了。该函数的原型为:WINSHELLAPI BOOL WINAPI Shell_NotifyIcon( DWORD dwMessage, PNOTIFYICONDATA pnid)其第一个参数用以指定动作,如NIM_ADD将图标装载到托盘中,NIM_DELETE可将装载到托盘中的图标删除掉;其第二个参数是一个具有 NOTIFYICONDATA类型的结构指针。该结构的原型为:
typedef struct _NOTIFYICONDATA { // nid
DWORD cbSize;
HWND hWnd;
UINT uID;
UINT uFlags;
UINT uCallbackMessage;
HICON hIcon;
charszTip[64]; }NOTIFYICONDATA, *PNOTIFYICONDATA;
在该结构的成员中,hWnd为接受该图标所发出的消息的窗口的句柄,可以将主框架类的m_hWnd变量传给它;uID 为被显示图标的ID;uFlags指明其余的几个成员(hIcon、uCallBackMessage和szTip)的值是否有效;uCallbackMessage为一个自定义的消息,当用户在该图标上作用一些鼠标动作时,将向hWnd 成员中指定的窗口发出该消息,可以定义该消息WM_TRAY为WM_USER+100;hIcon为被显示图标的句柄即前面的LoadIcon的返回值;szTip为一字符数组,当鼠标停留在该图标上时,将其内容显示在浮动的提示信息框中。显然,上述工作必须在程序一开始执行时就完成,所以在主框架类的OnCreate函数中修改如下:
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
……
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_wndToolBar);
……
NOTIFYICONDATA tnd;
tnd.cbSize=sizeof(NOTIFYICONDATA);
tnd.hWnd=this->m_hWnd;
tnd.uID=IDR_MAINFRAME;
tnd.uFlags=NIF_MESSAGE|NIF_ICON|NIF_TIP;
tnd.uCallbackMessage=WM_TRAY;
tnd.hIcon=LoadIcon(AfxGetInstanceHandle(),MAKEINTRESOURCE(IDR_MAINFRAME));
strcpy(tnd.szTip,"托盘演示软件");
Shell_NotifyIcon(NIM_ADD,&tnd);
return 0;
}
(三) 弹出式菜单的添加及激活
显然,在添加到托盘中的图标上使用弹出式菜单是用户同程序交互的再合适不过的手段,许多此类知名软件也多使用这种方法同用户相交互。弹出式菜单不能象通常菜单那样在ResourceView中对其进行编辑,而要通过编辑软件对*.rc的资源文件进行手工编辑,如果没有接触过SDK 编程,那么此步会有些困难。首先找到原有菜单IDR_MAINFRAME MENU 的定义区,添加如下对菜单的描述:
IDR_BEGINMENU MENU DISCARDABLE
BEGIN
POPUP ""
BEGIN
MENUITEM "技术支持", ID_SUPPORT
MENUITEM SEPARATOR
MENUITEM "主界面", ID_MAIN
MENUITEM SEPARATOR
MENUITEM "退出程序", ID_EXIT
END
END
其中SEPARATOR为菜单中的分割符。之后还应当在Resource.h中添加ID_SUPPORT、ID_MAIN和ID_EXIT的定义。该菜单的激活只有通过自定义消息WM_TRAY的响应函数这一条途径来实现,因为当用户在该图标上作用一些鼠标动作时,只会发出这个消息,可以通过判断消息的低位参数lParma来决定是鼠标的何种动作,下面代码使当鼠标右键按下时在鼠标当前位置弹出上述菜单:
……
uID=(UINT) wParam;
uMouseMsg=(UINT) lParam;
CMenu menu;
if(uMouseMsg==WM_RBUTTONDOWN)//如果是单击右键
{
switch(uID)
{
case IDR_MAINFRAME://如果是我们的图标
GetCursorPos(&pt);//取得鼠标位置
//执行相应操作
menu.LoadMenu(IDR_BEGINMENU);
pMenu=menu.GetSubMenu(0);
ASSERT(pMenu!=0);
pMenu->TrackPopupMenu (0,pt.x,pt.y,this);
break;
default:
break;
}
}
……
(四) 菜单项及图标对外界激励的响应
为使人机界面更友好,与用户更好的进行交互,使用户在该图标上双击鼠标左键时能弹出程序主界面,再次双击则隐藏之。可以继续在自定义消息WM_TRAY 的响应函数里加入对左键的处理过程:
……
if(uMouseMsg==WM_LBUTTONDBLCLK)//如果是左键双击
{
static bool isMax=false;
switch(uID)
{
case IDR_MAINFRAME:
if(isMax==false)
{
//显示主窗口
ModifyStyle(WS_POPUP,WS_OVERLAPPEDWINDOW);
ModifyStyleEx(WS_EX_TOOLWINDOW,WS_EX_TOPMOST);
ShowWindow(SW_SHOWMAXIMIZED);
isMax=true;
}
else
{
ShowWindow(SW_HIDE);//隐藏主窗口
ModifyStyle(WS_CAPTION|FWS_PREFIXTITLE|WS_SYSMENU| WS_MINIMIZEBOX|WS_MAXIMIZEBOX,WS_POPUP);
ModifyStyleEx(WS_EX_TOPMOST,WS_EX_TOOLWINDOW);
isMax=false;
}
break;
default:
break;
}
}
……
另外,当用右键弹出菜单时,对其菜单项的响应需要用到自定义消息的响应,这种自定义消息的响应过程不能象其他的系统消息一样通过ClassWizard来添加,仍需要一步步地手工添加,首先在头文件里添加响应函数的声明:
……
//{{AFX_MSG(CMainFrame)
……
afx_msg void OnMainWindow();
afx_msg void OnExit();
afx_msg void OnSupport();
……
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
……
然后,在源文件里添加消息映射:
……
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
//{{AFX_MSG_MAP(CMainFrame)
……
ON_COMMAND(ID_MAIN,OnMainWindow)
ON_COMMAND(ID_EXIT, OnExit)
ON_COMMAND(ID_SUPPORT, OnSupport)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
……
最后在其各自的实现函数中加入与其菜单项相对应的功能即可:
……
void CMainFrame::OnMainWindow()
{
//显示程序主框架
ModifyStyle(WS_POPUP,WS_OVERLAPPEDWINDOW);
ModifyStyleEx(WS_EX_TOOLWINDOW,WS_EX_TOPMOST);
ShowWindow(SW_SHOWMAXIMIZED);
}
void CMainFrame::OnSupport()
{
……
}
void CMainFrame::OnExit()
{
//退出程序之前用NIM_DELETE属性指定删除掉加入到托盘的图标
NOTIFYICONDATA tnid;
tnid.cbSize=sizeof(NOTIFYICONDATA);
tnid.hWnd=this->m_hWnd;
tnid.uID=IDR_MAINFRAME;//保证删除的是我们的图标
Shell_NotifyIcon(NIM_DELETE,&tnid);
//退出程序
AfxPostQuitMessage(0);
}
小结:
本程序对系统托盘的MFC编程做了较详细的介绍,同时也对弹出菜单及自定义消息等技术也做了适当的说明,在理解了本程序的编程思路和实现方法的前提下,可以修改、添加部分代码,进一步完善程序,使之跟自己的程序更好的融合在一起。本程序在Windows 98下,由Microsoft Visual C++6.0编译通过。