第九章 图标与Windows任务条
如果问一个非程序人员Windows最好的特色是什么,得到的答案应该是系统最有吸引力的图标。无论是Windows98现在支持的通用串行总线(USB)还是WDM(看上去有点像一个软件协会而不象普通的设备驱动体系结构的缩写),图标在人们的心目中仍然是亲切的。你必须承认,微软总是从它的图形族群中获得最大的利益。
要了解使用图标表述菜单命令背后的的简单(或不简单)概念,你需要的不是绘制激情也不是艺术欣赏,而是应该清楚,仅使用32X32像素和16种颜色的图块做这样的表述是一个伟大的成绩。对于微软的图标,最值得欣赏的是即使在最低分辨率(16X16像素)下,它们也是清晰的和容易理解的。
随着Windows95的发布,图标巩固了它在Windows中已有的地位。种类也增加了—此时的图标有16X16甚至48X48的分辨率,而且系统也支持更多的颜色。在很多商业产品中使用256色图标是很普通的。
与图标相关的另一个主题是任务条。这并不是说任务条仅和图标有关。而是因为从程序按钮,快速启动工具条到托盘区域,它都非常好地使用了图标。
从编程人员的观点看,最好的消息是微软引进了SHGetFileInfo()函数,它的行为我们已经在第四章中彻底地讨论过了。无论它的名字怎样,这个函数对图标操作而言是最好的。此外随着活动桌面和后来的Windows98的引入,系统给出了一个与任务条一同工作的招牌接口。任务条窗口的结构(以及桌面本身的结构)有了相当的改变。如此,在这一章中我们打算:
对操作图标的函数提供一个总的说明
示范说明怎样从模块中抽取图标
首先给出一种方法在托盘区域放置图标,而后是管理托盘区域图标
检测新任务条的布局
解释关于任务条的没有说明资料的COM接口
在这一章中我们还写了一个浏览包含在任何可执行文件中图标的函数,以及一段自动重启Shell的代码,更重要的是这段代码能够感觉到什么时候Shell重启动了。后一点直接关系到shell32.dll管理托盘图标代码的一个可能的Bug。
关于图标应该知道的
图标可以用于标识任何Shell命名空间中出现的对象;它与Bitmap的主要差别是使用位屏蔽方式显示。在与像素层组合时,这个屏蔽使图标相对于下面的背景有透明的感觉。图标可以是单个资源或一组相关的图象,它们在不同分辨率和颜色深度上重复同一个主题。
在整个Windows Shell范围内,图标都是由一个称之为IExtractIcon的COM接口管理,这在第五章中已经遇到了。IExtractIcon是由封装命名空间扩展的代码实现的,对于文件型文件夹,是在shell32.dll中的代码。然而,你可以通过Shell扩展模块提供你自己的IExtractIcon,这样你就可以客户化Shell图标,在15章中我们将展示应该怎样做。
Windows提供了一个标准图标集,应用可以加载和使用它们而无须卸载。这些图标都由带有IDI_前缀的符号标识,它们定义在winuser.h中—典型的例子是IDI_ICONQUESTION 和 IDI_ICONSTOP,在使用MessageBox()时,可以交叉使用它们。
在建立和加载图标时,要分配一个唯一的Handle,其类型是HICON。很多Win32函数操作图标时都要求一个这种类型的Handle。你必须释放所有在你的程序模块中显式建立和抽取的图标,但是对于系统图标,象上面提到的那些图标则不应该这样,因为它们属于系统,只有在可以释放的时候才释放。
建立图标
建立图标有许多种方法。你可以使用图像编辑器建立.ico文件,或使用资源编辑器在.res文件中把图标与应用的其他资源一起编译。编程建立图标也是可能的,此时,可以使用的函数是:
CreateIcon()
CreateIconFromResource()
CreateIconIndirect()
然而,在程序代码中建立图标的最好方法是使用通用控件:图像列表。
编程建立和修改图标
在第5章中我们举例说明过怎样使用图像列表控件编程修改已存在的图标。我们还特别说明了怎样动态地组合两个图标。这个例子产生了一个手握文件夹的图标,这个图标是系统用于说明给定文件夹是一个共享文件夹的。
建立一个图标太容易了,所要做的就是把一个图标或图像放入一个图像列表控件中,然后通过ImageList_GetIcon()读出来即可。例如,如果有一个HBITMAP,则,下面的代码把它转换成图标:
HICON HBitmapToHIcon(HBITMAP hbm, int cx, int cy)
{
HIMAGELIST himl = ImageList_Create(cx, cy, ILC_COLOR, 1, 1);
int i = ImageList_Add(himl, hbm, NULL);
HICON hIcon = ImageList_GetIcon(himl, i, ILD_NORMAL);
ImageList_Destroy(himl);
return hIcon;
}
这是一个相当简单的实现。在调用ImageList_Create()个ImageList_GetIcon()过程中你可能已经探索了许多其它的ILC_和ILD_标志,但是,我们还是建议你进一步查阅MSDN库资料中这些函数的详细说明。
一个以这种方式获得图标的应用必须注意当不再需要时释放这个图标。Bitmap图像和图标在这种情况下是十分相似的。你可以用GetIconInfo()函数从HICON中抽取ICONINFO结构:
BOOL GetIconInfo(HICON hIcon, PICONINFO piconinfo);
这个结构描述了一个图标,有如下定义:
typedef struct _ICONINFO
{
BOOL fIcon; // TRUE 如果这个结构引用了一个图标
DWORD xHotspot; // 热点的x-坐标(见下面)
DWORD yHotspot; // 热点的y-坐标(见下面)
HBITMAP hbmMask; // 使图标透明的位屏蔽
HBITMAP hbmColor; // 图标的彩色图像
} ICONINFO;
如上所见,在任何图标内部都有一个HBITMAP。ICONINFO用于双重目的:用于描述图标和光标的内部结构,fIcon成员标识资源的实际类型—TRUE标识图标,FALSE表示光标。
不要混淆了‘热点’成员。与光标一样,图表也有热点,但是它的热点总在中心区域。对于光标,热点是可以改变的。这个结构最值得注意的部分是两个HBITMAP成员,它们说明系统提供了从HICON到 HBITMAP转换的手段。下面是一个简单而又直接的封装:
HBITMAP HIconToHBitmap(HICON hIcon)
{
ICONINFO ii;
GetIconInfo(hIcon, &ii);
return ii.hbmColor;
}
绘制图标
无论有多少可供编程建立图标使用的方法,最终都需要从外部文件加载它们。有几个函数做这个工作,但是最常用的是LoadIcon()和LoadImage()。过一会我们将解释这些函数。即使绘制图标,也需要使用几个函数把图标放到屏幕上。通常,这与你的需求有关,最简单的方法是调用DrawIcon():
BOOL DrawIcon(HDC hdc, int x, int y, HICON hIcon);
它是快速而易于使用的,但是它不是很灵活。这个函数在32X32分辨率和16色彩时曾经被介绍过。现在有许多类型的图标,像这样一个简单的函数是不能满足需求的。DrawIcon()函数可用于绘制由Handle提供的大/小图标,然而这也限制了它的灵活性。
如果需要比DrawIcon()函数更多的功能,一个好方法是使用ImageList_Draw()。这个函数允许使用图形滤波器,如‘混合’。驻留在Windows Shell中的‘选中’或‘隐藏’图标都是由这种技术实现的。
活动图标
活动图标已经大部分由动画GIF和AVI文件所取代,但是如果在某些交互的场合,需要使用这种图标,则可以使用DrawIconEx()API函数。它也给出一种拉伸图标到指定尺寸的方法。
从文件中抽取图标
从文件中抽取图标,我们可以选择使用ExtractIcon()或ExtractIconEx(),以及 ExtractAssociatedIcon(),LoadImage()和SHGetFileInfo()。下面我们比较和对照一下这些函数的能力:
函数 | 描述 |
ExtractIcon() | 从一个文件中抽取指定索引位置的图标,索引从0开始。这个函数总是返回大图标。 |
ExtractIconEx() | 与ExtractIcon()相同,但是可以抽取大图标和小图标。 |
ExtractAssociatedIcon() | 返回与给定文件或路径关联的大图标。 |
SHGetFileInfo() | 返回给定文件,路经或PIDL的大/小图标,也可以应用于某些图形效果,如第4章所述。 |
LoadImage() | 从给定文件中抽取期望分辨率的图标,这时唯一能取得48X48图标的方法。 |
LoadIcon() | 从给定可执行文件的资源中抽取图标,源文件由实例标识,不是由文件名标识,图标由ID标识不是由索引标识。 |
如上所示,我们在描述中区分了返回图标的函数和抽取图标的函数。第一组成员取文件,文件夹或PIDL作为输入,以及遍历注册表来加载默认图标。它们是SHGetFileInfo()和ExtractAssociatedIcon()。在第二组中的函数期望带有资源的文件名(EXE,DLL,ICO等),函数遍历这个资源来查找指定的图标,这是由从零开始的索引指定的。
抽取和返回之间的区别基本上是理论上的,因为所有函数都给出HICON作为结果,只是输入的参数有稍微的不同而已。决定是加载和返回或抽取图标依赖于你能向函数提供什么样的信息。如ExtractIcon()函数要求一个HINSTANCE变量,而它的姊妹函数ExtractIconEx()就不需要。现在,ExtractIconEx()函数的原型似乎更自然一些:
HICON ExtractIcon(HINSTANCE hInst,
LPCTSTR szFile,
UINT nIconIndex);
UINT ExtractIconEx(LPCTSTR lpszFile,
int nIconIndex,
HICON* phiconLarge,
HICON* phiconSmall,
UINT nIcons);
如上所述,ExtractIconEx()可以同时取得大图标和小图标。此外,它还能通过ID恢复图标,只是需要使用一个小窍门,给nIconIndex赋一个负值的ID,例如,要获取ID值为1001的图标,nIconIndex需要取值为-1001。注意,这是32位环境一个特殊的增强。在16位下是不可用的。
当然这项技术对没有数值ID的图标是不能工作的,此时,必须使用索引引用图标。
ExtractAssociatedIcon()函数是SHGetFileInfo()函数的一个早期版本:
HICON ExtractAssociatedIcon(HINSTANCE hInst, LPTSTR lpIconPath, LPWORD lpiIcon);
它在指定文件(或相关的可执行文件)中对索引图标进行搜索,并总是返回大图标。这个函数检查lpIconPath是否是一个嵌入了图标的文件,以及是否能成功地通过lpiIcon索引抽取到图标。其功能几乎等价于ExtractIconEx()。如果lpIconPath不包含图标,ExtractAssociatedIcon()函数则努力在基类中查找,它通过察看文件扩展名(BMP,DOC等)来判定文件类型,和遍历注册表取得那个类型的图标。
WORD wID;
ExtractAssociatedIcon(hInst, __TEXT("c:/myfile.doc"), &wID);
例如,如果微软的Word已经安装,上面代码返回相关于Word文档的图标。有趣的是lpiIcon是一个输入/输出参数,被设置为选中图标的ID。
在第4章中我们仔细地检视了SHGetFileInfo()函数的特征。但是要记住,它并不允许你用数字从文件中挑选图标。
关于LoadImage()和LoadIcon()函数
花一点时间讨论LoadImage()和LoadIcon()是值得的。几年来LoadIcon()函数都是访问应用和系统图标的唯一方法,而且它有一个简单且易于记忆的原型:
HICON LoadIcon(HINSTANCE hInst, LPCTSTR szIconName);
不幸地是这个函是不允许加载来自ICO文件的图标,而是要求被装入存储器能够抽取图标的可执行文件(DLL,EXE,OCX,DRV等)。事实上它通过一个HINSTANCE类型的Handle查找资源。在这方面(和其它几个方面)LoadImage()函数就有了极大的改进。例如,它提供从磁盘文件加载图标的能力并且满足你的尺寸要求。如果这样的图标存在,函数就装入它,否则,最接近的图标被拉伸到要求的尺寸。
HICON hIcon = LoadImage(hInst, szIconName, IMAGE_ICON, 48, 48, LR_DEFAULTCOLOR);
上面这行代码说明怎样装入一个48x48的图标。此外,LoadImage()的最后一个参数可用于滤波图标的颜色。
加载系统图标
加载系统图标,如Windows的商标或询问标记,你应该传递一个NULL应用实例:
HICON hIcon = LoadIcon(NULL, MAKEINTRESOURCE(IDI_WINLOGO));
不需要释放这个图标,因为它属于系统,在系统停止时才被释放。
如果你不熟悉SDK编程,应该注意,MAKEINTRESOURCE()宏用于转换数字ID到串,适用于装入资源的LoadXXX()函数。MAKEINTRESOURCE()也在MFC中使用,但对程序员是隐藏的。
系统图像列表
只要Shell或应用程序使用图标,系统为了提供快速访问和易于处理总是缓存它们。这个缓存是通过一个图像列表的方法实现的。你可以通过SHGetFileInfo()函数由指定SHGFI_SYSICONINDEX标志取得列表的Handle。如果需要小图标,只需加入SHGFI_SMALLICON标志即可(详细请参考第4章)。
哪种方法最好
然而哪一种是抽取图标最好的方法呢,就我们的经验,如果只是想从文件中获得图标,而且不需要提供小图标,我们推荐使用ExtractIcon()。如果需要小图标,绝对必须使用ExtractIconEx()。
顺着这个思路,想要了解Shell所关联的文件对象(驱动器,文件夹,打印机,普通文件等)的图标时,使用SHGetFileInfo()函数。LoadImage()函数比LoadIcon()函数复杂得多,所以,我们建议只有在需要特殊分辨率的图标时,如 48x48像素,再借助于此函数。
到这里已经清晰地概览了底层图标编程技术。如果需要更进一步的信息,应该参考MSDN库。
指派对话框的图标
如果你正在建立顶层窗口,或更一般地,你能够控制你的窗口类,则指派图标就不是什么难事了,你只需要适当地设置WNDCLASS结构成员,和调用RegisterClass()函数。如果还想处理小图标,则应该使用WNDCLASSEX和RegisterClassEx(),概念是相同的。然而,对于对话框,应该怎样做呢?它们都有相同的系统定义类WC_DIALOG(取值为#32770),在其上没有控制,而且,你是要改变分配给这个类的图标,整个系统范围内的对话框都将受到影响。可以通过调用SetClassLong()函数改变所有对话框的图标,但是因为可能影响到整个系统,所以我们不推荐这么做:
SetClassLong(hDlg, GCL_HICON, reinterpret_cast<LONG>(hIconNew));
hDlg是一个窗口Handle,它用于间接地引用这个窗口的类。换句话说,这个函数改变属于这个类的窗口的图标。
幸运地,如果你只是想改变单一对话框的图标,有两个消息允许你这样做:WM_SETICON和 WM_GETICON。就象名字提示的,前者可以设置图标到特殊的对话框,后者可以读出当前的HICON。你可以在任何需要的时候调用下面的代码(典型地是响应WM_INITDIALOG消息的时候):
SendMessage(hDlg, WM_SETICON, FALSE, reinterpret_cast<LPARAM>(g_hIconSmall));
SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast<LPARAM>(g_hIconLarge));
消息的lParam变量是HICON,大或小图标。wParam则告诉系统怎样存储和操作图标—实际上是指示系统图标应该存储在哪一个图像列表上,FALSE说明小图标,TRUE是大图标。相反,下面代码说明怎样从对话框窗口获得图标(大图标和小图标):
HICON hIconSm = SendMessage(hDlg, WM_GETICON, ICON_SMALL, 0);
HICON hIconLg = SendMessage(hDlg, WM_GETICON, ICON_BIG, 0);
浏览图标
浏览图标能力是很多程序可以装饰的特征。不幸地是,没有资料说明编程生成下图对话框的方法:
当你到开一个快捷方式的属性对话框并点击‘改变图标…’时出现这个对话框,这个图显示了所有包含在Explorer.exe中的图标。要写出一个函数(定名为SHBrowseForIcon())向上图中对话框那样工作有多困难呢?事实上比想象的要容易,我们下面将给出解释。
SHBrowseForIcon()函数
我们选择下面的原型,并把它加入到SHHelper.h中:
int SHBrowseForIcon(LPTSTR szFile, HICON* lphIcon);
SHBrowseForIcon()接受要浏览的文件名,和一个Handle指针,在其中函数存储选择的图标。成功,函数返回取得的基于0的图标索引,失败,返回-1。
当然,这个函数需要一个对话框模版,上面的截图显示了应用的界面—它的标识符为IDD_BROWSEICON。SHBrowseForIcon()函数的行为是直观的,可以概括为下面几步:
建立图像列表来控制所有包含在文件中的图标
抽取图标并填充图像列表
用列表控件关联图像列表,并充填列表控件
取得当前选中的图标,恢复它在图像列表中的索引
从图像列表中抽取,并返回
这个函数的代码如下:
int SHBrowseForIcon(LPTSTR szFile, HICON* lphIcon)
{
// 函数假设图表的默认尺寸(通常是32 x 32)
int cx = GetSystemMetrics(SM_CXICON);
int cy = GetSystemMetrics(SM_CYICON);
lstrcpy(g_szFileName, szFile);
g_himl = ImageList_Create(cx, cy, ILC_MASK, 1, 1);
DialogBox(g_hThisDll,MAKEINTRESOURCE(IDD_BROWSEICON),
GetFocus(),BrowseIconProc);
// 释放图像列表
ImageList_Destroy(g_himl);
// 设置返回值(文件可能已经改变)
*lphIcon = g_hIcon;
lstrcpy(szFile, g_szFileName);
// 这个索引由对话框过程设置
return g_iIconIndex;
}
首先,我们建立一个全程的图像列表,指定我们感兴趣的默认图像尺寸(通常是32x32像素)。然后显示对话框,对话框关闭之后,我们销毁图像列表和设置返回值—选中的图标(或NULL,如果对话框被取消)和它的索引。此处我们使用了由对话框窗口过程设置的全程变量。此外,由于我们的对话框模版提供了浏览按钮,因此提供选择图标的文件与初始调用者应用给定的文件可能不是相同的文件。我们也使用szFile缓冲返回文件按名。下面的代码包含了对话框的窗口过程和某些内部使用的函数。
BOOL CALLBACK BrowseIconProc(HWND hDlg, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
switch(uiMsg)
{
case WM_INITDIALOG:
OnInitDialog(hDlg);
break;
case WM_COMMAND:
switch(wParam)
{
case IDC_BROWSE:
OnBrowse(hDlg);
break;
case IDCANCEL:
EndDialog(hDlg, FALSE);
return FALSE;
case IDOK:
DoGetIcon(hDlg);
EndDialog(hDlg, TRUE);
return FALSE;
}
}
return FALSE;
}
void OnInitDialog(HWND hDlg)
{
HWND hwndList = GetDlgItem(hDlg, IDC_LIST);
SetDlgItemText(hDlg, IDC_FILENAME, g_szFileName);
ListView_SetImageList(hwndList, g_himl, LVSIL_NORMAL);
DoLoadIcons(hDlg, g_szFileName);
}
void OnBrowse(HWND hDlg)
{
TCHAR szWinDir[MAX_PATH] = {0};
TCHAR szFile[MAX_PATH] = {0};
// 浏览文件...
OPENFILENAME ofn;
ZeroMemory(&ofn, sizeof(OPENFILENAME));
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.lpstrFilter = __TEXT("Icons/0*.exe;*.dll;*.ico/0");
ofn.nMaxFile = MAX_PATH;
GetWindowsDirectory(szWinDir, MAX_PATH);
ofn.lpstrInitialDir = szWinDir;
ofn.lpstrFile = szFile;
if(!GetOpenFileName(&ofn))
return;
SetDlgItemText(hDlg, IDC_FILENAME, ofn.lpstrFile);
DoLoadIcons(hDlg, ofn.lpstrFile);
lstrcpy(g_szFileName, ofn.lpstrFile);
}
SHBrowseForIcon()的核心是DoLoadIcons()和DoGetIcon()函数。它们抽取图标和填充列表控件,当用户点击‘OK’按钮后取得选中的图标。
int DoLoadIcons(HWND hDlg, LPTSTR szFileName)
{
TCHAR szStatus[30] = {0};
// 取得图标数
int iNumOfIcons = reinterpret_cast<int>(ExtractIcon(g_hThisDll,
szFileName, -1));
// 更新用户界面
HWND hwndList = GetDlgItem(hDlg, IDC_LIST);
ListView_DeleteAllItems(hwndList);
wsprintf(szStatus, __TEXT("%d icon(s) found."), iNumOfIcons);
SetDlgItemText(hDlg, IDC_ICONCOUNT, szStatus);
// 同时充填图像列表和列表观察
for(int i = 0 ; i < iNumOfIcons ; i++)
{
HICON hIcon = ExtractIcon(g_hThisDll, szFileName, i);
int iIndex = ImageList_AddIcon(g_himl, hIcon);
// 加到罗列表观察
LV_ITEM lvi;
ZeroMemory(&lvi, sizeof(LV_ITEM));
lvi.mask = LVIF_IMAGE;
lvi.iItem = iIndex;
lvi.iImage = iIndex;
ListView_InsertItem(hwndList, &lvi);
}
return iNumOfIcons;
}
void DoGetIcon(HWND hDlg)
{
HWND hwndList = GetDlgItem(hDlg, IDC_LIST);
// 取得列表观察的选择项索引
g_iIconIndex = -1;
int i = ListView_GetNextItem(hwndList, -1, LVNI_SELECTED);
if(i == -1)
return;
g_iIconIndex = i;
// 取得选择项信息
LV_ITEM lvi;
ZeroMemory(&lvi, sizeof(LV_ITEM));
lvi.mask = LVIF_IMAGE;
lvi.iItem = i;
ListView_GetItem(hwndList, &lvi);
// 取得图标的图像列表索引,和返回HICON
g_hIcon = ImageList_GetIcon(g_himl, lvi.iImage, 0);
}
浏览一下列表观察,不能理解的是为什么它们有如此离奇和混乱的借口—我们使用特殊的算法才能获得选择项。当然,如果你不知道曾经的答案(或谁提的问题),这个运气是你需要求助于自绘制列表框。
当我们已知了选中的列表观察项的索引后,我们就能够避免通过列表观察和图像列表获得这个HICON—可以使用当前文件名和图标索引再调用ExtractIcon()。由于没有必要再次访问磁盘文件,我们在这里使用的是你所看到的方法,主要原因是它更有效。如果系统本身是动态地维护图像列表的,我们有理由相信,这是最好的方案。
要编译和连接这个DLL,你还需要把SHBrowseForIcon()加到DEF文件中去,并且完成头文件和库文件列表。头文件要求shlobj.h,resource.h,commdlg.h 和shellapi.h。需要连接的库文件是comctl32.lib和comdlg32.lib。
怎样调用SHBrowseForIcon()
11章中将给出使用SHBrowseForIcon()函数的例子,为了结束这一节,我们快速观察一下外部应用可以怎样调用它:
int iIconIndex = SHBrowseForIcon(szFileName, &hIcon);
if(iIconIndex >= 0)
{
...
}
托盘通知区
托盘通知区(TNA)是一个TrayNotifyWnd类的窗口,定位在任务条的右边(当任务条水平放置时)。
默认情况下系统放置一个含有时钟的子窗口到TNA中。在系统启动期间某些图标默认地显示在TNA上是通过systray.exe程序设置的,它可以依赖机器的硬件驱动添加图标到TNA,典型地,如果安装了声卡或机器是笔记本电脑,则它添加相关的图标。如果你希望自己的图标启动时出现在TNA上,你就必须写一段应用程序来管理TNA,并把程序放到‘启动’文件夹下。
在第7章中我们遇到过TNA,在我们讨论通过探测器点击一键建立文件夹时。这里我们将更深入地研究怎样管理TNA中的图标。
当然也有函数来编程地添加或删除托盘区图标,它的名字是Shell_NotifyIcon()。放进托盘区的图标都有一个ID,工具标签文字,关联菜单和一个与之通讯和通知鼠标事件的窗口。你大概也会发现我们将要运行的代码中还有两个讨厌的Bugs。
将图标放入托盘通知区
Shell_NotifyIcon()函数有下面的原型:
BOOL WINAPI Shell_NotifyIcon(DWORD dwMessage, PNOTIFYICONDATA pnid);
NOTIFYICONDATA是一个结构,它包含了构造托盘通知区图标的所有数据。dwMessage参数指出我们想要完成的活动:
活动 | 描述 |
NIM_ADD | 添加新图标到托盘区 |
NIM_DELETE | 从托盘区删除一个存在的图标 |
NIM_MODIFY | 修改托盘区存在的图标 |
每一个图标都由下面的结构完整描述:
typedef struct _NOTIFYICONDATA
{
DWORD cbSize;
HWND hWnd;
UINT uID;
UINT uFlags;
UINT uCallbackMessage;
HICON hIcon;
char szTip[64];
} NOTIFYICONDATA, *PNOTIFYICONDATA;
成员 | 描述 |
cbSize | 包含结构的尺寸 |
hWnd | 从图标接收通知消息的窗口 |
uID | 图标标识符—即,允许调用者应用程序唯一识别图标的用户定义值 |
uFlags | 指示这个函数使用什么样的成员组合:uCallbackMessage,hIcon和szTip由NIF_MESSAGE,NIF_ICON 和NIF_TIP分别表示。如果你使用这些成员,一定要记住打开对应的标志。 |
uCallbackMessage | 图标用于与hWnd窗口通讯的消息ID。要求NIF_MESSAGE设置到uFlags中 |
hIcon | 要显示的图标Handle 。它应该是小图标(16x16),但是如果需要,系统自动拉伸。要求NIF_ICON设置在uFlags中 |
szTip | 显示工具标签的文字,最大64字节长,要求NIF_TIP被设置到uFlags中。 |
有了这些知识,把一个图标放入托盘通知区就简单了,你可以这样做:
NOTIFYICONDATA nid;
ZeroMemory(&nid, sizeof(NOTIFYICONDATA));
nid.cbSize = sizeof(NOTIFYICONDATA);
nid.hWnd = hWnd;
nid.uID = ICON_ID;
nid.uFlags = NIF_TIP | NIF_ICON | NIF_MESSAGE;
nid.uCallbackMessage = WM_MYMESSAGE;
nid.hIcon = hSmallIcon;
lstrcpyn(nid.szTip, __TEXT("This icon's been added by me!"), 64);
Shell_NotifyIcon(NIF_ADD, &nid);
删除图标要简单得多,因为不必设置任何其他成员,只是指定uID和cbSize即可。你在任何时候都可以修改任何前面设置的变量以反映你应用中的变化。此时可以使用NIM_MODIFY来代替NIM_ADD调用Shell_NotifyIcon()。
例如OutLook Express,在发送或接收数据时使用NIF_MODIFY来显示少许的动画。类似地,当有新的未读邮件时,信封图标的显示使用NIM_ADD消息添加,然后通过NIM_DELETE来删除。
鼠标事件通知
在讨论托盘图标时,有个不正确的表述(一般是可以接受和理解的)是“图标通知窗口所有事件”。事实上,这个句子的主题出现了错误,我们应该说,“TrayNotifyWnd窗口通知指定窗口所有鼠标相关的事件”。实际的图标被绘制在TrayNotifyWnd窗口的客户区域。窗口的大小是根据它所包含的图标数目和任务条泊靠的屏幕边缘变化的。
如果TrayNotifyWnd窗口感觉到鼠标正在做影响其中一个图标的活动,它就通知与这个图标相关的窗口(NOTIFYICONDATA结构的hWnd成员)发生了什么。事实上当鼠标在图标界定的矩形上移动,点击或右击时有系统产生的消息发送到这个窗口。作为示例,发送到消息有下面这种形式:
SendMessage(nid.hWnd, nid.uCallbackMessage, nid.uID, lParam);
SendMessage()的wParam变量标识事件发起的图标,而lParam是消息码:WM_RBUTTONUP,WM_LBUTTONUP,WM_MOUSEMOVE等。注意,这里没有相对于初始消息的信息(例如,鼠标位置)被发送到应用窗口。下面是窗口怎样处理来自托盘图标通知的代码:
case WM_MYMESSAGE:
if(wParam == ICON_ID)
{
switch(lParam)
{
case WM_RBUTTONUP:
ShowContextMenu();
break;
case WM_LBUTTONUP:
DoMainAction();
break;
}
}
正常情况下与托盘图标关联的窗口要做两件事:
响应右击图标显示关联菜单
在用户点击图标时执行原始活动,大多数情况下是显示对话框
对于显示工具标签,窗口不需要做任何事,工具标签由TrayNotifyWnd窗口透明处理。
编写托盘应用
基于托盘的应用与其它Windows程序的布局稍有不同,它应该有主窗口,但是在大多数场合,都是不可见的。这个窗口在后台接收和处理事件,仅在用户单击或双击这个图标后才可能显示。没有规则来阻止应用既有一个可视的主窗口又有一个托盘图标。然而,你可以用托盘图标作为一个用户提示,表明你的程序正在后台启动和运行—这特别适用于不要求大量用户干预的程序。概念是在必要的时候点击托盘图标弹出用户界面进行操作。
托盘应用可以看作等价于老MS-DOS下TSR(终止并驻留)程序的Windows程序。如果你没有接触过MS DOS程序,可以理解为TSR程序装入的空闲程序,直到是用特殊键组合唤醒它,而后它弹出对话。
在下面几页我们将给出一个简单托盘应用的必要代码。在WinMain()函数中,我们首先装入要放置到托盘区的小图标,然后建立接收消息的对话框。一旦设置了图标(由TrayIcon()完成的任务),我们就进入循环以保持程序活动和运行。在退出循环后,释放图标和终止应用。
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
// 复制全程实例Handle
g_hInstance = hInstance;
// 加载要放入托盘的16x16图标
HICON hSmallIcon = reinterpret_cast<HICON>(LoadImage(hInstance,
__TEXT("APP_ICON"), IMAGE_ICON, 16, 16, 0));
// 建立不可视对话框以从图标接收消息
HWND hDlg = CreateDialog(hInstance, __TEXT("DLG_MAIN"), NULL, APP_DlgProc);
// 显示图标
TrayIcon(hDlg, hSmallIcon, NIM_ADD);
// 进入循环保持程序运行
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
if(!IsDialogMessage(hDlg, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// 删除图标和退出
TrayIcon(hDlg, hSmallIcon, NIM_DELETE);
DestroyWindow(hDlg);
DestroyIcon(hSmallIcon);
return 1;
}
与传统的Windows程序有哪些不同,答案是你没有使主窗口可视,相反必须使用托盘图标。下一个函数显示怎样做这个工作。设置uFlags字段的值就是希望支持回调消息,图标和工具标签。
回调消息是用户定义的消息,用WM_APP偏移进行声明:
const int WM_EX_MESSAGE = (WM_APP + 1);
BOOL TrayIcon(HWND hWnd, HICON hIcon, DWORD msg)
{
NOTIFYICONDATA nid;
ZeroMemory(&nid, sizeof(NOTIFYICONDATA));
nid.cbSize = sizeof(NOTIFYICONDATA);
nid.hWnd = hWnd;
nid.uID = ICON_ID;
nid.uFlags = NIF_TIP | NIF_ICON | NIF_MESSAGE;
nid.uCallbackMessage = WM_EX_MESSAGE;
nid.hIcon = hIcon;
lstrcpyn(nid.szTip, __TEXT("This icon's been added by me!"), 64);
// 执行特殊的图标操作
return Shell_NotifyIcon(msg, &nid);
}
典型的托盘应用例子是音量控制,它出现在几乎所有的Windows系统中,当你点击图标时,配置对话框出现:
关注关联菜单
托盘应用的一个普通特征是右击图标时出现的关联菜单。下面是一个典型托盘程序隐藏窗口的的窗口过程。
BOOL CALLBACK APP_DlgProc(HWND hDlg, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
switch(uiMsg)
{
case WM_COMMAND:
switch(LOWORD(wParam))
{
case IDCANCEL:
PostQuitMessage(0);
return FALSE;
}
break;
case WM_EX_MESSAGE:
if(wParam == ICON_ID)
{
switch(lParam)
{
case WM_RBUTTONUP:
ContextMenu(hDlg);
break;
}
}
break;
}
return FALSE;
}
在指定的消息被接收并且验证(这是重要的,因为某些应用可以有多个图标)是相关的图标后,你就可以显示关联菜单了,关联菜单完全由与图标关联的窗口管理,不是系统托盘的特征。
显示关联菜单不是问题,下面是它的代码:
void ContextMenu(HWND hwnd)
{
HMENU hmenu = LoadMenu(g_hInstance, MAKEINTRESOURCE(IDR_MENU));
HMENU hmnuPopup = GetSubMenu(hmenu, 0);
SetMenuDefaultItem(hmnuPopup, IDOK, FALSE);
POINT pt;
GetCursorPos(&pt);
TrackPopupMenu(hmnuPopup, TPM_LEFTALIGN, pt.x, pt.y, 0, hwnd, NULL);
DestroyMenu(hmnuPopup);
DestroyMenu(hmenu);
}
这段代码从应用的资源中装入菜单,抽取第一个弹出菜单,调用SetMenuDefaultItem()声明一个用粗体字绘制的默认项。当右击图标时,关联菜单显示,正像你期望的那样。然而首次运行,你并没有测试菜单命令,所以,在菜单外点击使它消失。你将发现这个菜单倔强地保留在那里,只有鼠标再次移动到它上面时它才消失。换言之,你最终使菜单不安地隐藏在任务条之后:
这是一个已知的Bug,但是你可以通过封装TrackPopupMenu()或TrackPopupMenuEx()到一对SetForegroundWindow()调用之间:
void ContextMenu(HWND hwnd)
{
HMENU hmenu = LoadMenu(g_hInstance, MAKEINTRESOURCE(IDR_MENU));
HMENU hmnuPopup = GetSubMenu(hmenu, 0);
SetMenuDefaultItem(hmnuPopup, IDOK, FALSE);
POINT pt;
GetCursorPos(&pt);
SetForegroundWindow(hwnd);
TrackPopupMenu(hmnuPopup, TPM_LEFTALIGN, pt.x, pt.y, 0, hwnd, NULL);
SetForegroundWindow(hwnd);
DestroyMenu(hmnuPopup);
DestroyMenu(hmenu);
}
这就保证了所有输入都重定向到我们的窗口,并解除了菜单锁定。这个Bug在TrayNotifyWnd窗口的代码中,不是TrackPopupMenu()或我们应用的Bug。
托盘通知区中有多少图标
我们不能保证是否总有结果出现,但是有一种没有说明资料的方法能够编程地找出有多少图标被存储在托盘通知区。如果对于你的应用是重要的话,你可以通过检查TrayNotifyWnd窗口的尺寸来获得结果。这个工作并不简单,因为你必须考虑任务条泊靠不同边缘的情况,以及是否显示时钟窗口。如果任务条是垂直排列的,则图标通常都显示在时钟下方,但是,如果任务条足够宽,则它们将顺序绘制。此外图标有时是单列绘制的。因此,取得不同图片数的可能性是大的,可能根本就是乱的。
感知Shell重启动
如果在任何情况下Shell重新启动,其托盘通知区上的图标是不能恢复的。这显然是由于Shell代码Bug所致,与托盘上有多少图标有关,这是相当麻烦的。然而重启Shell不是一个我们期望的频繁操作,从经验上看,有两种情况可能必须重启Shell:恢复探测器冲突(GPF),或在测试Shell扩展时建立新实例。前者是系统重建了Shell对象的新实例,后者则完全在于我们自己。我们可以编程或手动地做这个操作。
如果需要编程重启Shell,可以采用下面的代码:
void SHShellRestart()
{
HWND hwnd = FindWindow(__TEXT("Progman"), NULL);
PostMessage(hwnd, WM_QUIT, 0, 0);
ShellExecute(NULL, NULL, __TEXT("explorer.exe"), NULL, NULL, SW_SHOW);
}
我们首先退出Shell的主窗口,然后再次运行它。在导出explorer.exe时,它首先验证是否有另一个运行着的实例,如果没有,它就建立任务条和初始化Windows Shell,否则,简单地弹出传统的浏览器。
在WindwosNT下,每次在空桌面上导出explorer.exe时都建立任务条。用Windows NT的行话将,桌面实际是屏幕的工作区,具有菜单,图标窗口,钩子和运行程序。所不同的是Windows NT允许你建立多个同时工作的桌面。然而对于当前用户仅有一个桌面是可视的。例如,运行在不同桌面上的屏幕保护。你可以使用API函数来建立新的桌面并可以在它们之间转换—CreateDesktop()和 SwitchDesktop()是其中的两个。Windows9x仅支持一个桌面。
是否有一种方法能感觉到Shell重启动,如果是,严重依靠托盘图标的应用程序就能够简单地执行一段代码编程地恢复它们。高兴地是Internet客户端SDK提供了这个答案:每次Shell重启动或重建任务条时,他都注册和广播一个消息TaskbarCreated。
因而任何监听这个消息的应用都可以恢复图标或做些可能需要响应Shell重启动的处理。这段代码的要求是直接的:程序必须注册相同的消息,存储注册返回的值,这个值保证在整个系统及会话中是有效并唯一的。
UINT g_uShellRestart;
g_uShellRestart = RegisterWindowMessage(__TEXT("TaskbarCreated"));
如果你注册的消息已经被另一个模块注册了,实际返回的值是已分配的值。这时两个模块都知道这个消息,并可以通过它互相通讯。
在初始化你的应用时注册这个消息是较好的选择。任何响应Shell重启的活动都应在窗口过程中编码:
if(uiMsg == g_uShellRestart)
{
...
}
注意,这个特征仅仅适用于Shell4.71以上版。我们认为这个消息间接地证实了(由于所提供的工作环境)引起托盘图标消失的Bug在Shell重启动上,这是我们早先提到过的。
重启动Windows Shell
这一节的前面我们说明了一个函数可以从你的程序里调用,重新启动Windows Shell。然而,我们也可以遵循下面几步手动做这个操作:
按住Ctrl-Alt-Del键
从任务管理器中选择探测器,然后杀掉它
在典型的关闭窗口出现时,取消这个操作
几秒钟以后,系统将警告你探测器没有响应—杀掉这个任务
再过几秒钟,最后Shell重新启动,并具有一个新的任务条
知道怎样手动和编程重启Shell是一个重要的成果,在开发Shell扩展时,这有时是唯一卸载一些模块的方法。此时才可以重新编译这些模块。
任务条的布局
我们在第2章中说过,任务条的布局随Shell版本的提升而改变。其主窗口仍然是Shell_TrayWnd,也仍然有一个‘开始’按钮和TrayNotifyWnd子窗口,但是其差别在于显示活动任务的tab控件窗口包含了一个酷条(coolbar)窗口。这个窗口与一定数量的工具条窗口共享了可用的空间。
上图展示了这个新布局。使用任务条的关联菜单,工具条1到n可以加到这个酷条上,这个菜单可以通过右击任务条导出。
窗口进入任务条
Windows的任务条实际是一个具有特定TCS_BUTTONS风格的tab控件,其每一个页面都有类按钮的外观。你在任务条中所看到的根本不是按钮,只是一个SysTabControl32窗口页面(SysTabControl32是一个tab控件的正式类名)。绝对精确地说,它并不是任务条直接拥有的—有一个MSTaskSwWClass窗口在中间衔接。这个信息可以很容易地通过Spy++ 验证。
默认情况下,tab控件没有内容—需要你的代码充填它。这个控件仅限于向它的父窗口通知选择改变的消息。此时的任务条中,tabs显示某些顶层窗口的图标和标题。
显示在任务条上的并不是一定时间上运行的所有进程的列表。要获得所有运行进程的信息,你不应当依靠任务条,而应该使用任务管理器。或者求助于VC++中的进程观察工具。在15章中我们将给出类似于进程观察工具的Shell扩展代码的例子。
不是所有进程都有任务条上的窗口,也就是说,不是所有窗口都符合驻留在任务条上的条件。任务条仅仅接受:
无主的,可视窗口
特有的,具有WS_EX_APPWINDOW扩展风格的可视窗口
任务条拒绝接受的:
不可视窗口
特有的,具有WS_EX_TOOLWINDOW扩展风格的可视窗口
不可视窗口所拥有的可视窗口。
设置任务条上的可见性
在VB中,窗体可以有ShowInTaskbar属性。如果在具有这个属性的VB窗体上运行Spy++,你就会发现ShowInTaskbar的值赋给了WS_EX_APPWINDOW状态位,换言之,
Form1.ShowInTaskbar = True
意思是
DWORD dwStyle = GetWindowLong(Form1.hWnd, GWL_EXSTYLE);
dwStyle |= WS_EX_APPWINDOW;
SetWindowLong(Form1.hWnd, GWL_EXSTYLE, dwStyle);
相反,
Form1.ShowInTaskbar = False
意指
dwStyle = GetWindowLong(Form1.hWnd, GWL_EXSTYLE);
dwStyle &= ~WS_EX_APPWINDOW;
SetWindowLong(Form1.hWnd, GWL_EXSTYLE, dwStyle);
窗口的闪动
在Windows SDK中有一些函数和技术是很不起眼的,直到在某个著名的应用中使用了它们,这些不起眼的函数和技术才有了一瞬间的荣耀。在VS97和Office97引进的自绘制菜单出现时,这种情况就发生过,现在对FlashWindow()再次出现了这种情况,这个函数在‘活动配置’中用于通知重要且不可视的消息。
FlashWindow()用于设置窗口标题的活动/休眠颜色,就象手动激活/休眠它那样。在窗口以类图标方式显示在任务条上时,指定窗口呈现的按钮颜色改变。
BOOL FlashWindow(HWND hWnd, // 要闪动窗口的Handle
BOOL bInvert // 闪动状态
);
hWnd变量表示要闪动的窗口,而bInvert是TRUE表示想要转换标题颜色(从活动到休眠,或相反),如果是FALSE,则窗口标题返回到它初始的状态,活动的或休眠的。FlashWindow()函数用于通知用户其窗口在后台工作中。
这个函数是非常有用的,但是正象它表示的那样它有一个重大缺陷。FlashWindow()仅设计了闪动一次,而你实际需要重复闪动来捕获用户的注意(记住,闪动窗口不是前台窗口,所以用户可能不注意它)。用使用定时器的函数连续闪动窗口几秒钟不是一个好方法吗?实现这个功能的函数在早期的平台上是不可用的,但是在Windows98之后,FlashWindowEx()函数填补了这个空白,它使窗口在任务条上的闪动变得容易—只需调用一次函数。
BOOL FlashWindowEx(PFLASHWINFO pfwi);
FLASHWINFO结构声明如下:
typedef struct
{
UINT cbSize; // 结构尺寸,字节数
HWND hwnd; // 要闪动的窗口
DWORD dwFlags; // 闪动状态
UINT uCount; // 闪动次数
DWORD dwTimeout; // 闪动时间
} FLASHWINFO, *PFLASHWINFO;
窗口任务条
Win32 API定义了几个建立应用桌面工具条(appbars)的函数。这些对象与‘客户任务条’十分相似,并且在Office快捷方式条中有它们的正式表述。对于商业产品能够将主要功能集中表示在单一的基于桌面的窗口中是有用的,对于应用套件这尤其重要。为了帮助程序员来处理这些对象,微软定义了任务条编程接口,不幸的是,由于系统任务条与appbars不同,在这种情况下使用Word‘任务条’是乎一准会令人迷惑。
要区分任务条和appbars是有技巧的,因为系统任务条和应用桌面工具条共用SHAppBarMessage()函数:
UINT APIENTRY SHAppBarMessage(DWORD dwMessage, PAPPBARDATA pData);
然而,可安慰的是这个函数仅仅处理发送给系统任务条的两个消息,这两个消息仅简单地恢复信息。一个是ABM_GETSTATE,它告诉我们当前Windows任务条是‘自动隐藏’的,还是‘总是置顶’的。另一个是ABM_GETTASKBARPOS,它恢复任务条的边界区域和泊靠边信息。在第7章子类化‘开始’按钮时我们使用了这个特征。
没有任何通过SHAppBarMessage()产生的其它消息可以使系统任务条做出处理操作。
编程获取任务条状态
让我们实际地看一下怎样编程读出系统任务条的状态。就象上面提示的,为了知道任务条是‘置顶’的还是‘自动隐藏’的,我们需要调用SHAppBarMessage()函数并指定ABM_GETSTATE作为dwMessage变量。返回值是下面常量的组合:
ABS_ALWAYSONTOP
ABS_AUTOHIDE
要调用SHAppBarMessage()函数,我们需要了解APPBARDATA结构,它的声明如下:
typedef struct _AppBarData
{
DWORD cbSize;
HWND hWnd;
UINT uCallbackMessage;
UINT uEdge;
RECT rc;
LPARAM lParam;
} APPBARDATA, *PAPPBARDATA;
事实上,在读取任务条的自动隐藏状态时,这个结构并不是非常重要的,下面的代码段说明了这一点:
APPBARDATA abd;
ZeroMemory(&abd, sizeof(APPBARDATA));
abd.cbSize = sizeof(APPBARDATA);
rc = SHAppBarMessage(ABM_GETSTATE, &abd);
if(rc & ABS_ALWAYSONTOP)
{
lstrcat(szText, __TEXT("always on top"));
}
if(rc & ABS_AUTOHIDE)
{
lstrcat( szText, __TEXT("autohide"));
}
要取得任务条当前的泊靠边和占有的区域,需要使用ABM_GETTASKBARPOS消息。此时,SHAppBarMessage()才用有效信息填充APPBARDATA结构:
APPBARDATA abd;
ZeroMemory(&abd, sizeof(APPBARDATA));
abd.cbSize = sizeof(APPBARDATA);
SHAppBarMessage(ABM_GETTASKBARPOS, &abd);
switch(abd.uEdge)
{
case ABE_BOTTOM:
lstrcat(szText, __TEXT("aligned at the bottom"));
break;
case ABE_TOP:
lstrcat(szText, __TEXT("aligned at the top"));
break;
case ABE_LEFT:
lstrcat(szText, __TEXT("aligned on the left"));
break;
case ABE_RIGHT:
lstrcat(szText, __TEXT("aligned on the right"));
break;
}
uEdge成员包含了表示任务条当前泊靠屏幕边缘的常量,rc成员含有任务条矩形的坐标。Shell工作区—即,屏幕减去所有泊靠的任务条和appbars—可以通过SystemParametersInfo()函数指定SPI_GETWORKAREA标志获得。我们也可以获得涉及到‘任务条属性’对话框的设置信息:时钟。为了确定时钟是否被显示,你必须取得它的窗口Handle,并检查它的WS_VISIBLE标志。
// 取得任务条窗口handle
hwndTaskbar = FindWindow(__TEXT("Shell_TrayWnd"), NULL);
// 取得托盘窗口Handle
hwndTray = FindWindowEx(hwndTaskbar, NULL, __TEXT("TrayNotifyWnd"), NULL);
// 取得时钟窗口handle
hwndClock = FindWindowEx(hwndTray, NULL, __TEXT("TrayClockWClass"), NULL);
if(hwndClock)
{
if(IsWindowVisible(hwndClock))
lstrcat(szText, __TEXT("clock visible"));
else
lstrcat(szText, __TEXT("clock not visible"));
}
在这一章余下的部分中,我们给出一个总的程序,集中实现我们所讨论过的知识。我们还是建立一个基于对话框的应用Taskbar,它读出任务条的状态,感知Shell重启等,下面是它的用户界面:
‘恢复’按钮执行不同的代码段并显示任务条的位置,‘自动隐藏’/ ‘总是置顶’状态,和时钟设置。‘重启Shell’则引起Shell重新启动。
首先,如果我们希望感觉到Shell重启,则需要在WinMain()中注册TaskbarCreated消息:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevious,LPTSTR lpsz, int iCmd)
{
// 暂时省略的代码
g_uShellRestart = RegisterWindowMessage(__TEXT("TaskbarCreated"));
// 运行住对话框
BOOL b = DialogBox(hInstance, "DLG_MAIN", NULL, APP_DlgProc);
// 退出
DestroyIcon(g_hIconLarge);
DestroyIcon(g_hIconSmall);
return b;
}
这里g_uShellRestart是UINT型全程变量。其次,加入代码到APP_DlgProc()函数处理‘恢复’和‘重启’按钮,和测试Shell重启。
BOOL CALLBACK APP_DlgProc(HWND hDlg, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
switch(uiMsg)
{
case WM_INITDIALOG:
OnInitDialog(hDlg);
break;
case WM_COMMAND:
switch(wParam)
{
case IDC_RETRIEVE:
OnTaskbarSettings(hDlg);
return FALSE;
case IDC_RESTART:
SHShellRestart();
return FALSE;
case IDCANCEL:
EndDialog(hDlg, FALSE);
return FALSE;
}
break;
}
// 当Shell重启时 ...
if(uiMsg == g_uShellRestart)
{
TCHAR szTime[50] = {0};
TCHAR szMsg[MAX_PATH] = {0};
GetTimeFormat(LOCALE_SYSTEM_DEFAULT, 0, NULL, NULL, szTime, 50);
wsprintf(szMsg, __TEXT("The shell was last restarted at %s"), szTime);
SetDlgItemText(hDlg, IDC_TASKBAR, szMsg);
}
return FALSE;
}
如果‘重启’按钮被按下,则调用SHShellRestart()函数(我们前面定义的),并且发生TaskbarCreated消息来实现这个条件。当‘恢复’按钮按下时,唤醒OnTaskbarSettings()函数:
void OnTaskbarSettings(HWND hDlg)
{
TCHAR szText[MAX_PATH] = {0};
APPBARDATA abd;
abd.cbSize = sizeof(APPBARDATA);
// 恢复任务条泊靠的边缘
SHAppBarMessage(ABM_GETTASKBARPOS, &abd);
switch(abd.uEdge)
{
case ABE_BOTTOM:
lstrcat(szText, __TEXT("aligned at the bottom/r/n"));
break;
case ABE_TOP:
lstrcat(szText, __TEXT("aligned at the top/r/n"));
break;
case ABE_LEFT:
lstrcat(szText, __TEXT("aligned on the left/r/n"));
break;
case ABE_RIGHT:
lstrcat(szText, __TEXT("aligned on the right/r/n"));
break;
}
// 恢复任务条的状态
DWORD rc = SHAppBarMessage(ABM_GETSTATE, &abd);
if(rc & ABS_ALWAYSONTOP)
lstrcat(szText, __TEXT("always on top/r/n"));
if(rc & ABS_AUTOHIDE)
lstrcat(szText, __TEXT("autohide/r/n"));
// 恢复时钟显示选择
HWND hwnd1 = FindWindow(__TEXT("Shell_TrayWnd"), NULL);
HWND hwnd2 = FindWindowEx(hwnd1, NULL, __TEXT("TrayNotifyWnd"), NULL);
HWND hwndClock = FindWindowEx(hwnd2, NULL, __TEXT("TrayClockWClass"), NULL);
if(hwndClock)
{
if(IsWindowVisible(hwndClock))
lstrcat(szText, __TEXT("clock visible/r/n"));
else
lstrcat(szText, __TEXT("clock not visible/r/n"));
}
// 显示设置
SetDlgItemText(hDlg, IDC_TEXT, szText);
}
使用这个函数的地方要#include resource.h,你现在可以编译和执行这个应用,获得如下的结果:
系统任务条的其它设置,如‘自动隐藏’,显然不能编程设置。如果能这样做,其方法也是完全没有说明资料的。
隐藏任务条
我们前面说过,任务条是一个属于Shell进程的普通窗口。可以向系统中的其它窗口那样子类化和隐藏这个窗口。我们在第7章中讨论过进程内子类化,并且说明浏览器辅助对象和SHLoadInProc()怎样使你的代码进入Shell地址空间。如果你试着子类化任务条而没有首先把代码注入到Shell进程,你是不能成功的。之所以这样,不是因为你不能子类化任务条或‘开始’按钮(或任何其它的系统窗口),而是因为你没有映射代码到Shell关联空间中。子类化任务条并不比子类化‘开始’按钮困难(见第7章)。但是,有一些关于任务条的操作可以简单地通过使用窗口handle来完成。一般,你可以通过已知的HWND安全地发送消息到另一个进程的窗口,而不需要使用指针。我们来看一个例子。
我们已经说明怎样使用FindWindow()来获得任务条的Handle。只要我们得到了这个Handle,隐藏任务条就是简单地调用正确的函数:
void SHHideTaskbar(BOOL fHide)
{
HWND hwndTaskbar = FindWindow(__TEXT("Shell_TrayWnd"), NULL);
ShowWindow(hwndTaskbar, (fHide ? SW_HIDE : SW_SHOW));
}
SHHideTaskbar()函数根据它接收的逻辑值隐藏或恢复任务条窗口。注意,无论任务条属于哪一个进程,这段代码都正常工作。
ITaskbarList接口
在版本4.71以后,有一个新的相关COM接口出现,其名字是ITaskbarList。这不是一个要求我们在自己的应用程序中应该实现的接口(事实上它是由Shell实现的),但是是一个简单的编程修改系统任务条的接口。
关于ITaskbarList有两点需要注意,首先,对这个接口存在文档资料说明,但是不够清晰,其次,缺少包含这个接口定义的头文件,也就是说,如果你想要使用这个接口,就必须自己写它的定义。
ItaskbarList的承诺
在一个封装下,ITaskbarList给出一种轻微修改Windows9x任务条部件内容的方法:任务列表。通过ITaskbarList,你可以在任务条上添加新的客户按钮,和删除它们。这个接口的方法描述如下:
方法 | 描述 |
ActivateTab() | 资料上说,“激活一个任务条上的项。窗口实际是不活动的;任务条上的窗口项仅仅被显示为活动”我们没能重生成这个行为。 |
AddTab() | 添加一个新的任务条tab。这个函数要求一个HWND,更适宜的是具有WS_CAPTION风格的HWND,以避免空tabs。 |
DeleteTab() | 删除一个由AddTab()添加的tab。这个操作不影响相关的窗口。 |
HrInit() | 初始化你所建立的保持tabs踪迹的内部结构。这个方法只需在任何其它方法之前调用一次。 |
SetActiveAlt() | 资料上说“标志一个任务条项为活动但是视觉上并没有激活它”。我们没能重生这个行为。 |
ITaskbarList接口的IDL定义
最后这个shlguid.h文件定义一个CLSID和IID,但是ITaskbarList接口的形式定义(作为实现的基础)到处都没有找到。在给出这个定义之前有两个选择:我们可以用一段代码给出接口的定义,或更永久一点,使用IDL文件形式。用MIDL编译器我们将获得一个有用的头文件。
// 任务条.idl
import "oaidl.idl";
import "oleidl.idl";
//--------------------------------------------------------------------------
// 接口: ITaskbarList
//--------------------------------------------------------------------------
[
local,
object,
uuid(56FDF342-FD6D-11d0-958A-006097C9A090),
pointer_default(unique)
]
interface ITaskbarList : IUnknown
{
HRESULT ActivateTab([in] HWND hWnd);
HRESULT AddTab([in] HWND hWnd);
HRESULT DeleteTab([in] HWND hWnd);
HRESULT HrInit();
HRESULT SetActiveAlt([in] HWND hWnd);
};
把这个文件加到我们的工程(project)中并补充设置生成头文件ITaskbarList.h。用这个头文件,我们就可以考虑开始测试一些代码了:
#include <shlguid.h>
void OnAddTab(HWND hWnd)
{
ITaskbarList* pTList = NULL;
CoInitialize(NULL);
CoCreateInstance(CLSID_TaskbarList, NULL, CLSCTX_SERVER,
IID_ITaskbarList, reinterpret_cast<void**>(&pTList));
pTList->AddTab(hWnd);
pTList->Release();
CoUninitialize();
}
这明显是我们需要向任务条添加tab的最小代码(我们将在示例程序中使用这段程序的扩展版本)。资料说明中推荐使用至少WS_CAPTION风格的窗口,但是任何可用的窗口,可视或不可视都能被接受。
前面我们说任务条拒绝不可视窗口。这里写的代码怎么适合了呢?很简单,ITaskbarList是一个底层接口,使你能安排任务条的tabs。所有支配任务条建立新按钮的逻辑都构筑在ITaskbarList之上,例如窗口和tabs只存在于建立,激活和删除操作中—它们根本不知道任务条的‘商业规则’。
为了更好地理解ITaskbarList扮演的角色,让我们看一下怎样使用它。
ITaskbarList示例程序
为了节省工作量,我们决定扩展上面开发的示例程序,添加两个新按钮到主对话框上—标签为‘添加Tab’和‘删除Tab’,标识符分别为IDC_ADDTAB和IDC_DELETETAB。
现在我们需要做的工作就简单了,首先,我们需要修改APP_DlgProc()来处理这两个按钮:
case WM_COMMAND:
switch(wParam)
{
case IDC_ADDTAB:
OnAddTab(hDlg);
return FALSE;
case IDC_DELETETAB:
OnDeleteTab();
return FALSE;
case IDC_RETRIEVE:
OnTaskbarSettings(hDlg);
return FALSE;
case IDC_RESTART:
SHShellRestart();
return FALSE;
case IDCANCEL:
EndDialog(hDlg, FALSE);
return FALSE;
}
break;
然后是两个实现加删新tab的新消息处理器。在OnAddTab()第一次调用时,它建立一个隐藏窗口(我们任意选择了一个按钮),由一个tab表示。
void OnAddTab(HWND hWnd)
{
static BOOL bFirstTime = TRUE;
ITaskbarList* pTList = NULL;
HRESULT hr = CoCreateInstance(CLSID_TaskbarList, NULL, CLSCTX_SERVER,
IID_ITaskbarList, reinterpret_cast<void**>(&pTList));
if(FAILED(hr))
return;
// 仅头一次调用
if(bFirstTime)
{
bFirstTime = FALSE;
pTList->HrInit();
// 建立一个新的按钮窗口(尽管任何窗口类都可以)
g_hwndButton = CreateWindow(__TEXT("Button"), __TEXT("Custom button..."),
WS_CAPTION | WS_SYSMENU | WS_VISIBLE,
-300, -300, 50, 50, hWnd, NULL, NULL, NULL);
}
pTList->AddTab(g_hwndButton);
pTList->Release();
ShowWindow(g_hwndButton, SW_HIDE);
}
void OnDeleteTab()
{
ITaskbarList* pTList = NULL;
HRESULT hr = CoCreateInstance(CLSID_TaskbarList, NULL, CLSCTX_SERVER,
IID_ITaskbarList, reinterpret_cast<void**>(&pTList));
if(FAILED(hr))
return;
pTList->DeleteTab(g_hwndButton);
pTList->Release();
}
要使这段代码工作,你还需要添加一个类型为HWND的全程变量来保持新窗口的Handle。COM库也应该在WinMain()中被初始化(终止化),例如:
// 运行主对话框
CoInitialize(NULL);
BOOL b = DialogBox(hInstance, "DLG_MAIN", NULL, APP_DlgProc);
// 退出
OnDeleteTab();
CoUninitialize();
DestroyWindow(g_hwndButton);
DestroyIcon(g_hIconLarge);
DestroyIcon(g_hIconSmall);
return b;
}
最后,需要#include ITaskbarList.h和shlguid.h,以及连接ole32.lib。在这里我们将能获得下面截图显示的行为。新按钮可以被添加和删除tab,但是它们是无生命的,你可以不断地操作,然而没有任何更多的事情发生。
任务条-窗口通讯
如果任务条按钮是一个真实的按钮,任何相关的事件都相当容易截取,可悲的是任务条的按钮实际上是一个页面tab控件,这就使事情变得相当困难。受此问题的困扰,我们自己也确实感到奇怪,为什么ITaskbarList::AddTab()推荐传递的窗口要有WS_CAPTION风格。是不是有可能把任务条按钮当作窗口的标题呢。为了找出答案,我们子类化一个窗口,并把它传递给AddTab(),就我们所讨论的问题,可以预感到这是正确的。
某些相对于标题活跃的消息被寄送到这个按钮表示的窗口,也就是说,在点击对应任务条按钮时,通过AddTab()传递的窗口接收WM_ACTIVATE消息(和其它非客户域相关消息)。
我们可以象下面那样子类化窗口(g_pfnOldProc是一个类型为WNDPROC的全程变量):
void OnAddTab(HWND hWnd)
{
static BOOL bFirstTime = TRUE;
ITaskbarList* pTList = NULL;
HRESULT hr = CoCreateInstance(CLSID_TaskbarList, NULL, CLSCTX_SERVER,
IID_ITaskbarList, reinterpret_cast<void**>(&pTList));
if(FAILED(hr))
return;
// 仅调用一次
if(bFirstTime)
{
bFirstTime = FALSE;
pTList->HrInit();
// 建立新的按钮窗口(虽然可以是任何窗口类)
g_hwndButton = CreateWindow(__TEXT("Button"), __TEXT("Custom button..."),
WS_CAPTION | WS_SYSMENU | WS_VISIBLE,
-300, -300, 50, 50, hWnd, NULL, NULL, NULL);
g_pfnOldProc = SubclassWindow(g_hwndButton, ButtonProc);
}
pTList->AddTab(g_hwndButton);
pTList->Release();
ShowWindow(g_hwndButton, SW_HIDE);
}
在我们讨论子类化按钮可能引发的事情之前,应该提一下传递给CreateWindow()的变量是作为某种非常陌生行为的实验结果而选择的。如果传递给AddTab()的窗口有一个标题和WS_SYSMENU风格,则任务条按钮显示图标。然而,如果窗口也是可视的,将会引起下面这些问题:
应用的主窗口失去焦点,而且没有办法恢复
在右击任务条按钮时,应用的系统菜单总也不能正常显示
在可视窗口被加入任务条时发生的另一件事情是这个新按钮是非选中的,这更象是我们想要的。为此,我们初始把窗口放到屏幕之外,然后在新按钮加入之后,我们使用ShowWindow()将它‘适当地’隐藏。
设置菜单
现在我们已经知道了任务条和窗口之间通讯过程是怎样工作的,设置和显示弹出菜单就相当容易了。下面是用于子类化与新任务条按钮关联的窗口(‘Button’类)的过程代码。
LRESULT CALLBACK ButtonProc(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
switch(uiMsg)
{
case WM_ACTIVATE:
if(LOWORD(wParam) == TRUE)
OnButtonActivation();
}
return CallWindowProc(g_pfnOldProc, hwnd, uiMsg, wParam, lParam);
}
void OnButtonActivation()
{
// 取得tab控件的Handle
HWND h0 = FindWindow(__TEXT("Shell_TrayWnd"), NULL);
HWND h1 = FindWindowEx(h0, NULL, __TEXT("RebarWindow32"), NULL);
HWND h2 = FindWindowEx(h1, NULL, __TEXT("MSTaskSwWClass"), NULL);
HWND h3 = FindWindowEx(h2, NULL, __TEXT("SysTabControl32"), NULL);
// 建立新的弹出菜单
HMENU hmenu = CreatePopupMenu();
// 获得tab控件当前选中的按钮
int i = TabCtrl_GetCurSel(h3);
// 如果没有tab被选中显示只有一个基本项‘关闭’的菜单
if(i == -1)
AppendMenu(hmenu, MF_STRING, IDC_DELETETAB, __TEXT("&Close"));
else
{
AppendMenu(hmenu, MF_STRING, IDC_RESTART, __TEXT("&Restart the shell"));
AppendMenu(hmenu, MF_STRING, IDC_RETRIEVE,
__TEXT("Re&trieve Taskbar Settings"));
AppendMenu(hmenu, MF_SEPARATOR, 0, NULL);
AppendMenu(hmenu, MF_STRING, IDC_DELETETAB, __TEXT("&Delete Me"));
}
// 找出菜单位置,它靠在任务条的边缘上
STARTMENUPOS smp;
if(i == -1)
{
POINT pt;
GetCursorPos(&pt);
smp.ix = pt.x;
smp.iy = pt.y;
smp.uFlags = TPM_BOTTOMALIGN;
}else
GetMenuPosition(h3, i, &smp);
// 显示,然后销毁菜单
TrackPopupMenu(hmenu, smp.uFlags, smp.ix, smp.iy, 0, g_hDlg, 0);
DestroyMenu(hmenu);
}
如果这个按钮活动时不再有当前选中的项,显示一个不同的菜单。正如所见,菜单项作为主程序的其它控件给定了同样的标识符,所以你可以从关联菜单以及主对话框重启动Shell或恢复任务条的设置。要使调用TrackPopupMenu()正常工作,我们需要一个HWND类型的全程变量,你可以在OnInitDialog()中把它设置为住对话框的Handle。
确定菜单位置
这个应用的最后一部分关系到弹出菜单的位置(这是上面代码中GetStartMenuPosition()函数的功能),最终依赖于任务条泊靠的边,和任务条按钮的相对位置。实际确定正确位置的算法十分类似于第7章中‘开始’菜单的方法—依靠ABM_TASKBARPOS消息确定任务条的泊靠边。然而,在这里还有另一个问题:x-坐标,在‘开始’菜单中总是0,现在它依赖于按钮的位置。
struct STARTMENUPOS
{
int ix;
int iy;
UINT uFlags;
};
typedef STARTMENUPOS* LPSTARTMENUPOS;
int GetMenuPosition(HWND hwndTab, int iItem, LPSTARTMENUPOS lpsmp)
{
// 设置和重置尺寸以获得按钮的当前宽度和高度
long iItemSize = TabCtrl_SetItemSize(hwndTab, 0, 0);
TabCtrl_SetItemSize(hwndTab, LOWORD(iItemSize), HIWORD(iItemSize));
// 取得tab控件的矩形
RECT r;
GetWindowRect(hwndTab, &r);
// 恢复任务条的泊靠边
APPBARDATA abd;
abd.cbSize = sizeof(APPBARDATA);
SHAppBarMessage(ABM_GETTASKBARPOS, &abd);
switch(abd.uEdge)
{
case ABE_BOTTOM:
lpsmp->ix = r.left + LOWORD(iItemSize) * iItem + 3;
lpsmp->iy = abd.rc.top;
lpsmp->uFlags = TPM_LEFTALIGN | TPM_BOTTOMALIGN;
break;
case ABE_TOP:
lpsmp->ix = r.left + LOWORD(iItemSize) * iItem + 3;
lpsmp->iy = abd.rc.bottom;
lpsmp->uFlags = TPM_LEFTALIGN | TPM_TOPALIGN;
break;
case ABE_LEFT:
lpsmp->ix = abd.rc.right;
lpsmp->iy = r.top + HIWORD(iItemSize) * iItem + 3;
lpsmp->uFlags = TPM_LEFTALIGN | TPM_TOPALIGN;
break;
case ABE_RIGHT:
lpsmp->ix = abd.rc.left;
lpsmp->iy = r.top + HIWORD(iItemSize) * iItem + 3;
lpsmp->uFlags = TPM_RIGHTALIGN | TPM_TOPALIGN;
break;
}
return 1;
}
在这些计算中,x-坐标由tab控件左边缘加上按钮宽度的偏移给出:
lpsmp->ix = r.left + LOWORD(iItemSize) * iItem + 3;
项目尺寸对于所有项都是一样的,使用一个技巧获得。在设置新尺寸时,当前尺寸被返回,所以我们能够通过设置然后立即恢复尺寸取得宽度和高度。宽度和高度存储在一个Long型之中,低字为宽度。
因为调用tab控件的代码是另一个进程的一部分,因此我们不能使用TabCtrl_GetItemRect()。反之,窗口是全程对象,任何进程都可访问,如果不使用指针,这个函数可以正常工作,消息也可以发送。不幸地是,TabCtrl_GetItemRect()要求返回实际矩形的缓冲。
如果任务条垂直泊靠,可能改变的坐标应该是y:
lpsmp->iy = r.top + HIWORD(iItemSize) * iItem + 3;
为了证实它正常工作,下面的截屏显示了任务条靠右边泊靠的情形:
小结
在这一章中我们从查看图标开始,最后讨论了一个用于任务条操作的,新的,没有资料说明的COM接口。沿着这条线索,我们讨论了许多Windows图标的课题。尤其是把图标放入托盘通知区的函数。
处理托盘图标的代码在Shell重启动时有一些问题,而微软没有解决这些问题,最近才引进了一个可以通知应用Shell即将重启的消息。这就相当于默认了这个错误。但是,真实地是所有存在的应用都将受到影响。现在看来,我们这里给出的是最好的方法。
我们还试图澄清关于系统任务条和应用桌面工具条(appbars)的某些观念。最后我们在实践中使用了ITaskbarList接口来修改任务条的内容。概括地讲,在这一章中我们讨论了:
概览了Win32下的图标
进一步细述了托盘图标的编程
描述了通知Shell重启的半公开的消息
比较了任务条和appbars
报道了使用ITaskbarList接口的真实体验