Windows 2000 UI 新特点之四:其他类型的外壳扩展
编译/赵湘宁
第一部分
Windows 2000 UI 新特点之一:信息条提示(Infotip)
第二部分
Windows 2000 UI 新特点之二:自定义文件夹栏目
第三部分

2000 UI 新特点系列文章的第四篇,探讨几个其它的外壳扩展类型,如外壳执行、图标覆盖、磁盘清理管理程序、使用属性页的文件夹定制、以及上下文菜单。
Windows 2000 还有三个新的外壳扩展:外壳执行管理器(shell execution handler)、图标覆盖管理器(icon
overlay handler)和清理管理器(cleanup handler)。
所谓“外壳执行”,是提供 IShellExecuteHook 接口的一个模块,这个接口导致自己的代码在某个命令行被外壳经由资源管理器或"运行"对话框处理之前被调用。IShellExecuteHook 接口与WH_SHELL没有什么关系,因为您的代码的执行先于目标程序的启动,而且都是在外壳的地址空间内工作。
Windows 2000是第一个全面支持图标覆盖的Windows 版本。有关的接口有两个:IShellIconOverlay
和 IShellIconOverlayIdentifier。前者是为显示覆盖的名字空间扩展保留的。后者是允许您定义自制图像用于文件夹图标覆盖的主要接口。
图标覆盖是某种条件下外壳自动在图标左下角绘制的代表文件夹项目的一个小图像。典型的例子是快捷方式图标坐下角的小箭头,以及共享文件夹图标左下角的小手图标。这些用户看到的图标是结合在一起的两个重叠图标。这种机制在Windows
2000中被开放,已不是什么秘密。
当绘制一个文件夹图标时,资源管理器试图从代表特定文件夹类型的名字空间获得一个IShellIconOverlay指针。如果这个接口存在,名字空间便有机会使用定制的项目。Windows
2000 的平台SDK文档中没有关于IShellIconOverlay 和IShellIconOverlayIdentifier接口的信息,但这两个接口实际上自从Windows
9x 和Windows NT 4.0的桌面更新引入一来就已经存在了。如果您在Windows 9x 和Windows NT 4.0 或更早的
OS 版本上编写代码,请参考 Q192055 文档包含的一些有用的技巧。

Windows 98发布之初,微软制作了一个叫"磁盘清理(Disk Cleanup)"的实用程序挂在操作系统中(参见图十)。这个工具的目的是通过删除、压缩或备份无用的文件来释放磁盘空间。为此,"磁盘清理"的实用程序清除几个标准的文件夹中的内容,如:垃圾箱、下载的程序文件、临时的Internet 文件。

