任务栏(Taskbar)是微软公司在Windows 95中引入的一种特殊的桌面工具条,它为用户快速访问计算机资源提供了极大的方便,而状态栏(以下称通知栏)无疑是任务栏上较为特殊的一个窗口。编程人员可以调用API函数Shell_NotifyIcon向通知栏发送消息来添加、删除或修改图标,当在图标上发生鼠标或键盘事件时,系统会向应用程序发送编程时预先定义的消息,通知栏处理回调函数就会被自动调用以做出相应的处理。实现上述功能的相关文章俯仰即拾,此处不再赘述。本文将讨论通知栏编程中几个较为深入的问题及其在Delphi中的实现方法。
l 新版Windows操作系统引入的卡通风格的气泡提示(Balloon ToolTips)的实现及相关事件通知
l 外壳Explorer.exe崩溃而重启后通知栏图标的自动恢复
l 为通知栏图标快捷菜单选择适当的弹出时机
l 鼠标双击事件发生时单击事件的避免
1气泡提示(Balloon ToolTips)的实现
1.1显示气泡提示
我们知道,Shell_NotifyIcon函数需要传入指向某个特定结构的指针,系统根据该结构所包含的信息来决定是向通知栏添加、删除或修改图标。该结构的传统定义如下所示:
_NOTIFYICONDATAA = record
cbSize: DWORD;
//该结构的大小
Wnd: HWND;
//接收通知消息的窗口句柄
uID: UINT;
//图标标识(可以添加多个图标)
uFlags: UINT;
//指明该结构中哪些字段的值有效
uCallbackMessage: UINT;
//程序定义的接收通知的回调消息
hIcon: HICON;
//图标句柄
szTip: array [0..63] of AnsiChar;
//鼠标经过图标时显示的提示信息
end;
气泡提示(Balloon ToolTips)(如图1)是装有Internet Explorer 5及以上版本浏览器的操作系统(Windows Me/2000/XP,不包括Windows9x)中引入的通知栏图标的新行为,同时系统也定义了新版本的NOTIFYICONDATA结构,用于支持气泡提示。本文中将新结构取名为TNotifyIconData50,其Object Pascal定义及相关字段意义说明如下所示:
TNotifyIconData50 = record
前7个字段定义与_NOTIFYICONDATAA基本相同
uFlags: UINT;
//uFlags字段增加了如下常数定义
NIF_STATE:dwState、dwStateMask字段有效
NIF_INFO:szInfo、uTimeout、szInfoTitle、
dwInfoFlags字段有效
NIF_GUID:保留值
dwState: DWORD;
//图标状态
NIS_HIDDEN:图标是隐藏的
NIS_SHAREDICON:图标是共享的
dwStateMask: DWORD;
//指明dwState的哪些位可以被读取
如:设置为NIS_HIDDEN则表示图标的隐藏状态可以被读取
szInfo: array[0..255] of
AnsiChar;
//保存气泡提示字符串
uTimeout: UINT;
//气泡提示显示的持续时间
系统默认设置最短10秒,最长30秒
szInfoTitle: array[0..63]
of AnsiChar;
//保存气泡提示标题
dwInfoFlags: DWORD;
//指明是否在气泡提示上显示图标
NIIF_ERROR:“错误”图标
NIIF_INFO:“信息”图标
NIIF_NONE:不显示图标
NIIF_WARNING:“警告”图标
NIIF_ICON_MASK:保留值
NIIF_NOSOUND:不播放音效
end;
以下代码演示了在Delphi中如何实现气泡提示。
//{-------------------常数声明----------------------
Const
NIIF_NONE = $00000000;
NIIF_INFO = $00000001;
NIIF_WARNING = $00000002;
NIIF_ERROR = $00000003;
//--------------------------------------------------------------------------}
//{------------------类型声明--------------------
Type
TBalloonTimeout = 10..30; //气泡提示持续时间,单位为秒
TBalloonIconType = ( //气泡提示信息图标控制
bitNone, //不显示图标
bitInfo, //“信息”图标(蓝色)
bitWarning, //“警告”图标(黄色)
bitError); //“错误”图标(红色)
……
end;
//-----------------------------------------------}
//{---------填写公共结构----------------------------
procedure TEoCSysTray.FillDataStructure;
begin
with FIconData do
begin
cbSize := SizeOf(TNotifyIconData50);
wnd := FWindowHandle;
uID := 0;
uFlags := NIF_MESSAGE or NIF_ICON or NIF_TIP;
//uCallbackMessage、hIcon、szTip三个字段有效
hIcon := FIcon.Handle;
StrPCopy(szTip, FHint);
uCallbackMessage := WM_SYSTRAY;
end;
end; //end of procedure FillDataStructure
//--------------------------------------------------}
//{---------显示气泡提示信息----------------------
function TEoCSysTray.Balloon(Title, Text: string;
IconType: TBalloonIconType; Timeout: TBalloonTimeout): Boolean;
const
aBalloonIconTypes : array[TBalloonIconType] of Byte =
(NIIF_NONE, NIIF_INFO, NIIF_WARNING, NIIF_ERROR);
begin
if fActive then //若通知栏图标处于显示状态
begin //删除原先的气泡提示
FillDataStructure;
with FIconData do
begin
uFlags := uFlags or NIF_INFO; //设置与气泡提示相关的字段有效
StrPCopy(szInfo, ''); //设置提示信息为空,删除气泡提示
end;
Shell_NotifyIcon(NIM_MODIFY, @FIconData);
//以下显示新的气泡提示
FillDataStructure;
with FIconData do
begin
uFlags := uFlags or NIF_INFO;
StrPCopy(szInfo, Text);
uTimeout := Timeout;
StrPCopy(szInfoTitle, Title);
dwInfoFlags := aBalloonIconTypes[IconType];
end {with};
Result := Shell_NotifyIcon(NIM_MODIFY, @FIconData)
end
else
result := True;
end; //end of procedure Balloon
//---------------------------------------------------}
1.2气泡提示的事件通知
由于新风格提示的引入,通知栏图标的消息通知也相应增加,如果通知栏图标实现了气泡提示,那么当用户将鼠标指针移动到通知栏图标上时,Windows外壳会向通知栏应用程序送出如下四个消息中的一个或多个。
NIN_BALLOONSHOW
当气泡提示显示后外壳发送此消息
NIN_BALLOONTIMEOUT
当气泡提示由于超时而消失时外壳发送此消息
NIN_BALLOONHIDE
当气泡提示消失时(比如通知栏图标被删除)外壳发送此消息,但气泡提示由于超时而消失不会产生此消息
NIN_BALLOONUSERCLICK
当用户点击鼠标时(点击气泡提示和通知栏图标均可)外壳发送此消息
在Delphi强大的消息封装机制支持下,可以方便地将上述四个消息封装为四个事件供开发人员使用。简单来说就是在控件中一个隐藏窗口(创建隐藏窗口的方法可查阅相关文章,此处略过)的窗口消息处理过程中接收这四个消息并分别映射到四个事件,示范代码如下:
procedure TEoCSysTray.WndProc(var Msg: TMessage);
begin
……
case Msg.LParam of
WM_LBUTTONDOWN:
……
WM_RBUTTONDBLCLk:
……
else if Msg.lParam = NIN_BALLOONSHOW then //气泡提示显示后
begin
if Assigned(FOnBalloonShow) then
FOnBalloonShow(Self)
end
else if Msg.lParam = NIN_BALLOONHIDE then //气泡提示由于超时而消失
begin
if Assigned(FOnBalloonHide) then
FOnBalloonHide(Self)
end
else if Msg.lParam = NIN_BALLOONTIMEOUT then //气泡提示消失
begin
if Assigned(FOnBalloonTimeOut) then
FOnBalloonTimeOut(Self)
end
else if Msg.lParam = NIN_BALLOONUSERCLICK then //用户点击鼠标
begin
if Assigned(FOnBalloonClick) then
FOnBalloonClick(Self)
end
else
Msg.Result := DefWindowProc(FWindowHandle, Msg.Msg, Msg.wParam, Msg.lParam);
end;
end; //end of procedure WndProc
2Windows发生错误导致外壳Explorer重启时图标的重建
相信很多Windows用户都碰到过这种情况:运行某个程序时出现意外错误,导致外壳程序Explorer.exe崩溃而发生重启(即Explorer.exe被关闭后重新运行),任务栏也在消失后重新生成,但应用程序在通知栏添加的图标消失了,虽然这些程序仍在运行,但再也无法通过通知栏图标与用户交互。为避免这种情况出现,Windows提供了相应的机制。
在安装了Internet Explorer 4.0及以上版本的Windows操作系统中,当任务栏建立后,外壳会向所有顶层的应用程序发出通知消息,该消息是外壳以字符串“TaskbarCreated”为参数向系统注册获得的,应用程序窗口接收到该消息后就应该重新添加的通知栏图标。
在Delphi中实现过程如下:
initialization
MsgTaskbarRestart := RegisterWindowMessage(‘TaskbarCreated’);
1. 重载主窗口的消息处理过程,拦截任务栏重建消息,进行重新添加图标的操作。
procedure TMainForm.WndProc(var Message: TMessage);
begin
……
if Message.Msg = MsgTaskbarRestart then
begin
TrayIcon.Active := False; //删除通知栏图标
TrayIcon.Active := True; //添加通知栏图标
end;
……
inherited WndProc(Message);
end; //end of WndProc
值得一提的是,如果将自动恢复的功能封装为控件,将以后的开发带来方便。但由于外壳只向所有顶层的应用程序发送通知,封装起来有一定的困难。因为通知栏图标的回调函数只能接收WM_XBUTTONDOWN、WM_XBUTTONUP等有限的几个消息,并不能接收所有的窗口消息。
解决的方法是使用SetWindowLong函数。通过向它传入GWL_WNDPROC参数,可以改变一个窗口的窗口过程。只需在创建控件时将应用程序窗口的窗口过程指针保存起来,并指向为控件中的某个新的窗口处理过程,在控件中就能够响应所有的窗口消息了(包括任务栏重建的消息);当控件销毁的时候再将保存的原始窗口过程指针恢复即可,此处不再赘述。
3与通知栏图标关联的快捷菜单弹出的时机
本节将讨论编写通知栏应用程序时应该注意的一个问题,即快捷菜单弹出的时机问题。Windows为通知栏图标提供了几个鼠标消息(事件),那么我们应该将弹出快捷菜单的代码写在哪个事件中呢?先别急于回答“放在MouseDown事件中”,事实上,这个看似简单的问题,其中却小有讲究。许多软件(有的甚至号称专业级软件)也都或多或少忽视了这个问题。
首先需要明确一个软件设计中通用的原则,即:应当给用户一个机会以确认是否执行他选择的操作。这在软件设计中有很多例子。大的方面,最普遍的,如用户选择了删除文件,应弹出窗口予以确认。小的方面,如Windows中对鼠标的常规处理,也有一个确认的动作。一般来说,Windows中的程序对于鼠标事件的响应都是这样:在用户松开鼠标后才认为他确认了点击操作。以按钮(Button)为例,对于Windows的标准按钮,用户都可以在按下鼠标后而未松开鼠标前把鼠标移动到按钮区域以外来取消这次单击操作。再如Windows中窗口系统菜单的弹出,当用户在窗口标题栏上按下鼠标右键后,可以把鼠标移动到标题栏以外再松开,这样系统菜单就不会弹出,即等价于用户取消了该次操作。
遵照这个原则,通知栏快捷菜单的弹出显然应该在用户松开鼠标按键后,即WM_XBUTTONUP消息到来时才发生,以保证用户能够在松开鼠标之前取消其弹出,而不应简单的把弹出菜单的代码放在WM_XBUTTONDOWN的消息响应中。纵观Windows操作系统附带的程序,皆是如此。
4鼠标双击事件发生时单击事件的避免
编写过通知栏应用程序的朋友大概都碰到过这样的情况:如果编写了响应鼠标单击(WM_XBUTTONUP)与双击(WM_XBUTTONDBLCLK)的代码,那么在用户双击鼠标时单击事件也会发生。而在实际应用中通常希望单击与双击是相互独立的两个操作,它们之间不应该互相影响。对于这一问题,有些软件采用“鸵鸟战术”,不响应单击事件(即对WM_XBUTTONUP消息不作响应),只响应双击事件,这未尝不是一种解决办法,但浪费了单击事件,算不得好。通过下面的分析,我们将会看到一个较为令人满意的解决方法。
4.1原理分析
在Windows中并没有定义表示鼠标单击的消息,单击事件在Delphi等可视化编程语言中定义为鼠标按下后松开,因而单击事件一般在WM_XBUTTONUP中触发。而双击事件则不同,它在Windows中有明确的定义,当用户双击任意一个鼠标按键时,实际上按如下顺序Windows送出了四次消息:WM_XBUTTONDOWN、WM_XBUTTONUP、WM_XBUTTONDBLCLK、WM_XBUTTONUP。显然,如果响应WM_XBUTTONUP消息而触发了单击事件,那么双击时必然会先触发一次单击。
我们的目的是对双击事件单独处理,为此只需引入一个延时机制即可。让计时器在发生WM_XBUTTONDOWN时开始计时,待超时后检查WM_XBUTTONDBLCLK是否已经发生,若已发生则触发双击事件,否则触发单击事件。关键的是延时多久才合适呢?长了没有意义,短了可能超时后WM_XBUTTONDBLCLK都没有发生。显然应该至少延迟双击时两次单击之间的时间间隔,这一时间可以有系统API函数GetDoubleClickTime得到。
4.2解决方案
按照如下几个步骤对通知栏图标控件的代码稍加修改即可(注意WM_XBUTTONUP等消息中的“X”可为“L”、“M”、“B”,表示鼠标左键、中键、右键)。
A. 定义两个变量FMouseDblClicked和FMouseUp,分别用以指示双击和鼠标松开是否已经发生,均初始化为False。
B. 再为TEoCTrayIcon控件添加一个TTimer类成员变量FTimer,并在OnCreate事件中对它进行初始化:
constructor TEoCSysTray.Create(AOwner: TComponent);
begin
……
FMouseDblClicked := False;
FMouseUp := False;
FTimer := TTimer.Create(Self);
with FTimer do
begin
Enabled := False;
Interval := GetDoubleClickTime; //时钟间隔设为双击的时间间隔。
OnTimer := OnButtonTimer; //设置时钟超时响应过程。
end;
……
end; //end of Create
C. 接下来在前述重载的隐藏窗口消息处理过程中响应不同消息来设置上述两个变量的状态。
procedure TEoCSysTray.WndProc(var Msg: TMessage);
begin
……
case Msg.LParam of
WM_XBUTTONDOWN:
begin
……
FMouseDblClicked := False;//双击尚未发生
FMouseUp := False; //鼠标尚未松开
FTimer.Enabled := False; //结束上次延时
FTimer.Enabled := True; //开始延时
end;
WM_XBUTTONUP:
FMouseUp := True; //设置鼠标已经松开,便于Timer检查
WM_XBUTTONDBLCLk:
begin
FMouseDblClicked := True; //设置双击已经发生的标志
触发双击事件;
end;
else
Msg.Result := DefWindowProc(FWindowHandle, Msg.Msg, Msg.wParam, Msg.lParam);
end;
end; //end of WndProc
D. 在延时处理程序中判断鼠标状态,触发单击事件。
procedure TEoCSysTray.OnButtonTimer(Sender: TObject);
begin
FTimer.Enabled := False;
if (not FMouseDblClicked) and FMouseUp then //双击尚未发生且鼠标已松开
begin
触发单击事件;
触发MouseUp事件;
end;
end; //end of procedure OnButtonTimer
如此一来,单击事件就表现为WM_XBUTTONDOWN, Click, WM_XBUTTONUP,而双击事件则表现为WM_XBUTTONDOWN, WM_XBUTTONDBLCLK(过滤掉了两条MW_XBUTTONUP消息),从而避免了双击事件发生时触发单击事件。
5总结
关于通知栏图标的编程还有很多话题,比如动态切换图标、响应MouseLeave和MouseEnter事件等,在实际中都有应用,难以面面俱到。