VC编程制作系统托盘程序
Windows操作系统中的某些程序运行时不显示运行窗口,只在任务栏上显示一个图标,表示程序正在运行,用户可以通过鼠标与应用程序交互,比如金山毒霸等应用程序,我们有时也需要编制一些仅在后台运行的类似程序,为了不干扰前台程序的运行界面和不显示不必要的窗口,应使程序运行时的主窗口不可见。同时将一个图标显示在任务栏右端静态通告区中并响应用户的鼠标动作。下面介绍Visual C++开发这类程序的设计方法。
一、隐藏程序的主窗口
首先,要使程序的主窗口不可见,并且不在任务栏上出现任务按钮,要做到这两点,需分别设置主边框窗口的风格和扩展风格:
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
cs.style =WS_POPUP;//使主窗口不可见
cs.dwExStyle |=WS_EX_TOOLWINDOW;//不显示任务按钮
return CFrameWnd::PreCreateWindow(cs);
}
二、将表示程序运行的图标加入任务栏
在主框架窗口的CMainFrame::OnCreate()函数中调用上述函数,就可以在任务条上显示图标这一步是利用系统API函数Shell_NotifyIcon()将一个图标显示在任务栏的通告区中。该函数的原型为:在调用该函数之前,需要确定其参数的取值。其中Shell_NotifyIcon()函数的第一个参数是一个预定义的消息,可以取如下值之一:NIM_ADD、NIM_DELETE或NIM_MODIFY,分别表示添加图标、删除图标或修改图标。另一个参数为指向NOTIFYICONDATA类型的指针。其原型为:
typedef struct _NOTIFYICONDATA {
DWORD cbSize;
HWND hWnd;
UINT uID;
UINT uFlags;
UINT uCallbackMessage;
HICON hIcon;
charszTip[64]; }
NOTIFYICONDATA
在该结构的成员中,cbSize为该结构所占的字节数,hWnd为接受该图标所发出的消息的窗口的句柄(鼠标在任务栏上程序图标上动作时图标将发出消息,这个消息用户要自己定义),uID为被显示图标的ID,uFlags指明其余的几个成员(hIcon、uCallBackMessage和szTip)的值是否有效,uCallbackMessage为一个用户自定义的消息,当用户在该图标上作用一些鼠标动作时,图标将向应用程序的主框架窗口(hWnd成员中指定的窗口)发出该消息,。hIcon为将在任务栏上被显示图标的句柄,szTip鼠标停留在该图标上时显示的字符串。
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
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_MYMESSAGE;
file://用户自定义的消息,即鼠标在任务栏上程序图标上动作时图标发送的消息
tnd.hIcon=LoadIcon(AfxGetInstanceHandle(),MAKEINTRESOURCE(IDR_MAINFRAME));
strcpy(tnd.szTip,"测试程序");//图标提示为"测试程序"
Shell_NotifyIcon(NIM_ADD,&tnd);//向任务栏添加图标
}
三、用户与程序交互的实现
用户进行交互,也就是当用户在该图标上单击或双击鼠标左键或右键时要执行相应的操作,至少也要响应用户终止该程序的意愿。上面已经提到,当用户在图标上进行鼠标动作时,将向hWnd成员中指定的窗口发出自定义的消息,该消息为uCallbackMessage成员所指定的WM_MYESSAGE,取值为WM_USER+101(如何自定义消息,我就不多说了)。我们要实现任务就是在hWnd窗口中响应该自定义消息:
void CMainFrame::OnMYMESSAGE(WPARAM wParam,LPARAM lParam)
{
UINT uID;//发出该消息的图标的ID
UINT uMouseMsg;//鼠标动作
POINT pt;
uID=(UINT) wParam;
uMouseMsg=(UINT) lParam;
if(uMouseMsg==WM_RBUTTONDOWN)//如果是单击右键
{
switch(uID)
{
case IDR_MAINFRAME://如果是我们的图标
GetCursorPos(&pt);//取得鼠标位置
AfxGetApp( )-> m_pMainWnd->ShowWindow(SW_SHOWNORMAL);//显示程序窗口
break;
default:
}
}
return;
}
四、程序结束时删除程序图标
当程序结束时,需要删去通告区中的图标,这时还应该调用Shell_NotifyIcon函数,只不过第一个参数是表示删除图标的NIM_DELETE了:
void CMainFrame::~CmainFrame()
{
NOTIFYICONDATA tnid;
tnid.cbSize=sizeof(NOTIFYICONDATA);
tnid.hWnd=this->m_hWnd;
tnid.uID=IDR_MAINFRAME;//保证删除的是我们的图标
Shell_NotifyIcon(NIM_DELETE,&tnid);
}
Windows 95以及后来的Windows版本允许你将程序图标放入系统托盘。所谓系统托盘,通常指的是屏幕右下方显示时间,音量等图标的那个区域。这个区域主要用于显示状态信息或者当你运行的程序不可见时,允许你方便地访问程序的主要特性。这个区域还可以用于显示小程序的图标,以便用户容易访问主程序,或者在预定的时间加载主程序。有些系统托盘图标可以变化用以指示程序状态,例如,浏览器的系统托盘图标当modem接收和发送数据时显示的是不同的图标。把鼠标移到托盘图标上停留一下常常会显示一个提示,根据程序的状态,它可能也会变化。在托盘图标上单击鼠标右键常常显示一个程序菜单,而双击鼠标左键常常可以启动主窗口或应用程序。访问系统托盘的方法是通过Shell_NotifyIcon函数和NOTIFYICONDATA结构实现的。
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
UINT uTimeout; //Version 5.0
TCHAR szInfoTitle[64]; //Version 5.0
DWORD dwInfoFlags; //Version 5.0
} NOTIFYICONDATA, *PNOTIFYICONDATA;
为了要在系统托盘中显示图标,用NIM_ADD标志调用Shell_NotifyIcon函数。
#define ID_TASKBARICON 100
#define WM_ICONNOTIFY (WM_USER + 101) //自定义消息NOTIFYICONDATA nid;
// 初始化系统托盘图标
nid.cbSize = sizeof(NOTIFYICONDATA);
nid.hWnd = hWnd;
nid.uID = ID_TASKBARICON;
nid.uFlags = NIF_ICON|NIF_MESSAGE|NIF_TIP;
nid.uCallbackMessage = WM_ICONNOTIFY;
nid.hIcon = LoadImage(hInst, MAKEINTRESOURCE(IDI_TRAY1), IMAGE_ICON, 16, 16, 0);
strcpy(nid.szTip, "My Tooltip Text");Shell_NotifyIcon(NIM_ADD, &nid);
cbSize成员是结构的大小(使用它主要是为了支持将来这个结构大小增加)。
hWnd是窗口句柄。当图标发生某事件时(如单击、双击等),Windows将向窗口发送uCallbackMessage成员指定的消息。
uID成员指定与图标关联的ID。它不是很重要,除非你需要显示并跟踪几个图标。
uFlag成员告诉Windows应该读取哪个成员。当添加一个图标时,应该包含这个结构的大多数成员。当更新图标时,如只是需要改变图标时,你只要设置相应的标志就可以了。
hIcon成员是你想显示的图标。
最后,szTip成员是提示文本。设置好这些结构成员后,调用Shell_NotifyIcon函数。
当与图标关联的事件发生时,Windows将发送uCallbackMessage成员指定的消息。IParam包含发送的消息。当获得WM_LBUTTONDBLCLK消息时显示主窗口或者启动主程序。当获得WM_RBUTTONUP消息时显示菜单。
注意:如果在系统托盘中单击鼠标右键,有时会有一个弹出式(上下文菜单)菜单显示/消失的怪现象,详细信息击解决办法请参阅微软知识库文章Q135788,也可以参考下列代码加以解决。
switch(nMsg) {
case WM_ICONNOTIFY:
switch(lParam) {
case WM_LBUTTONDBLCLK:
// Load main window here
break;
case WM_RBUTTONUP:
{
POINT point;
HMENU hMenu, hSubMenu;
// Get mouse position
GetCursorPos(&point);
// Popup context menu
hMenu = LoadMenu(hInst, MAKEINTRESOURCE(IDR_MYMENU));
hSubMenu = GetSubMenu(hMenu, 0);
SetMenuDefaultItem(hSubMenu, IDM_DEFAULTCMD, FALSE);
SetForegroundWindow(hMainDlg); // Per KB Article Q135788
TrackPopupMenu(hSubMenu,
TPM_LEFTBUTTON|TPM_RIGHTBUTTON|TPM_LEFTALIGN,
point.x, point.y, 0, hWnd, NULL);
PostMessage(hMainDlg, WM_NULL, 0, 0); // Per KB Article Q135788
DestroyMenu(hMenu);
}
break;
default:
return FALSE;
}
}
不论什么时候,你都可以用 NIM_MODIFY 调用Shell_NotifyIcon。
程序终止之前,用 NIM_DELETE 调用 Shell_NotifyIcon从托盘中清除图标。
Shell_NotifyIcon(NIM_DELETE, &nid);
很多软件例如:KV3000等在关闭之后会在托盘区有一个小的托盘图标,这表示程序并没有真正停止而在后台运行。当我们单击鼠标时可以使它在桌面上出现一个最大化窗口;而如果我们单击程序右上角х,或者单击文件菜单中的退出项时程序仍然在托盘上运行;只有我们右键单击托盘图标在出现的菜单中选择退出才能够真正退出程序等。
现在我们通过一个类CSystemTray和修改系统菜单来实现上述功能。
我们先来改变系统菜单。
1、当我们在程序的标题栏上单击鼠标右键时,只出现如图的几项。
而通过MFC生成的还会出现About一项,我们已经把它给去掉了。
我们调用函数GetSystemMenu()来取的系统菜单,然后调用DeleteMenu()来去掉不要的系统菜单项,在这里我们去掉了“关闭”下面的分隔线和“about”项代码如下:
CMenu *pSystemMenu=GetSystemMenu(FALSE);
pSystemMenu->DeleteMenu(8,MF_BYPOSITION);
pSystemMenu->DeleteMenu(7,MF_BYPOSITION);
其中的数字7和8表示系统菜单的第7项和第8项,注意分隔线在VC中也算是一项。(这里和前面所讲的修改系统菜单的方法是一样的)。 当我们去掉“about”这项后就应该在系统命令中也去掉相应的相应代码。我们找到函数OnSysCommand(UINT nID, LPARAM lParam)去掉其中截获about对话框的代码: if ((nID & 0xFFF0) == IDM_ABOUTBOX)
{
CAboutDlg dlgAbout;
dlgAbout.DoModal();
}
它对我们没有任何用处,放在这里只能给我们调试带来麻烦。当然如果你不去掉它程序完全没有一点错误。 那么,为什么我们要去掉这两项呢,因为我们准备在托盘中实现相同的功能。 1, 我们再改变程序的关闭标志“X”的功能,把它改为隐藏而不是完全关闭。 我们来截获关闭命令,把它改为隐藏。if ((nID & 0xFFF0)==SC_CLOSE){
//OnClose();本来这个是关闭的这里也改为隐藏。
AnimateWindow(GetSafeHwnd(),1000,AW_HIDE|AW_BLEND);
KillTimer(0);
ShowWindow(SW_HIDE);//系统菜单的关闭也改为隐藏。
}
在ShowWindow(SW_HIDE)前面的两句是为了实现一种特效,即是慢慢很温和的隐藏,不过只有这两句还是不行的还应该在stdafx.h的前面加上如下两行代码: #undef WINVER
#define WINVER 0X500
2、我们来建立一个托盘菜单,ID号为IDR_TUOPAN当我们在托盘上点击右键时,会出现一个菜单,在菜单里我们有:关于、显示(隐藏)、换歌、退出四项。 我们先在头文件VioletPlayDlg.h中自定义一个消息:
#define WM_USER_TRAY_NOTIFICATION (WM_USER+0x101)
注意它的格式。在文件中定义一个托盘变量CSystemTray m_trayIcon,有了这些后我们就可以先做一个托盘雏形了。在主文件VioletPlayDlg.cpp中来产生一个托盘了。我们用Creat()函数定义如下: m_trayIcon.Create(this, WM_USER_TRAY_NOTIFICATION, "VioletPlay", m_hIcon, IDR_TUOPAN);
其中第二个参数为我们自定义的功能,即出现托盘图标,第三个参数为当我们把鼠标放在图标上时出现的说明文字,第五个参数为当我们单击右键时出现的菜单,由于我们通过这个函数并不能响应右键,所以在这里并不能出现这个菜单,(下文会讲) 当我们的鼠标离开图标的时候出现的说明文字VioletPlay会自动消失,我们通过一个条件语句来实现。首先我们定义一个BOOL变量BOOL start_minimized我们来判断它的值: if (start_minimized)
PostMessage(WM_CLOSE);
当我们鼠标移走的时候就发送一个CLOSE命令,关闭窗口。我们在文件VioletPlay.cpp中加上一句代码时程序运行的时候即可以在托盘中显示又可以在桌面上显示: dlg.start_minimized=GetProfileInt(_T(""),_T("StartMinimized"),FALSE);
我们在这里在显示主程序的对话框之前加上这句代码,起到一个“拦截”作用。 这样我们就实现了最初的功能,即在托盘中出现了一个图标,但是这还是远远不够的,我们在来一步步实现其他的功能。
3、虽然我们有了一个托盘菜单,但是我们到现在还不能通过单击鼠标右键使它出现,因为我们的程序还不能响应这个消息,下面我们通过重载函数:afx_msg LONG OnTrayNotification( WPARAM wparam, LPARAM lparam )并在里面加上响应代码来实现这个功能。
我们用switch…case…结构来响应鼠标右键这一消息,当然如果你愿意也是可以用if语句来实现同样的功能的。
代码如下:
LONG CVioletPlayDlg::OnTrayNotification(WPARAM wparam, LPARAM lparam)
{
switch ( lparam )
{
case WM_RBUTTONDOWN:
{// 用户在托盘图标上单击鼠标右键,弹出上下文菜单隐藏/显示对话框。
CMenu oMenu;
if (oMenu.LoadMenu(IDR_TUOPAN))
{
CMenu* pPopup = oMenu.GetSubMenu(0);
ASSERT(pPopup != NULL);
CPoint oPoint;
if (IsWindowVisible())// 根据对话框窗口的显示/隐藏状态修改菜单名称
oMenu.ModifyMenu(IDC_SHOW,MF_STRING,IDC_SHOW,"隐藏(&H)");
else
oMenu.ModifyMenu(IDC_SHOW,MF_STRING,IDC_SHOW,"显示(&S)");
// 确定鼠标位置以便在该位置附近显示菜单
GetCursorPos( &oPoint );
SetForegroundWindow();
pPopup->TrackPopupMenu(
TPM_LEFTALIGN | TPM_RIGHTBUTTON,
oPoint.x, oPoint.y, this);
}
}
break;
// 单击/双击鼠标左键均显示出对话框
case WM_LBUTTONDBLCLK:
case WM_LBUTTONDOWN:
OnShow();
break;
}
return 0;
}
在上述代码中我们同时实现了动态改变菜单“显示/隐藏”的功能,当程序主界面出现时,菜单中变为“隐藏”表示如果我们再单击它时程序主界面就隐藏起来了。如果为隐藏状态时菜单中变为“显示”表示当我们单击它时可以显示程序主界面。 另外,在上面代码的最后三句表示不管我们是单击还是双击鼠标左键它发送的命令都是一样的即出现主程序界面,即OnShow()。
我们在对托盘菜单中的命令一个一个的分析。 这里的“退出”菜单我们使程序真正的退出,因此它接受的命令为 ::PostQuitMessage(0),使程序真正退出,注意前面我们也说了,其它的两个“退出”皆为隐藏。因为程序的使用者也许并不知道这一点,所以我们有必要在这里设计一个对话框进行提醒,如果要求这一点我们就把点击“退出”菜单所发送的命令改为调用一个AfxMessageBox,当是使用者点击“是”才实现真正的退出。点击“否”而不退出。
代码如下: void CVioletPlayDlg::OnClose()
{
if(AfxMessageBox("真的要退出吗",MB_YESNO)==IDYES)
{
AnimateWindow(GetSafeHwnd(),1000,AW_HIDE|AW_BLEND);
KillTimer(0);
::PostQuitMessage(0);
}
}
当你点击了文件菜单中的退出时也是同样的道理只是当你点击“是”时只能隐藏主界面,而当你点击“否”时,不能隐藏主界面。我们只需要把上面代码中的退出命令:::PostQuitMessage(0)改为隐藏命令ShowWindow(SW_HIDE)就可以了。
到现在为止我们实现了托盘图标的所有功能。
注意:类CSystemTray继承于CWnd是public形式,类的定义和实现可以参考文件:VioletPlay,文件名分别为SystemTray.h和SystemTray.cpp 源文件见:VioletPlay (注,有一些代码用的是VCKBASE网站中,我只对其进行了分析和总结。再此表示谢意和歉意
有的网友开发了自己的托盘类,实现起来略微烦琐。在这里我向大家推荐一个实现简单而十分有效的托盘类(是我在一本书中学来的)。
1、 把TrayIcon.cpp和TrayIcon.cpp拷贝到你的项目目录,并添加到项目中。
2、 在DemoDlg.h中加入#include “TrayIcon.h”
3、 通过类向导向类CDemoDlg添加成员变量CTrayIcon m_CTrayIcon;
4、 建立菜单资源,使其ID为:IDR_DEMO,设计菜单:
向demo1和exit添加事件处理程序:
void CDemoDlg::OnFileDemo1()
{
ShowWindow(SW_SHOW);
m_TrayIcon.RemoveIcon();
}
void CDemoDlg::OnFileExit()
{
m_TrayIcon.RemoveIcon();
OnCancel();
}
5、 在DemoDlg.cpp中自定义消息 #define WM_ICON_NOTIFY WM_USER+10,并在声明消息处声明消息处理函数: BEGI
N_MESSAGE_MAP(CDemoDlg, CDialog)
......
ON_MESSAGE(WM_ICON_NOTIFY, OnTrayNotification)
……
END_MESSAGE_MAP()
在类CDemoDlg中增加成员函数:LRESULT OnTrayNotification(WPARAM wParam,LPARAM lParam);
实现部分:LRESULT CDemoDlg::OnTrayNotification(WPARAM wParam,LPARAM lParam)
{
return m_TrayIcon.OnTrayNotification(wParam,lParam);
}
6、 在对话框添加“开始”按扭,并双击“开始”按扭编辑处理程序:
void CDemoDlg::OnBnClickedButton1()
{
m_TrayIcon.Create(this,WM_ICON_NOTIFY,"鼠标指向时显示",m_hIcon,IDR_DEMO); //构造
ShowWindow(SW_HIDE); //隐藏窗口
}
7、 可以灵活使用其它类成员函数。如:SetIcon改变图标,可以通过Timer消息实现托盘图标动画效果。
8、 删除托盘图标:m_TrayIcon.RemoveIcon();