图十 磁盘清理程序
通过写一个磁盘清理扩展,可以将新的入口增加到图十显示的对话框中以便管理特定的自制应用程序文件集。"磁盘清理"有一个模块化的结构并由一些系统级的处理器构成,您可以编写并注册自己的清理扩展。每一个扩展实现几个COM接口来与"磁盘清理"管理器通讯。
编写清理扩展必须要创建一个暴露IEmptyVolumeCache2接口的COM对象。Windows
98 和Windows 2000中的清理扩展有一些细微的差别。在Windows 98中必须提供IEmptyVolumeCache接口,而在Windows
2000中还必须提供IEmptyVolumeCache2接口。IEmptyVolumeCache2接口是IEmptyVolumeCache接口的超集并加入了InitializeEx方法。
下面是本文提供的一个很基本的清理扩展实现:
Cleanup Extension
// IEmptyVolumeCache2Impl.h
#include <AtlCom.h>
#include <emptyvc.h>
class ATL_NO_VTABLE IEmptyVolumeCache2Impl : public IEmptyVolumeCache2{
public:
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) =
0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IEmptyVolumeCache2Impl)
// IEmptyVolumeCache::Initialize
STDMETHOD(Initialize)(HKEY hkRegKey, LPCWSTR pcwszVolume,
LPWSTR *ppwszDisplayName,
LPWSTR *ppwszDescription,
DWORD *pdwFlags){
// Allows to initialize a Windows 98 handler
MessageBox(0, _T("Initialize"), 0, 0);
return S_OK;
}
// IEmptyVolumeCache::Deactivate
STDMETHOD(Deactivate)(DWORD *pdwFlags){
// Called when the handler is going to be unloaded
MessageBox(0, _T("Deactivate"), 0, 0);
return S_OK;
}
// IEmptyVolumeCache::GetSpaceUsed
STDMETHOD(GetSpaceUsed)(DWORDLONG *pdwSpaceUsed,
IEmptyVolumeCacheCallBack *picb){
// Returns the amount of space the handler can free
MessageBox(0, _T("GetSpaceUsed"), 0, 0);
return S_OK;
}
// IEmptyVolumeCache::Purge
STDMETHOD(Purge)(DWORDLONG dwSpaceToFree,
IEmptyVolumeCacheCallBack *picb){
// Actually deletes the files
MessageBox(0, _T("Purge"), 0, 0);
return S_OK;
}
// IEmptyVolumeCache::ShowProperties
STDMETHOD(ShowProperties)(HWND hwnd){
// Provides a UI
MessageBox(0, _T("ShowProperties"), 0, 0);
return S_OK;
}
// IEmptyVolumeCache2::InitializeEx
STDMETHOD(InitializeEx)(HKEY hkRegKey, LPCWSTR pcwszVolume,
LPCWSTR pcwszKeyName,
LPWSTR *ppwszDisplayName,
LPWSTR *ppwszDescription,
LPWSTR *ppwszBtnText,
DWORD *pdwFlags){
// Initializes the handler under Windows 2000
MessageBox(0, _T("InitializeEx"), 0, 0);
return S_OK;
}
};
// CleanSomething.h : Declaration of the CCleanSomething
#ifndef __CLEANSOMETHING_H_
#define __CLEANSOMETHING_H_
#include "resource.h" // main symbols
#include "IEmptyVolumeCache2Impl.h" // IEmptyVolumeCache2
/////////////////////////////////////////////////////////////////
// CCleanSomething
class ATL_NO_VTABLE CCleanSomething :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CCleanSomething, &CLSID_CleanSomething>,
public IEmptyVolumeCache2Impl,
public IDispatchImpl<ICleanSomething, &IID_ICleanSomething,
&LIBID_DCHDEMOLib>{
public:
CCleanSomething(){}
DECLARE_REGISTRY_RESOURCEID(IDR_CLEANSOMETHING)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CCleanSomething)
COM_INTERFACE_ENTRY(IEmptyVolumeCache)
COM_INTERFACE_ENTRY(IEmptyVolumeCache2)
COM_INTERFACE_ENTRY(ICleanSomething)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
// IEmptyVolumeCache2
public:
STDMETHOD(InitializeEx)(HKEY hkRegKey, LPCWSTR pcwszVolume,
LPCWSTR pcwszKeyName,
LPWSTR *ppwszDisplayName,
LPWSTR *ppwszDescription,
LPWSTR *ppwszBtnText,
DWORD *pdwFlags);
STDMETHOD(GetSpaceUsed)(DWORDLONG *pdwSpaceUsed,
IEmptyVolumeCacheCallBack *picb);
};
#endif //__CLEANSOMETHING_H_
// CleanSomething.cpp : Implementation of CCleanSomething
#include "stdafx.h"
#include "DCHDemo.h"
#include "CleanSomething.h"
// CCleanSomething
HRESULT CCleanSomething::InitializeEx(HKEY hkRegKey,
LPCWSTR pcwszVolume, LPCWSTR pcwszKeyName,
LPWSTR *ppwszDisplayName, LPWSTR *ppwszDescription,
LPWSTR *ppwszBtnText, DWORD *pdwFlags){
USES_CONVERSION;
MessageBoxW(0, pcwszVolume, 0, 0);
*ppwszDisplayName = (LPWSTR)CoTaskMemAlloc(100);
*ppwszDescription = (LPWSTR)CoTaskMemAlloc(100);
*ppwszBtnText = (LPWSTR)CoTaskMemAlloc(100);
lstrcpyW(*ppwszDisplayName, T2OLE("DCHDemo"));
lstrcpyW(*ppwszDescription, T2OLE("Clean Something"));
lstrcpyW(*ppwszBtnText, T2OLE("Click Me!"));
*pdwFlags |= EVCF_HASSETTINGS;
return S_OK;
}
HRESULT CCleanSomething::GetSpaceUsed(DWORDLONG *pdwSpaceUsed,
IEmptyVolumeCacheCallBack *picb){
*pdwSpaceUsed = 1024000;
return S_OK;
}
它能释放1MB的磁盘空间。标准实现提供了一个消息框帮助理解不同的方法是如何被调用以及以什么顺序被调用的。注意在当前的MSDN文档中关于Deactivate的原型定义有一个错误,位于emptyvc.h头文件中,正确的原型应该是:
STDMETHOD(Deactivate)(DWORD *pdwFlags)
文档中的参数应该是DWORD。Windows 2000专用的InitializeEx方法是用来本地化扩展的。Windows 98里,按钮文本,描述和显示的处理器名字都是从注册表里读出来的,而在Windows
2000里,扩展代码本身就决定了这些信息。清除处理器能提供一个对话框来显示可删除文件的预览。ShowProperties方法负责打开窗口。为了让定制扩展工作,应该在适当的位置提供一个按钮并在InitializeEx中设置EVCF_HASSETTINGS标志。

桌面更新引入的一个特性是定制文件夹的图标,在Windows 9x 和Windows NT 4.0中已经有了这样的特性。Windows 2000解决了一些小文件夹图标中存在的矛盾,使用Windows
9x中的桌面更新也许会碰上这些问题。特别是现在定制图标显示在资源管理器的左右两个窗格里,而且紧挨着被"打开/保存"对话框使用的外壳视图对象。您可以通过资源管理器右窗格中的工具条提示或标签为任何在外壳显示的文件夹指定一个描述。
对这些用户界面增强的关键是一个名字为desktop.ini的文本文件。将一个文件取名为desktop.ini并放入一个文件夹,将文件夹设为只读,外壳将自动赋予这个desktop.ini文件一个特别的意义。如果文件夹未被设置为只读,文件desktop.ini将被外壳认为是一个普通文件,其内容就不会被用来增强文件夹的外观。(注意在只读文件夹中仍然能拷贝和删除文件)
如果在一个文件夹内单击右键就可以启动"定制此文件夹向导"。然后,在向导中允许设置文件夹的背景图像和为内容的显示指定一个HTML模板。此外,还可以为文件夹输入一个注释。这个注释可以是在"文件夹设置(Folder
Settings)"子文件夹中以名字comment.htt存放的任何HTML文本。也就是说,可以在资源管理器的右窗个中嵌入整个HTML页面来描述一个文件夹(参见图十二)。别忘了desktop.ini和"文件夹设置"
子文件夹都是隐藏的,所以看不到它们,除非选择在"文件夹选项"对话框中选择"显示所有文件"选项。

图十二 定制的文件夹
定制向导是一个外壳提供的用来增强文件夹用户界面的交互式工具。它不支持文件夹分配定制图标和文件夹描述。解决这个问题的方法是编写一个文件夹属性页处理器。它也是一个外壳扩展。本文例子代码实现了在文件夹的"属性"对话框中插入附加的页面。新的页面有一个标签为"Customize"(参见图十三)

图十三 属性页管理器
您可以输入描述文本并选择一个图标来代替标准的文件夹位图。属性页外壳扩展需要组织安排一个对话框模板,还必须实现IShellExtInit 和IShellPropSheetExt接口。实际上,也可以通过实现两个函数来完成:Initialize
和AddPages。
Initialize 函数的工作是决定文件夹的哪一个属性页被显示,AddPages负责通过PROPSHEETPAGE 结构增加一个新的属性页。因为是处理Windows
95的一个通用控件,所以在处理属性页数据结构之前要加一个对InitCommonControls的调用。本文实现的外壳扩展源代码在当前目录查找desktop.ini文件并使用一个传统的、可靠的GetPrivateProfileString
API函数来汲取数据。desktop.ini文件中的内容如下:
[.ShellClassInfo]
Infotip=Contains all the articles I''ve written for VCKBASE.
IconFile=D:\My Pictures\ICON\Special\vckbase.ico
IconIndex=0
注释文本的显示有两种方式。一种是当鼠标指针移到资源管理器有窗格目录名上时出现一个工具条提示,另一种是当选中文件夹时,在目录下方显示注释文本。属性页外壳扩展必须在注册表的下类位置注册:
HKCR
\Folder
\Shellex
\PropertySheetHandlers
\{CLSID}
注意:{CLSID}处应该是实际创建对象的CLSID。每个文件夹允许有多个属性页处理。ATL注册组件使用的实际脚本代码如下:
HKCR
{
NoRemove Folder
{
NoRemove Shellex
{
NoRemove PropertySheetHandlers
{
ForceRemove {1F8F343A-1DE0-4B26-97C9-18A39FFC9880}
}
}
}
}
按照外壳的组织结构,文件夹是任何在整个名字空间中可获得的对象和容器。目录是一个包含文件和表示文件系统目录的特殊的文件夹。您也可以将外壳扩展只应用到目录。为此,在前面的脚本中将"Folder"替换为"Directory"即可。
为所有文件夹注册外壳扩展的同时也将定制的页面添加到了某些特殊的文件夹,如:任何驱动器的根文件夹。在desktop.ini文件中,可以将一个描述关联到驱动器根文件夹,但不能改变它的图标。改变缺省的根文件夹图标意味着改变驱动器的图标,只能强行更改系统注册表。以下是驱动器定制图标应该在注册表中存放的位置:
HKLM
\Software
\Microsoft
\Windows
\CurrentVersion
\Explorer
\DriveIcons
在"DriveIcons"键下应该创建一个子键,其名字是表示驱动器的字母。如果想要改变"D"驱动器的图标,就要创建一个子树:
...
\DriveIcons
\D
\DefaultIcon
DefaultIcon的缺省键值必须指向一个用逗号分隔的串,其第一部分十图标文件名,第二部分是图标索引。注意您还可以使用资源的ID号在一个可执行模块中区分图标。如果指定的索引是负数,外壳自动将它翻译成一个资源ID并试图找到匹配的图标。例如:
mylib.dll,-204
用资源ID 204指向图标。图十四显示的是"D"驱动器和为它定制的月亮图标。

图十四 定制驱动器盘符图标
使用彩色图标有助于快速区分文件夹,尤其在完全结构化的和大的驱动器。但另一方面,太多的定制图标反而会引起用户的混乱,并且更难正确区分文件夹。所以应该有选择地使用定制页。

"发送到"是一个特殊的文件夹,Windows 2000将它移到了一个叫"Document and Settings"的文件夹下,这个文件夹是当前用户文件夹。它包含应用程序的快捷键,这些应用程序在命令行接受选中文件夹的名字并处理之。
"发送到"菜单有两个有趣的特性:一个是在菜单项附近显示位图,另一个是打开子菜单项。这个特性已经存在很长时间,但几乎没有那个商业应用使用和开发它们。在上下文外壳扩展中,增加单个菜单项,仰或一个弹出式菜单之间没有实质性差别,不用去调用InsertMenu
和AppendMenu来传递单个项目,只要创建并插入一个弹出式子菜单就可以了。

图十五 定制上下文菜单
在文件夹的上下文菜单中还有另外一个功能就是:创建子文件夹以及从文件夹自身进入命令行,参见图十五。单击"New Folder"菜单项,会弹出一个对话框接受子文件夹名字的输入。为了创建一个新文件夹,可以利用一个很少有人知道的API:MakeSureDirectoryPathExists。它有一个路径名作为参数,顾名思义,这个API检查参数中的所有的目录是否都存在,如果不存在就创建它们。所以您可以用以下的串做参数:
one\two\three
然后用外壳扩展来创建文件夹子树。
至于命令行,要使用Comspec环境变量来获取用户的缺省命令行。如果找不到这个环境变量,那么在Windows
9x中缺省使用command.com命令,NT和Windows 2000中缺省使用cmd.exe命令。用CreateProcess可以指定初始目录。
下面是调用命令行的实现代码:调用命令行
TCHAR szCommand[MAX_PATH] = TEXT("");
STARTUPINFO sui;
PROCESS_INFORMATION pi;
OSVERSIONINFO osi;
GetEnvironmentVariable(TEXT("ComSpec"), szCommand,
ARRAYSIZE(szCommand));
if(!*szCommand){
osi.dwOSVersionInfoSize = sizeof(osi);
GetVersionEx(&osi);
if(VER_PLATFORM_WIN32_NT == osi.dwPlatformId){
lstrcpy(szCommand, TEXT("cmd.exe"));
} else{
lstrcpy(szCommand, TEXT("command.com"));
}
}
//set up the STARTUPINFO structure
ZeroMemory(&sui, sizeof(sui));
sui.cb = sizeof(sui);
//create the process
CreateProcess( NULL, szCommand, NULL, NULL,
FALSE, NORMAL_PRIORITY_CLASS,
NULL, szInitialDirectory,
&sui, &pi);
从图十五中可以看到,每一个菜单想多显示了一个小位图。这是一种组合了Win32标准的宿主绘画机制和IContextMenu3
接口以后的效果。一个外壳扩展如果想要使用位映图像,必须实现IContextMenu3或IContextMenu2接口。这两个接口都是从标准的IcontextMenu继承而来,并且IContextMenu3是建立在IcontextMenu2之上的接口。这些接口被绑定到外壳的特定版本,但在Windows
2000里,它们已经与Windows外壳称为一体了。今天的应用必须把焦点集中在IContextMenu3上。除了IContextMenu的方法以外,
IContextMenu3有一个函数叫做HandleMenuMsg2。在菜单从系统接受的所有消息中,HandleMenuMsg2允许扩展处理其中的四个消息:它们是WM_INITMENUPOPUP、
WM_MENUCHAR,、WM_MEASUREITEM、WM_DRAWITEM。通过处理这些消息,您可以自己绘制菜单项并使用位图和您喜欢的字体。当您用外壳扩展插入被绘制的菜单项时,不要忘了置MF_OWNERDRAW标志位开关。

在本系列文章中,FolderExt工程将系列文章中描述的所有的UI特性都集中在一起。如果能仔细研究其中的代码,您一定能开发出界面更丰富、更吸引人的Windows
2000应用程序。外壳扩展的概念是在Windows 2000操作系统中被加固定型的。其中的新特性(诸如:信息条提示、栏目、清除和搜索处理)大大增强了开发者UI编程的能力。在下一次的文章中,VC知识库将向您提供Windows
2000 UI编程方法中更多的现实可能性,那就是超文本模板(HTT)的应用。