第十六章 命名空间扩展
探测器使用层次结构表述形成系统的许多对象——文件,文件夹,打印机,网络对象等等。这些对象组合定义了一个命名空间,这是一个封闭的符号或名字集合,其中任何给定的符号或名字都能成功地被唤醒。在命名空间中解析一个名字就是成功地连接给定的名字到某个它所表述的实际信息。
探测器仔细地把所有这些对象收集到一起,与它们通讯,把它们的内容显示在典型的两窗框窗口中,树状观察在左侧,列表观察在右侧。
我们实际所关注的是探测器是否支持插入代码到它的结构中并增加全新定制对象的接口。事实上,Windows本身就伴随一定数量的命名空间扩展,例子包括‘拨号网络’,‘我的公文包’,以及‘我的计算机’文件夹。在这一章中,我们打算解释整个命名空间是怎样工作的,带你一起轻松游历其中的代码。
命名空间扩展实际是一个巨大的课题,然而却不难找到关于这个课题的文章,许多文章都只就上述两个方面之一进行讨论,要么解释基础,提供大量的自由代码来观察整个机理,要么集中于编码,讨论技巧和方法,而不提供命名空间扩展工作的全面解释,这里,我们依序解释下面各项内容:
命名空间扩展概览
安装命名空间扩展
编写完整的命名空间扩展来浏览所有当前打开窗口的层次结构
定义客户PIDLs的规则
开发命名空间扩展使客户应用驻留在探测器中
命名空间扩展是建立在本身并不特别复杂的概念之上的,然而,其极其丰富的资料,编程方法,实现特征,以及要求的技巧使编写这些任务远不是一般程序任务所能胜任的。即使已经有一个扩展安装并运行了,要想最终完成它也仍然有许多要做的工作。有非常多的附加特征可能需要你付出双倍的的时间和精力来完成。
公平地讲,空的命名空间扩展并不比我们前一章讲的Shell扩展复杂。麻烦是,在绝大多数情况下,空命名空间扩展是相当没用的。
命名空间扩展概览
命名空间扩展最容易的定义是:
命名空间扩展是允许扩展和定制被集成到Windows探测器中信息的一个方法。
‘集成’的基本意义是信息被显示和处理的方法与其它标准信息被显示和处理的方法一样。同义于‘命名空间扩展’的应该是‘客户绘制’文件夹——命名空间扩展包含访问和展示数据的代码,并试探与探测器进行集成。后面一步是相当标准的代码段,尽管被封装在一个类的集合中,它仍然是具有挑战意义的。
探测器显示的信息可能与物理目录相关,也可能不相关——例如,考虑‘拨号网络’文件夹,其中有关于Internet连接信息,或‘打印机’文件夹,其中包含了安装的打印机详细信息。其它命名空间扩展的例子是以非标准的方式显示文件数据,如‘回收站’或‘临时Internet文件’文件夹。
我们可以在命名空间扩展和通常意义文件夹之间确定三个层面上的差异:
观察,即,探测器右侧窗口的内容
菜单(和可能的工具条)
其它次要的图像变换,如树观察的图标和状态条上的客户文字
其中最重要的是显示在观察中的定制内容。尽管‘回收站’使用列表观察来显示其内容,但这仅是一个选择——在你的扩展中,可以使用任何种类的窗口,只要你喜欢(或许列表观察可能是你可以使用的最灵活的窗口)。
编写写命名空间扩展意味着什么
命名空间扩展在探测器中呈现一个定制的文件夹。它是一个Shell感知的进程内COM对象,倘若你正确地注册了它。命名空间扩展实现了一捆接口,探测器回调这些接口来获得它需要适当设置这个文件夹观察的所有信息。典型地,探测器要求:
文件夹管理对象,通过这个对象,回答探测器的请求
显示文件夹内容的窗口
枚举文件夹所包含各种项的对象
唯一地标识文件夹各个项的方法
客户化用户界面的附加功能集
下图说明了探测器的体系结构:
下图说明了与命名空间扩展的关系:
在探测器感觉到一个命名空间扩展存在时(后面我们将进一步精确解释它是怎样做到的),它就装入这个COM服务器,并查询IShellFolder接口。这是一个作为文件夹管理器工作的接口,并且向探测器提供它所要求的所有东西。换句话说,它是在探测器与其它扩展之间的一个代理。
当探测器需要显示观察内容时,它请求IShellFolder给出观察对象。类似的,在显示树观察的节点时,它请求枚举内容和查找文件夹和子文件夹属性,所有这些都是通过IShellFolder接口来做的。
命名空间扩展被装入后,探测器也给它机会来更新用户界面。所有可能感兴趣的事件都通过调用指定接口的适当函数通知到扩展。
反过来,写一个命名空间扩展实际就是准备回答来自探测器的所有输入呼叫,而回答呼叫就是在特定的COM接口上实现一定的功能。正如我们想象的那样,有一个扩展必须支持的最小接口和函数集以使其能够很好地被集成。
探测器内部结构
探测器仅仅是一个由空框架,如树观察,列表观察和几个其它控件组成的普通程序,它完全依赖于Shell和命名空间扩展来为其基本骨骼添加实在的血肉。实际显示的每一件东西都来自于explorer.exe文件之外。标准的扩展是在shell32.dll中实现的,这使其成为一类系统代码,然而,它们确切的是命名空间扩展。探测器扫描注册表来安装部件和打开与它们的通讯,不管它们是你自己写的还是操作系统提供的。
命名空间扩展与Shell扩展
在原理上,Shell扩展和命名空间扩展确实是相当类似的。二者都需要注册以便被感觉和被唤醒。二者都是进程内COM服务器,实现固定数量的接口,而且二者都允许Shell客户化。最大的差别是它们最终产生的效果:命名空间扩展把新文件夹加入到探测器,而Shell扩展被限制在文件类型上工作。
主接口
现在我们已经有点理解了在命名空间扩展运行时会发生什么情况,下面我们来看一下实际所发生的。这给我们一个机会来查看接口和一些函数原型。后面我们将使用这些信息来构造我们的例子。命名空间扩展绝对需要实现的接口是:
! IShellFolder
! IPersistFolder
! IEnumIDList
! IShellView
头两个就是我们前面说过的‘文件夹管理器’。IEnumIDList是我们称之为‘枚举器’的东西,而IShellView主要是提供观察窗口,这是将替代标准的列表观察的窗口。
这四个主要的接口(以及某些次要接口)都包含有PIDL的概念,我们在第2章就已经精确地解释了PIDL,以及它的实现,我们在这里重新概括一下:PIDL是在整个Shell命名空间中明确表示一个文件夹项的标识符,PIDL是对一类文件夹特定的,并且在写一个‘客户文件夹’时,你也应该提供一个‘客户’PIDL。虽然在做这个时有几个基本规则要遵循,但还是没有设计PIDL的一般方法,它十分依赖于它所要辅助呈现的内容。我们还要进一步解释这个概念。
还有一些其它可选的COM接口是命名空间扩展可以实现的,实际上,它们就是IContextMenu 和IExtractIcon,处理定制的关联菜单和单个项的图标。
下一节我们给出列表,说明各个接口定义的函数。如果有理由避免实现它们的话,其名字是由斜体字给出的。为了避免实现,和为了使探测器知道继续操作,需要返回E_NOTIMPL错误码。
活动时序
在我们结束关于接口的叙述之前,有一些观念应该记住。在使探测器显示命名空间扩展期间给出它们之间通讯的描述。必须绘制事件的时序关系:
探测器通过连接点感觉命名空间扩展,并且取得它的CLSID。
探测器建立它的实例,并且查询IShellFolder接口。
探测器请求实现IShellFolder的对象返回指向IShellView接口的指针于观察对象上。
指向IShellBrowser的指针被传递到观察对象,允许它处理探测器的菜单和工具条。观察对象也接收指向IShellFolder的指针。
探测器请求IShellFolder对象返回枚举文件夹内容的对象,这个对象实现IEnumIDList接口。
探测器遍历包含在文件夹中的元素,为每一个项取得PIDL,并根据其角色和特征绘制图标。
这是在探测器树观察中选择了命名空间扩展节点后所发生的操作。当你点击展开时,探测器作下面的操作:
请求IShellFolder返回枚举文件夹内容的对象。
显示那些有‘文件夹’属性的元素。如果它含有子文件夹特征,则绘制一个‘+’节点。
请求IShellFolder提供树观察中所属每一个节点显示的图标(事实上,它接收一个指向IExtractIcon接口的指针)。
请求IShellFolder提供每一项的显示文字
请求IShellFolder提供每一项的关联菜单。
文件夹管理器
IShellFolder是从IPersistFolder导出的,而IPersistFolder依次从IPersist导出。IPersistFolder的功能是允许探测器初始化新文件夹,告诉它在命名空间中的位置。IShellFolder特有的函数以探测器可以请求观察对象、枚举对象,或子文件夹的方式组成编程接口,进一步,IShellFolder对象必须能够提供它包含的每一个单项的属性,两个项的比较,以及返回它们的显示名。项通过PIDLs标识。
IpersistFolder接口
下表给出了IPersistFolder接口的函数:
函数 |
描述 |
GetClassID() |
返回对象的 CLSID。这个方法来自于IPersist。 |
Initialize() |
允许文件夹初始化它自己。这个方法接收PIDL来标识文件夹在命名空间中的位置。这个方法不一定相关于文件夹,如果相关,它应该缓存这个PIDL,以备进一步的使用,否则只需简单地返回S_OK即可。 |
一般应该实现这两个方法,它们的原型和典型的(最小)代码如下:
STDMETHODIMP CShellFolder::GetClassID(LPCLSID lpClassID)
{
*lpClassID = CLSID_WinView;
return S_OK;
}
STDMETHODIMP CShellFolder::Initialize(LPCITEMIDLIST pidl)
{
return S_OK;
}
这段代码取自本章中一个例子,现在,我们必须清楚CLSID_WinView是扩展本身的CLSID标识,而CShellFolder则是由IShellFolder和IPersistFolder导出的C++的类名。
你绝不应该直接调用IPersistFolder的方法,相反,系统在绑定它到你的文件夹过程中调用它们。
IShellFolder接口
IShellFolder接口输出的十个函数列表说明如下:
函数 |
描述 |
BindToObject() |
这是Shell请求模块打开子文件夹的方式。这个方法接收一个PIDL,应该简单地建立一个基于所接收PIDL的新文件夹对象。 |
BindToStorage() |
目前。Shell从不唤醒这个方法,因此只需返回E_NOTIMPL即可。 |
CompareIDs() |
携两个PIDLs,并决定它们的顺序——一个大于另一个,或它们是相等的。 |
CreateViewObject() |
建立和返回IShellView对象,这个对象将提供右窗框内的显示内容。 |
EnumObjects() |
建立和返回IEnumIDList对象,这个对象提供项的枚举操作。 |
GetAttributesOf() |
返回指定项的属性族——它是否可以重命名或拷贝,它是否有一个精灵图标,它是否是一个文件夹或有子文件夹,这些有效的常量都有以SFGAO_开始的助记符。资料中有完整的列表。 |
GetDisplayNameOf() |
返回显示项的名字,用于文件夹,地址栏或解析的目的。需要项名的应用把项名作为一个变量进行传递。值是SHGNO枚举类型。 |
GetUIObjectOf() |
探测器使用这个方法请求特定的接口,这些接口必须与UI一同操作。它是一种更特殊的QueryInterface()。 |
ParseDisplayName() |
返回一个PIDL给定的显示名。然而,这个显示名并不是必须出现在Shell观察中或地址栏中的。它是在SHGDN_FORPARSING标志设置后由GetDisplayNameOf()返回的。 |
SetNameOf() |
指派一个新的显示名到给定的对象。这是要使用在地址栏,文件夹或解析目的中的名字。
|
我们在第4章中解释过显示名,但是主要是在Shell内用于显示项的名字,在大多数场合,这个显示名与实际的文件名一致,然而对于不包含文件的文件夹,它可能就不一致了。有三种类型的显示名用于三种不同的关联。它们都来自下面的枚举类型:
typedef enum tagSHGDN
{
SHGDN_NORMAL = 0, // 相关于桌面的名字
SHGDN_INFOLDER = 1, // 相关于文件夹的名字
SHGDN_INCLUDE_NONFILESYS = 0x2000, // 非文件系统对象
SHGDN_FORADDRESSBAR = 0x4000, // 用于地址栏
SHGDN_FORPARSING = 0x8000, // 用于解析
} SHGNO;
在PIDL唯一的标识每一个项期间,它可以在不同的场合用不同的名字显示。为了在任何场合返回显示名,你应该实现GetDisplayNameOf()。这个函数接收值为SHGNO值组合的参数,特别是这个函数可以被要求返回绝对(SHGDN_NORMAL)名或相对名(SHGDN_INFOLDER)。前者,你确切地返回相对于桌面的显示名,而后者则要求相对于父文件夹的名字。
除此之外,可能还有关于Shell使用的名字的更多标志,这给出适当调整名字的机会。当名字被显示在地址栏的时候,SHGDN_FORADDRESSBAR标志设置,当你感觉到SHGDN_FORPARSING的时候,说明名字要被传递到ParseDisplayName(),来转换成一个PIDL。你可能需要包含特殊信息来辅助这个任务。
SHGDN_INCLUDE_NONFILESYS位简单地让这个方法知道调用者想要非文件系统对象。如果传递的PIDL不是文件系统对象,并且这个位没有设置,这个方法失败。
通过IShellFolder方法,探测器能够获得它所需要的关于扩展的任何信息。任何要求的接口都通过这个接口的方法获得:Shell观察,关联菜单,图标处理器,以及项枚举器等。
接口 |
通过函数获得 |
IShellView |
CreateViewObject() |
IContextMenu |
GetUIObjectOf() |
IExtractIcon |
GetUIObjectOf() |
IEnumIDList |
EnumObjects() |
项的枚举
写一个命名空间扩展来嵌入客户文件夹到Shell中,这个虚拟的(不是物理的)文件夹可能包含一些需要用非标准方式显示的内容。或者说,它包含了一些非标准的内容需要象文件对象列表那样显示。一个假想文件夹如‘我的硬件’可以包含对各种依附于系统的设备的引用。这个信息可以表示为一个列表观察,其中的设备作为显示项。
无论内容多么特殊,它都能由一个元素集组成,尽管命名空间扩展之外没有必要知道这个事实,不过,探测器需要枚举这些对象以便绘制树观察。为了允许外部模块遍历客户文件夹的内容,命名空间扩展应该实现IEnumIDList接口。这是一个函数集,它对外部模块提供枚举文件夹各个项的能力。这个接口及其普通,模块可以与它通讯,不需要知道文件夹本身内容和组织形式。
IEnumIDList接口
IEnumIDList接口输出四个函数,以便在给定集合中前后移动。
函数 |
描述 |
Next() |
返回集合中指定数量的项,每一个被找到的项都由PIDL标识。 |
Skip() |
跳过指定数目的项。 |
Reset() |
移动当前指针到列表顶部。 |
Clone() |
复制一个对象。 |
关键函数是Next(),它的原型如下:
HRESULT IEnumIDList::Next(ULONG celt,
LPITEMIDLIST* rgelt,
ULONG* pceltFetched);
函数的第一个变量指定要恢复的项数,而这些项的PIDLs将存储在rgelt数组中。实际拷贝的元素总数则存储在第三个变量pceltFetched中。枚举器对象处理所有项的一个链表。因而,完整实现应该保存一个指向当前项的指针,并且由Next()恢复的项数来移动它。
Skip()方法也由传递给它的变量向前移动项指针。但是它并不实际恢复和读出这些项的内容:
HRESULT IEnumIDList::Skip(ULONG celt);
Clone() 和 Reset()是这个对象的辅助方法,下面是这两个函数的典型实现:
STDMETHODIMP CEnumIDList::Reset()
{
m_pCurrent = m_pFirst;
return S_OK;
}
STDMETHODIMP CEnumIDList::Clone(IEnumIDList** ppEnum)
{
return E_NOTIMPL;
}
通常,这个接口的方法建筑在链表操作之上,链表则在类实现的初始化期间形成。链表中的每一个项指向一个PIDL。
PIDL的重要性
使用IEnumIDList的函数任何人都可以操作所有文件夹的内容。在命名空间扩展中,一个实现了IEnumIDList接口的对象作为调用IShellFolder::EnumObjects()的结果被返回。然而,一般枚举项的接口并不足以正确标识每一个文件夹的项,而且是以PIDLs适合的形式。
正向我们在第2章中所解释的,PIDL是一个指向SHITEMID结构集的指针。它使你能在一个文件夹中容易且清晰地标识任何给定对象的相对或绝对路由。说一个路由是相对的,如果它从包含项的文件夹开始,而绝对路由则是从桌面开始的一系列引用,并且直到这个对象。在整个Shell中PIDL总是唯一地标识一个元素。
定义一个好的PIDL显然是命名空间扩展的中心工作。PIDL应该是数据块集合,其中的每一个都涉及到从桌面到这个项的路径中所遇到的文件夹或子文件夹。PIDL的结构依赖于你想要文件夹展示的数据,而且决定怎样组织PIDL最终是程序员的工作,但是下面推荐的几点需要考虑:
PIDL应该通过Shell存储分配器(IMalloc接口)来分配。这允许探测器释放它。PIDL不是对象,但是它是一个存储块:一旦把它传递给探测器,它必须能够没有副作用地被释放。
PIDLs可以被保存到永久存储介质上,和从永久存储介质上读出。例如磁盘文件。这意味着所有需要的信息都必须顺序被找到。不是指向外部数据的指针,也不是表示外部数据的引用。
由于PIDLs可以是持续的,你可能需要考虑使用签名和版本号,以便在任何时候总能识别这些PIDL,并保证向后兼容。当然,如果对你的应用这并不是主要的,你可以不必这么做。
PIDL是SHITEMID结构的数组:
typedef struct _SHITEMID
{
USHORT cb;
BYTE abID[1];
} SHITEMID, *LPSHITEMID;
Cb是整个结构的尺寸,包括它自己。abID成员标示一个数据序列的开始,这个序列可以以任何你想要的方式构造。作为例子,考虑下面的PIDLDATA结构:
typedef struct _PIDLDATA
{
TCHAR szSignature[SIGNSIZE];
WORD wVersion;
TCHAR szFileName[MAX_PATH];
BYTE icon[ICONFILESIZE];
} PIDLDATA, *LPPIDLDATA;
这时展示形成文件名PIDL数据的一种可能的方法——由SHITEMID结构的abID字段指向的一个数据块,而且有两件事情需要注意,第一,串包含了全部字符,这也是不能使用指针的理由。TCHAR[]缓冲保证所有内容都顺序存储。第二,我们假设需要存储图标,你不能使用HICON作为等价的内存存储块。相反,你需要连续的形成图标图像的所有字节。
Shell观察
观察对象无疑是任何命名空间扩展最值得关注的部分。你所写的大部分命名空间扩展代码都在后台工作,静默的与探测器通讯,从不能明显地看到它们的活动。
然而,观察对象则建立和管理窗口——Shell观察。Shell观察是一个普通窗口,具有一般的风格和窗口过程。观察对象就是最终嵌入在探测器右窗框上的窗口,显示左窗框树观察中选中的文件夹内容。这个观察对象输出IShellView接口的方法,以便与Shell观察一道工作,并且处理任何关系到这个文件夹用户接口的事情,消息循环,菜单和工具条拼接。IShellView接口由IOleWindow导出。
IShellView接口
下面是支持IShellView接口所应该实现的函数:
函数 |
描述 |
AddPropertySheetPages() |
允许你添加定制页面到‘文件夹选项…’对话框 |
CreateViewWindow() |
建立和返回嵌入到探测器右窗框中的窗口。它应该是一个无边框对话框。 |
DestroyViewWindow() |
销毁上面建立的窗口 |
EnableModeless() |
探测器当前没有使用,简单地返回 E_NOTIMPL。 |
EnableModelessSV() |
探测器当前没有使用,简单地返回 E_NOTIMPL。 |
GetCurrentInfo() |
通过FOLDERSETTINGS结构返回文件夹当前设置。 |
GetItemObject() |
为关联菜单或剪裁板返回指向给定项集的接口指针,主要由通用对话框使用。 |
Refresh() |
引起文件夹内容的重绘 |
SaveViewState() |
保存观察状态 |
SelectItem() |
改变一项或多项的选择状态 |
TranslateAccelerator() |
当焦点在扩展上时,转换任何击键。返回S_OK以防止探测器再次转换。 |
UIActivate() |
活动状态改变时被唤醒——在文件夹被激活或禁止时。 |
GetWindow() |
返回观察的窗口Handle。这个方法来自IOleWindow |
ContextSensitiveHelp() |
文件夹应该进入或退出关联感觉的辅助模式,和处理所有不同的消息。这个方法来自IOleWindow |
观察对象有时给出保存状态到永久存储的机会。在可以这样做时,探测器就调用SaveViewState()。现在这个过程需要一点技巧,所以,尽管你或许能给出几个其它的方法来使文件夹的设置永久保持,我们还是推荐使用流来保存它。一个指向IStream对象的指针由IShellBrowser接口的GetViewStateStream()方法返回。且慢,这个接口从哪来的。事实上,这个接口是由探测器实现的,只是没有显式的函数获取它,一个指向它的指针由Shell观察在CreateViewWindow()的参数列表中传递,因而我们可以保存它以备将来在扩展中的使用。
对于SaveViewState()函数,需要有下面的代码做一些操作来辅助:
IStream* pstm;
pSB->GetViewStateStream(STGM_WRITE, &pstm);
pstm->Write(&data, sizeof(data), NULL);
用GetViewStateStream(),你可以握住一个流到你写状态设置的地方,如列宽度,图标,以及任何可以施加于扩展上的操作。要读回状态,我们遵循同样的方法,但是此时打开的观察流为读:
IStream* pstm;
pSB->GetViewStateStream(STGM_READ, &pstm);
pstm->Read(&data, sizeof(data), NULL);
这里的代码一般是出现在CreateViewWindow(),在构造了Shell观察窗口之后。没必要关闭这个流,这直接由探测器完成。
与探测器会话:IShellBrowser接口
在前面的图表中我们给出了探测器在初始化建立新文件夹过程中所使用的IShellFolder接口方法。它从IShellFolder中获取的东西(即,指向IShellView 和其它接口的指针)可以通过IShellBrowser与探测器交互,这是为在探测器与命名空间扩展之间通讯而精确实现的一个接口。
IShellBrowser输出几个函数,但是在你的命名空间扩展中使用它主要有两个目的:
获得观察状态流
与探测器菜单和工具条交互
我们已经讨论了前一个,所以现在让我们进入到修改菜单和工具条这一步,以便在我们自己的命名空间扩展中添加特殊的项。
修改探测器的菜单
文件夹,即使是客户文件夹也总是一个文件夹,这就是说,它有一个通常的菜单和工具条,这是任何文件夹都有的。或者说,如果它决定不改变的话,它有一个通常的菜单和工具条。
文件夹在获得焦点时将产生对探测器菜单和工具条的改变,并在失去焦点时删除它们。对于文件夹,活动状态由UIActivate()方法来通知:
HRESULT IShellView::UIActivate(UINT uState);
uState变量可以取下述三种可能值之一:
标志 |
描述 |
SVUIA_ACTIVATE_FOCUS |
文件夹现在有焦点 |
SVUIA_ACTIVATE_NOFOCUS |
文件夹被选中但是没有焦点 |
SVUIA_DEACTIVATE |
文件夹不再有焦点 |
当系统焦点属于观察的子元素时,文件夹有焦点,如果文件夹仅仅在左窗框被选中,则有这种情况,其中SVUIA_ACTIVATE_NOFOCUS出现。对于每一个不同的活动状态,都可以有不同的菜单和工具条显示在探测器的用户界面中,而且所有这些改变通常都在UIActivate()中完成。
为了改变菜单内——无论是要加一个标记,还是简单地添加或删除存在的项——你总是需要建立新的,空的顶层菜单。建立新菜单的代码简单地是:
hMenu = CreateMenu();
然后,只需请求Shell用正常的方法填写它即可,其代码如下:
OLEMENUGROUPWIDTHS omw = {0, 0, 0, 0, 0, 0};
m_pShellBrowser->InsertMenusSB(hMenu, &omw);
OLEMENUGROUPWIDTHS是一个数据结构,形式为六个长整数数组,它在容器和嵌入对象需要共享菜单时起作用(更详细的信息请参考VC++或MSDN资料关于OLE in_place编辑)。
基本上,探测器(和OLE容器)的菜单都划分成六个组,每一组都可以包含许多不同菜单,分组是:
组名 |
位置 |
控制者 |
文件 |
0 |
探测器(容器) |
编辑 |
1 |
命名空间扩展(对象) |
容器 |
2 |
探测器(容器) |
对象 |
3 |
命名空间扩展(对象) |
窗口 |
4 |
探测器(容器) |
帮助 |
5 |
命名空间扩展(对象) |
在组名与弹出菜单之间不必有一对一的关系,要么是对名字,要么对应数字。换句话说,‘文件’组的第一个菜单可以不同地包含:
单一称为‘文件’的菜单
两个弹出菜单如‘属性’和‘编辑’
一个‘文件’弹出菜单和另一个菜单,如‘目录’
任何其它可能的组合
每一组包含的不同弹出菜单数存储在OLEMENUGROUPWIDTHS结构中对应的位置。这也被作为菜单组的宽度引用。
通过调用InsertMenusSB(),你可以请求Shell填写它自己的共享菜单部分,象表中显示的那样,容器和对象——此时是探测器和命名空间扩展——各自负责三个组。此时的探测器作如下操作:
OLEMENUGROUPWIDTHS omw = {0, 0, 0, 0, 0, 0};
m_pShellBrowser->InsertMenusSB(hMenu, &omw);
菜单如下:
菜单 |
组 |
文件 |
文件 |
编辑 |
文件 |
观察 |
容器 |
Go |
容器 |
Favorites |
容器 |
工具 |
容器 |
帮助 |
窗口 |
OLEMENUGROUPWIDTHS结构包含{2, 0, 4, 0, 1, 0},而且,可以使用这些值作为偏移来正确地放置任何新的弹出菜单。添加自己的项或弹出菜单,使用传统的Win32函数,如InsertMenuItem(),DeleteMenu(),或任何可以修改菜单结构的函数。
我们知道AppendMenu(),InsertMenu(),ModifyMenu()和几个其它的菜单函数是在Win32 API中声明的老函数,它们仍然被支持并且能很好地工作,但是未来版本的Windows下,使用这些函数的代码不能保证继续工作。可以使用新函数InsertMenuItem()来代替它们。
一般来讲,主对象应该是一个好的居留者,不应该删除由容器设置的项或弹出菜单。同样,还应该避免侵入容器为放置菜单到所管理的菜单组而保留的空间。然而,这些规则是应用于OLE容器的,而且,如果探测器定义了一个空的‘编辑’菜单,我们的意见是,在你的文件夹选中时,没有理由继续保留它。另一个可以打破这个规则的情况是在添加客户‘About’命令时。此时,我们是用自己的项替换了标准项,也就是说删除了由探测器添加的项。
Shell指派唯一的标识符到它的弹出菜单,以便使你能通过命令而不是位置来做编辑工作。shlobj.h头文件定义如下的常量来辅助标识探测器弹出菜单的命令:
FCIDM_MENU_FILE
FCIDM_MENU_EDIT
FCIDM_MENU_VIEW
FCIDM_MENU_FAVORITES
FCIDM_MENU_TOOLS
FCIDM_MENU_HELP
如上所示,缺少了‘Go’菜单对应的常量。如果察看shlobj.h头文件,则会发现另一个类似的常量,但是它并不关联于命名空间扩展修改菜单。
被添加项的ID必须在0x0000 到0x7fff之间,这个限制封装在常量FCIDM_SHVIEWFIRST和FCIDM_SHVIEWLAST之中。一旦你根据需要改变了菜单,就必须使用下面调用保存它:
m_pShellBrowser->SetMenuSB(hMenu, NULL, hwndView);
奇怪的是SetMenuSB()函数在说明中只有两个变量:
HRESULT SetMenuSB(HMENU hmenuShared, HOLEMENU holemenuReserved);
而它实际要求三个变量:
HRESULT SetMenuSB(HMENU hmenuShared, // 共享菜单
HOLEMENU holemenuReserved, //探测器当前忽略
HWND hwndActiveObject // 观察窗口的 Handle
);
头一个变量是探测器与命名空间扩展共享的菜单,第二个是不考虑的,最后一个是文件夹展示窗口的Handle。为了理解为什么当前忽略第二个变量。我们需要暂时离题来讨论:在容器和对象之间共享菜单时,OLE in-place编辑是怎样工作的。
IShellBrowser的关于菜单和工具条操作的方法来自于IOleInPlaceFrame所采用的类似方法,他们是由OLE容器实现的接口方法。从一方面看,探测器本身对命名空间扩展是一个特殊的容器,在修改了菜单之后,典型的宿主于OLE容器的对象将需要填写它自己的OLEMENUGROUPWIDTHS结构部分,然后建立‘OLE菜单描述符’。事实上,有一个函数确切地做这个工作,其原型如下:
HOLEMENU OleCreateMenuDescriptor(HMENU hmenuShared, // 组合菜单
LPOLEMENUGROUPWIDTHS lpMenuWidths // 更新的OLEMENUGROUPWIDTHS
)
其次,它传递这个HOLEMENU到SetMenu(),这是一个非常类似于SetMenuSB()的方法,其原型为:
HRESULT SetMenu(HMENU hmenuShared, HOLEMENU holemenu);
在这个函数内部,容器最终调用OleSetMenuDescriptor(),它负责设置由菜单生成消息的派遣代码。实际这个函数安装了一个钩子,来感觉菜单消息,然后派遣它们到正确的窗口——是容器,还是对象的窗口。为了理解给定消息的目标是哪一个窗口,这个钩子简单地察看菜单生成的位置,它通过比较位置和由HOLEMENU引用的OLEMENUGROUPWIDTHS结构中的值解析疑惑。现在,探测器使用不同的逻辑来指派消息,而且既不需要HOLEMENU,也完全不需要客户充填OLEMENUGROUPWIDTHS结构。在调用SetMenuSB()时,探测器设置钩子来解释消息,而且消息都被指派到活动的观察窗口(hwndActiveObject参数),这个窗口是由它的ID来标识的,而不是通过菜单位置标识的。在活动状态不同于SVUIA_DEACTIVATE时,菜单将产生所有这些改变。在用户解除文件夹的活动状态时,你必须恢复前一个状态,如此,只需通过调用IShellBrowser::RemoveMenusSB()来删除菜单即可:
m_pShellBrowser->RemoveMenusSB(hMenu);
DestroyMenu(hMenu);
修改探测器工具条
要处理工具条,首先需要知道它的窗口Handle,这是IShellBrowser接口提供的另一个服务——需要调用GetControlWindow()函数,它正好返回HWND。然而,资料说明禁止直接发送消息到这个窗口,所以为了向前兼容,你应该使用SendControlMsg(),这是IShellBrowser的另一个方法:
HRESULT IShellBrowser::SendControlMsg(UINT id,
UINT uMsg,
WPARAM wParam,
LPARAM lParam,
LRESULT* pret);
它看上去类似于普通的SendMessage()函数,有两点不同:id变量标识你正在选择的控件(工具条或状态条),pret 则取得你发送的消息的返回值。使用FCW_TOOLBAR常量来选择工具条。
参考VC++ 或MSDN资料获得关于结构和消息的详细解释。
此时,你可以发送消息来添加任何你希望的按钮。然而添加按钮到工具条并不是一项容易的工作。工具条按钮有一个位图,所以首先必须在工具条中注册一个新的位图。下面是说明操作过程的代码段:
TBADDBITMAP tbab;
tbab.hInst = g_hInstance; // 设置包含位图的模块
tbab.nID = IDB_TOOLBAR; // 模块资源中的位图 ID
m_pShellBrowser->SendControlMsg(FCW_TOOLBAR, TB_ADDBITMAP,
1, reinterpret_cast
TBADDBITMAP结构只包含两个成员来标识一个位图。一是在资源中包含位图的模块,另一个是适当的ID,你使用TB_ADDBITMAP消息传递这个结构到工具条。这个消息可以实际接收一个结构数组,所以消息的wParam变量(上面代码中为1)表示了数组尺寸,而lParam则指向第一个元素。lNewIndex是包含消息返回值的缓冲,这是重要的,因为它是所生成图像在全部工具条位图中的索引。形成工具条的所有位图都存储在单一位图中,逐个小图像连续存放。
添加文字标签也使用相同的技术。(进入探测器工具条的按钮也需要文字标签)
m_pShellBrowser->SendControlMsg(FCW_TOOLBAR, TB_ADDSTRING,
NULL, reinterpret_cast
此时所涉及到的消息是TB_ADDSTRING,lParam变量指向串,lNewString将包含辅助工具条识别文字标签的索引。注册了新的位图和文字串到工具条后,就可以声明TBBUTTON结构来展示按钮了:
TBBUTTON tbb;
ZeroMemory(&tbb, sizeof(TBBUTTON));
tbb.iBitmap = lNewIndex;
tbb.iCommand = IDM_FILE_DOSOMETHING;
tbb.iString = lNewString;
tbb.fsState = TBSTATE_ENABLED;
tbb.fsStyle = TBSTYLE_BUTTON;
最后发送设置新按钮的消息:
m_pShellBrowser->SetToolbarItems(&tbb, 1, FCT_MERGE);
如果你有勇气,可以使用底层方法直接发送消息到工具条,然而应该再次指出微软阻止使用这项技术:
m_pShellBrowser->SendControlMsg(FCW_TOOLBAR, TB_INSERTBUTTON,
0, reinterpret_cast
这行代码添加新按钮到工具条的开始位置,作为第一个按钮。这个方法实际上比我们给出的任何添加按钮的方法都更有效。使用SetToolbarItems()添加按钮总是加在工具条的末端。
即使没有显式的资料说明,我们也建议你应该在文件夹失去焦点时删除所有添加的按钮。唯一的选择是使用SendControlMsg(),无论添加按钮使用的技术如何:
TBBUTTON tbb;
m_pShellBrowser->SendControlMsg(FCW_TOOLBAR, TB_GETBUTTON,
0, reinterpret_cast
if(tbb.idCommand == IDM_FILE_RUN)
m_pShellBrowser->SendControlMsg(FCW_TOOLBAR, TB_DELETEBUTTON, 0, TRUE, NULL);
在删除按钮之前一定要确保它是你想要删除的。检查命令ID是一项可靠的技术。
访问探测器状态条
上面工具条中我们使用的接口也可以用于状态条来设置客户文字,调用IShellBrowser::SendControlMsg()时标识状态条的ID为FCW_STATUS。此时,你可以完全自由地发送消息,和以你的方式格式化状态条。然而,如果你只是简单地想要某些文字显示的话,我们建议你使用IShellBrowser的另一个辅助函数,SetStatusTextSB():
m_pShellBrowser->SetStatusTextSB(wszText);
这个函数的唯一缺点是(如果开发的是基于ANSI的软件)它需要Unicode串。你必须自己站换。
附加接口
在使用探测器期间,你可以对每一个项激活关联菜单,拖拽它们,甚至拷贝它们到剪裁板。这些并不是探测器的内建行为,而是由文件夹本身提供的特征。更精确一点,它是由管理文件夹外观和行为的命名空间扩展提供的。谁也没有文件夹本身更了解其中项的实际表现和处理方式。
在文件夹执行封装数据的特殊活动或准备菜单期间,触发者仍然是探测器。它感觉到用户的活动,请求命名空间扩展提供数据拷贝到剪裁板或拖拽。
探测器的右窗框整个由观察对象绘制,但是命名空间扩展没有控件在左窗框上出现。不过即使在树观察中用户也可以唤醒关联菜单或打开子树,来显示给定文件夹中的子文件夹列表。这个关联菜单是谁提供的,还有填充树观察的图标?这始终是命名空间扩展在探测器请求上所作的活动。这一点也不奇怪,命名空间扩展通过实现几个附加的COM接口来做这个工作:IContextMenu,IDataObject 和IExtractIcon。它们的功能我们在相关的章节中都已经解释过了。如果某个接口缺失了,相关的功能就是不可用的。
取得附加接口指针
在上一章,我们看过几个Shell关联菜单的例子,因此应该还有怎样实现IContextMenu接口的基本概念。在命名空间扩展内实现IContextMenu基本上与Shell扩展一样——改变的只是初始化过程。当用户右击探测器树观察时,Shell努力获得指向文件夹实现的IContextMenu接口指针。如果返回了可用的东西,关联菜单将显示,否则操作被拒绝。探测器获得指向IContextMenu接口指针的方法是IShellFolder::GetUIObjectOf()。下面是其典型的实现:
STDMETHODIMP CShellFolder::GetUIObjectOf(HWND hwndOwner, UINT uCount,
LPCITEMIDLIST* pPidl, REFIID riid, LPUINT puReserved, LPVOID* ppvReturn)
{
// 清除返回数据的缓冲
*ppvReturn = NULL;
// 如果接口请求的PIDL数>1 则失败
if(uCount != 1)
return E_FAIL;
// 检查实现的附加接口的 riid
// IExtractIcon
if(IsEqualIID(riid, IID_IExtractIcon))
{
CExtractIcon* pei;
pei = new CExtractIcon(pPidl[0]); // pPidl 数组的第一项
if(pei)
{
pei->AddRef(); // 增加引用计数
pei->QueryInterface(riid, ppvReturn); // QI 提示增加引用计数
pei->Release(); // 减少引用计数
return S_OK;
}
return E_OUTOFMEMORY;
}
// IContextMenu
if(IsEqualIID(riid, IID_IContextMenu))
{
CContextMenu* pcm;
pcm = new CContextMenu(pPidl[0]);
if(pcm)
{
pcm->AddRef();
pcm->QueryInterface(riid, ppvReturn);
pcm->Release();
return S_OK;
}
return E_OUTOFMEMORY;
}
// 检查其它可能的接口...
return E_NOINTERFACE;
}
这个代码段来自后面将要讨论的例子,其中CShellFolder是实现IShellFolder接口的 C++ 类。同样CExtractIcon 和 CContextMenu 也实现了IExtractIcon 和 IContextMenu。现在让我们查看一下这些方法的原型。hwndOwner是任何显示对话框或窗口的父窗口Handle。这个方法接收一个PIDLs数组(pPisl),其尺寸在uCount中传递。Riid 请求的接口适用于这个数组的所有元素。
有一些接口并不能在多重PIDLs上同时操作——例如,关联菜单,总是引用一个项,图标也是如此。相反,IDataObject可以用于拷贝到剪裁板,或拖拽一个项集。在上面的例子中我们仅仅实现了IContextMenu 和 IExtractIcon,因此首先检查传递的PIDLs数。
在命名空间扩展中,我们并不需要从IShellExtInit 或 IPersistFile中导出实现Shell接口的类。命名空间扩展内接口使用PIDLs清楚地标识所操作的项。PIDL是Shell传递给GetUIObjectOf()的一个变量,所以最合理的是由一个类构造器来接收PIDL。使用这种方式,就可以初始化你的关联菜单,令其知道它所引用的是什么项。
关联菜单
在IContextMenu接口的三个函数中,仅需要实现InvokeCommand()和QueryContextMenu(),不必实现GetCommandString()。这些方法的实现遵循前一章给出的规则。
客户图标
如果你想要对探测器窗框中文件夹项的实际显示图标进行控制,就需要实现IExtractIcon接口。如第15章解释的,你可以使用两个方法传递图标到Shell:GetIconLocation()和Extract(),它俩是互相排斥的,一个调用成功,另一个就不能被调用。GetIconLocation()期望返回抽取文件的路径名和图标索引,实际抽取则由Shell执行。相反,Extract()做抽取操作,返回大图标和小图标的HICON。
拖拽操作
如果想要支持文件夹项的拖拽操作,你需要实现适当的接口。尤其是需要IDropTarget,以便确定项目落下时需要做什么。
拷贝到剪裁板
拷贝到剪裁板与包装拖拽数据都需要通过IDataObject接口传递数据。这是一个通用接口,提供在应用之间跨系统移动的各种格式数据,虽然Windows自己管理实际数据的存储。你所要做的就是给出你自己的格式和数据。
文件夹概念
写一个命名空间扩展就是你有某些想要使用探测器的文档为中心概念和层次逻辑展示的内容。也就是说你必须开始以文件夹,子文件夹和项的概念考虑你的内容。有时,这样更容易和直接。另一些场合则需要花一点时间进行整理。也有一些情况是根本不可能这样做的。文件夹的概念——在你的脑海中是清晰的,文件夹关联了什么——是开发命名空间扩展的中心。
一个命名空间扩展是一个主文件夹,即一种根目录。它的内容可能被划分成其它子文件夹和项。最简单的情况是没有文件夹和要显示的项,此时,你只是插入应用到探测器的框架中。后面我们将更进一步说明这一点。
你必须对探测器声明你的文件夹是什么,以及它们是否有子文件夹。一定要保证精确地做这个操作,因为它可能会影响到 Shell 处理扩展的方式。实际上,所有你声明成文件夹的项都将作为父文件夹的子节点显示在树观察中。如果你声明一个文件夹有子文件夹,探测器则使它成为树观察中的可扩展节点。
探测器通过调用IShellFolder::EnumObjects()来请求扩展枚举它的内容,这个函数在每次探测器需要枚举文件夹部分时都被调用,而不是只调用一次来获得枚举器对象的指针。下面是它的典型实现:
STDMETHODIMP CShellFolder::EnumObjects(HWND hwndOwner, DWORD dwFlags,
IEnumIDList** ppEnumIDList)
{
*ppEnumIDList = NULL;
// m_pidl 是 CShellFolder 的成员,存储这个文件夹的 PIDL 。必须在类的构造其中填写它。
*ppEnumIDList = new CEnumIDList(m_pidl, dwFlags);
if(*ppEnumIDList == NULL)
return hr;
return S_OK;
}
在几乎所有IShellFolder的实现中都定义了m_pidl数据成员,以保存文件夹的PIDL。(Shell 在通过IPersistFolder绑定这个文件夹时传递它的PIDL)
这里重要的是EnumObjects()接收一个DWORD来表示Shell想要枚举器提供那种项。换言之,dwFlags工作就象滤波器,使Shell的请求施加于任何IEnumIDList::Next()的返回。
当然,最终由你来决定对于你的客户文件夹这个标志是否有意义。没有任何东西阻止你简化忽略这个标志,只要你甘愿冒这个风险。
文件夹属性
dwFlags可以设定的值来自SHCONTF枚举类型,其定义如下:
typedef enum tagSHCONTF
{
SHCONTF_FOLDERS = 32,
SHCONTF_NONFOLDERS = 64,
SHCONTF_INCLUDEHIDDEN = 128,
} SHCONTF;
它告诉你是否它想要文件夹,文件夹和项,以及是否包含隐藏项。在正常情况下,Shell不要求文件夹仅仅枚举项,但是如果在你的设想中它是合理的,在你认为是适当的时候可以只返回项。要声明我们的项为文件夹所要做的是什么呢?我们正好要回答什么时候Shell请求给定项的属性。这是通过IShellFolder::GetAttributesOf()方法来完成的,下面的清单说明了它的基本实现:
STDMETHODIMP CShellFolder::GetAttributesOf(UINT uCount, LPCITEMIDLIST aPidls[],
LPDWORD pdwAttribs)
{
*pdwAttribs = 0xFFFFFFFF;
for(UINT i = 0 ; i < uCount ; i++)
{
DWORD dwAttribs = 0;
if(IsThisAFolder(aPidls[i]))
dwAttribs |= SFGAO_FOLDER;
if(HasSubFolders(aPidls[i]))
dwAttribs |= SFGAO_HASSUBFOLDER;
*pdwAttribs &= dwAttribs;
}
return S_OK;
}
当然IsThisAFolder()和HasSubFolders()是假象的函数,你应该使用自己的例程替换它们——我们放置它们在这里只是想要说明GetAttributesOf()应该有的代码框架。使用PIDLs数组调用这个函数,你应该指定它们所有的全部属性。
这里所看到的属性并不是你所能唯一设置的,在第4章中有完整的 SFGAO_XXX 常量列表。
命名空间扩展的风格
命名空间扩展不只是代码,还相关于安装期间实际存储在注册表中的内容,它可能以稍微不同的方式展示自己。因此你或许会听到过两种类型的命名空间扩展:根和非根。
二者之间的差异不在代码,而在于注册表条目——要实现的接口和所遵循的行为都是相同的。所不同的是容留它的探测器观察。在这一节我们详细讨论根与非根扩展,并确定什么情况应该做出选择。
根扩展
基本上,根扩展是一个以自身为根的文件夹,就是,你不能进一步浏览到它的上层,或跳到同等深度的并行节点。根扩展只显示它自己的子树,并且内容完全与探测器的其余命名空间隔绝。具体例子看下面这个截图:
它显示了一个窗口,当你在任务条的‘开始’菜单上要改变设置时这个窗口出现。开始菜单节点是这棵树的根,像上一层的按钮被禁止,所以,你不能向整个空间的更高层移动。
这个图没有显示客户扩展,而是简单地显示根观察。根命名空间扩展是具有根观察的客户文件夹。
非根扩展
非根扩展实际是相对于根扩展而言的,它们没有固定的根,允许你在整个探测器层次上浏览。非根扩展完美而确切地集成到Shell的命名空间中。注意,同一个命名空间扩展可能以两种方式访问和使用,这就提供了对前面说明的验证:‘根’和‘非根’是施加到观察的特征,而不是文件夹的特征,无论它是不是客户的文件夹。
根与非根
现在我们来概括某些观点。你有一个客户文件夹——即,一个命名空间扩展。你有机会使它可通过两个可能的观察来访问:根和非根。前者,观察显示文件夹作为单独对象,你看到一个受限的Shell命名空间部分。后者,探测器观察包含了所有文件夹,也包括我们的客户文件夹,所以你可以在文件夹中前后移动。
我们可以建立快捷方式以两种观察打开同一个文件夹。象在第11章中解释的那样,我们要做的所有事情就是在探测器的命令行中指定/root开关:
explorer /root, ::{clsid}
这里的例子表示打开一个由CLSID表示的根命名空间扩展。通过 /e 开关可以要求显示作窗框的树观察。
explorer /e, ::{clsid}
上面的命令使用指定选中的和打开的文件夹打开传统的探测器观察。这是非根观察。
什么时候做出选择
从代码的观点我们在重复一次,在编写根扩展与非根扩展之间绝对没有差别。事实上,这个特征应该更确切地施加于观察而不是扩展本身。但是,什么时候使用根或非根观察来使我们的命名空间扩展可访问呢?我们认为这个问题是存在争论的,但是,一般来讲,只有文件夹的内容确实独立,几乎和应用一样时才使用根观察。
不管你写了多少扩展,探测器还是基于文件管理器的,所以,对于不能在文件系统元素上工作的任何扩展你都应该考虑使用根观察,相反,当希望以客户的方式表示相关的文件和目录信息时,非根观察是一个好的选择。例子就是‘Temporary Internet Files’文件夹,它聚集四个隐藏的目录,所有文件都在Internet会话期间静默的下载。当你打开这个文件夹时,你并不能看到引用的四个子文件夹,而仅仅是文件。
关于根与非根的讨论直接把我们带到了开发命名空间扩展的另一个重点。这与某些其它方面不同,它对扩展的工作和使用有决定性的影响。
连接点
编写命名空间扩展仅仅是过程的一半,你知道我们还需要键入某些注册表信息。然而,有时我们需要做得比这还要多。依据我们在注册表中的设置,和扩展实际的活动,我们可能需要一个称之为连接点这样的东西。
在坚硬的外壳下,连接点是一种方法,通过这种方法我们能够连接我们的文件夹到Shell的命名空间。这实际并不是新概念——在我们讲到Shell扩展时连接点就已经出现。但是在第15章中我们并没有提到它们,因为它们是通过为Shell扩展提供的注册表信息自动处理的。对于命名空间扩展,这有点不同。
首先,命名空间扩展是进程内服务器,所以它要求适当地注册在HKEY_CLASSES_ROOT\CLSID键下,其次,它可能需要某些特殊的注册信息(后面我们将精确地讨论这一点)它们服务于文件夹的行为和显示设置。
仍然要明显定义的是命名空间扩展怎样与Shell连接。换言之,文件夹应该定位在Shell层次的那个地方。对于Shell扩展并没有这个问题(例如关联菜单处理器),因为Shell扩展是动态对象,它仅在需要的时候才获得调用,并且当它的引用计数返回到0后几秒钟之内就被卸载。
相反,命名空间扩展是一个文件夹,而文件夹应该在Shell中有一个位置。这个位置就是连接点。从另一个观点上看,我们可以说连接点就是访问命名空间扩展的方式。有四种方法来定位连接点:
用文件类型连接命名空间扩展(如果可用)
使用具有非常特殊内容的目录
使用具有特殊名字的目录
用一个已存在的命名空间连接它
下面让我们仔细的测试一下每一种选择。
使用文件类型
可能有点不太寻常(至少在我们的选择中),你可以考虑写一个命名空间扩展,使用户可以浏览你的文档内容。此时,你的扩展将绑定到文件类型,并且连接到关联菜单默认项。这说明命名空间扩展与Shell扩展实际上并没有多大差别。
自私地说,如果你有一个文档类型允许自身内容被浏览,最好是定义外部观察器,就象在第14章中我们对元文件的操作那样。使用外部观察器的明显优点是你省下了所有COM技巧和命名空间扩展的框架。缺点是建立了另一个进程,而命名空间扩展是运行在探测器地址空间上的。(这也可以看作是优点,因为不同的进程能更好地抗拒冲突)
使用文件类型作为连接点,你需要在文档类的关联菜单上建立新动词:
HKEY_CLASSES_ROOT
\YourDocument
\ShellEx
\NewVerb
\Command
在这个树中YourDocument和NewVerb是两个定制的键。如果你想要添加命名空间扩展来遍历HTML文件的内容,注册表条目应该是:
HKEY_CLASSES_ROOT
\htmfile
\ShellEx
\Browse
\Command
此时我们选择一个‘浏览’动词,它将把‘浏览’自动加到.htm的关联菜单中(我们在第14章看到了这个)。为了连接命名空间扩展与HTML文件,命令行应该是:
explorer /root, {CLSID}, %1
有某些文件可以作为不同类型信息的容器来工作,而且可以把这些考虑成文件夹和子文件夹。在HTML文件中可以找到这样的例子(包含对象,图像,表等的集合),如果它们是目录,你就可以浏览。此时有比命名空间扩展更好的办法吗?
在这种情况下,我们必然要使用根扩展,因为探测器不支持在树观察中浏览文件内容。
使用目录
正常情况,使用目录连接命名空间扩展到Shell。这个概念是非常简单的,因为命名空间扩展是客户文件夹,你必须用平常的名字建立一个普通文件夹,并且把它与你提供非标准行为的扩展联系到一起。有两个等价的方法来完成这一步。
desktop.ini文件
第一项技术要求你在可能的地方建立新目录,并给它一个你希望的名字。然后使它为只读并建立一个隐藏文件desktop.ini。这个文件的典型内容为:
[.ShellClassInfo]
CLSID={CLSID}
这就告诉探测器为需要显示的文件夹信息引用你指定的CLSID。你还可以通过设置下面路径下的CLSID默认键来改变这个文件夹的显示名:
HKEY_CLASSES_ROOT
\CLSID
\{CLSID}
同样,要设置客户图标,只需在同一路径下添加DefaultIcon键即可:
HKEY_CLASSES_ROOT
\CLSID
\{CLSID}
\DefaultIcon
这个键的默认值将定位所使用图标的位置和索引,一般的语法是:
"filename, index"
特殊文件夹名
一个更简单的(可能很少有人知道的)技术是建立一个文件夹并给它一个特殊的名字。没有其它操作。这个文件夹被自动建立,并具有只读属性,而且使用你设置在注册表中的图标和标题来显示,注册表设置如上所述。文件夹的名字应该有如下形式:
YourFolderName.{CLSID}
例如
SpecialName.{04051965-0fcc-11ce-bcb0-b3fd0e253841}
依附已存在的命名空间
Windows Shell提供了一个命名空间扩展聚集,其中有‘我的计算机’,‘网上邻居’,以及几个其它扩展。这就可以使你把自己的命名空间扩展放到这些已存在的特殊文件夹之下。尤其是你可以把它直接加到‘我的计算机’,或‘桌面’命名空间下。这样做可以自动地连接扩展到Shell,你不必作任何其它的动作。下面是添加命名空间扩展到‘我的计算机’节点的例子:
HKEY_LOCAL_MACHINE
\Software
\Microsoft
\Windows
\CurrentVersion
\Explorer
\MyComputer
\NameSpace
\{CLSID}
{CLSID}的默认值应该指向你希望探测器显示的串。其它可以依附的命名空间是:
桌面(Desktop)
网上邻居(NetworkNeighborhood)
Internet探测器(Internet)
通过简单地用括号里的文字置换上面路径中的‘MyComputer’条目,你就可以移动你的扩展到期望的命名空间。
命名空间扩展能做些什么
到目前为止,我们讨论了命名空间扩展的体系结构,我们还揭示了它的工作过程,以及一些基本概念。我们也接触了安装方面的热点课题。现在我们开始考虑这项技术的实际应用。如果你查看PC中的目录,你将看到许多具有定制图标的文件夹。有‘订阅’,‘下载程序文件’,‘临时Internat文件’,‘任务调度’,‘通道’,‘软件更新’… 等等。基本上,每当有某些信息可以逻辑上表示为嵌套文件夹形式显示时,Windows都给出命名空间扩展。这些要唤醒的信息必须收集到单一的主文件夹之中,并且必须关联到系统。如果是基于文件的就更好了。
这正是关于命名空间扩展的一种思考方式。另一个则集中于应用。你可以考虑在Shell中建立你自己的文件夹,并为团队的每一个应用保留一个子文件夹。在这些子文件夹中,你可以驻留整个应用,或关于它的信息,或(更简单)只是一个Internet快捷方式。
设计我们的命名空间扩展
下面这几点概述了这一章的前一部分的内容,我们需要对一定数量的问题给出一致的回答。我们想要加一个节点到探测器,使我们能够层次浏览当前打开的窗口。如果你了解Spy++ 这个随VC++ 给出的实用程序,很容易想象我们所瞄准的目标:基本上我们想要在Windows Shell上实现大多数Spy++的功能。通过扩展,如窗口观察节点,我们希望获得所有顶层窗口的完整列表。通过展开这些节点,我们能够找出上层窗口的所有关联窗口。
我们需要确定的是:
是否这个应用包含了‘文件夹’的概念
怎样建造PIDL
怎样枚举项
怎样表示信息给用户
要提供哪些附加的功能
在这一章的剩余部分,我们将致力于解决这些问题。
此时的文件夹是什么
在窗口层次中,我们正好有一种项:窗口。这不同于观察注册表的命名空间扩展的情形。那时的文件夹是注册表的键,而项是注册表键的值。
窗口观察扩展仅仅由窗口组成。如果给定窗口有子窗口,则它可以被考虑为文件夹。什么是子文件夹呢?子文件夹是一个具有父窗口的窗口,并且至少有一个子窗口。
设计定制的PIDL
在命名空间扩展开发阶段设计PIDL是非常重要的。在这里的特殊情况下,我们是相当幸运的。我们不需要连接数据部件来获得窗口的唯一标识,因为我们总能得到HWND。我们将直接使用HWND作为我们的PIDL,而且这是绝对可靠的,我们能清晰地识别任何文件夹项。
怎样构建Windows枚举器
命名空间扩展的另一个中心课题是枚举器对象,它有返回文件夹或子文件夹包含的各个项的任务。我们还是幸运的,因为窗口是一个系统部件,SDK对它提供了大量的支持。要枚举窗口,我们只需要调用EnumChildWindows(),并且把结果保存在自己的数据结构中即可。
设计观察
对于程序员,关于窗口的关键信息是它的HWND,它的标题,以及窗口类名。因此我们建立的这个观察应该允许你同时看到所有这些信息。一个报告型列表观察的显示是最好的选择,我们给出四个列:
指示是否窗口有子窗口
窗口的HWND
窗口所属的类名字
窗口的当前标题
为了方便观察,我们采用不同的图标来反映窗口是否有子窗口——这是区别文件夹窗口和项窗口的一种方法。进一步加入一些排序能力是有帮助的。
我们还想要文件夹通过关联菜单提供关于窗口的信息,这将要求我们实现IContextMenu接口。
实现我们的命名空间扩展
为了建立这个命名空间扩展,我们使用微软提供的‘注册表观察’扩展作为基础代码结构。注册表观察扩展显示如图:
这个关于RegView例子源码添加一个新文件夹到探测器,是与Internet 客户端SDK已同发布的,在VC++ 上也有。你可以在Samples\SDK Samples\Windows UI Samples\Shell Samples\RegView下找到它。我们建议你找出这段代码,以便能够更好的理解这里的讨论。
注册表观察与Windows观察的共有属性
前面我们提到使用微软的示例源代码作为基础代码是因为它实际是十分现实的例子。它向你展示:
怎样设计一个将自己插入到探测器命名空间的相当复杂的命名空间扩展
怎样编码和管理PIDL,以及把实际数据嵌入其中
怎样处理和组织子文件夹
怎样添加附属特征如修改菜单和不同的图标
怎样把它放到桌面上,并保存附加的和有帮助的信息到注册表
我们维护这个代码结构,并试图使它与另一种数据类型,不同的特征一起工作。这个例子采用纯C++代码,所以我们维护它。所有COM环境初始也是纯C++的。类似地我们也保持PIDL的管理代码——在单一管理类中封装每一件东西是一个较好的选择。我们只改变数据格式,并且采用某些类成员。
两个扩展(微软的和我们的)都采用了列表观察作为窗口显示文件夹内容然而我们的更简单一点。它不支持多重观察(大图标,小图标,和列表),而仅支持报告观察。相反,它提供排序能力和某些增强的列表观察用户界面(全行选择,自动跟踪,列拖拽)。
另外,两个扩展都改变了探测器菜单并支持不同项显示不同图标。此外,窗口观察实现了左右两个窗框的关联菜单。谈到用户界面特征,我们应该添加我们的,从公共对话框可见的属性,以及当桌面上鼠标在其上盘旋时提示的工具标签。
除了维护C++框架和组织PIDL相关代码之外,其余的——也是最大一部分代码——有一个标准的形式。它所实现的活动不能明显以根本不同的方式完成,因此,在编写命名空间扩展时它适合作为一个模块。
窗口观察项目
这个项目由下面主要的类组成,所有这些类都在注册表观察项目中存在:
类 |
接口 |
描述 |
CShellFolder |
IShellFolder, IPersistFolder |
定义文件夹管理器的行为,这是一个实现桥接探测器与扩展的模块 |
CEnumIDList |
IEnumIDList |
枚举在观察部分的窗口 |
CShellView |
IShellView |
提供一个占据探测器右窗框的观察 |
CExtractIcon |
IExtractIcon |
返回探测器使用的图标 |
CContextMenu |
IContextMenu |
返回探测器为关联菜单使用的菜单项 |
除了这些实现COM接口所必需的类以外,我们的项目还包含另一个重要的类,这个类提供管理PIDL的主要函数:CPidlMgr。
PIDL管理类
我们讨论过,这个命名空间扩展使用窗口Handle作为PIDLs,但是我们仍然需要代码层来封装这个HWND,以及提供编程接口来遵守PIDL规格和Shell的期望进行协调。
为了完成设计PIDL的所有任务,我们需要定义一个PIDL管理类。任何需要处理PIDL的类都将建立这个对象的实例。
#ifndef PIDLMGR_H
#define PIDLMGR_H
#include
#include
// PIDL的数据结构
struct PIDLDATA
{
// 如果要向后兼容,在这里添加签名和版本号,对于你这是实际结果,好药添加其它数据,要求标识
//你的文件夹元素。
HWND hwnd;
};
typedef PIDLDATA* LPPIDLDATA;
extern HINSTANCE g_hInst;
extern UINT g_DllRefCount;
/*---------------------------------------------------------------*/
// CPidlMgr class definition
/*---------------------------------------------------------------*/
class CPidlMgr
{
public:
CPidlMgr();
~CPidlMgr();
LPITEMIDLIST Create(HWND);
LPITEMIDLIST Copy(LPCITEMIDLIST);
void Delete(LPITEMIDLIST);
UINT GetSize(LPCITEMIDLIST);
LPITEMIDLIST GetNextItem(LPCITEMIDLIST);
LPITEMIDLIST GetLastItem(LPCITEMIDLIST);
BOOL HasChildren(HWND);
BOOL HasChildrenOfChildren(HWND);
HWND GetData(LPCITEMIDLIST);
DWORD GetPidlPath(LPCITEMIDLIST, LPTSTR);
private:
LPMALLOC m_pMalloc;
HWND GetDataPointer(LPCITEMIDLIST);
static BOOL CALLBACK WindowHasChildren(HWND, LPARAM);
};
typedef CPidlMgr* LPPIDLMGR;
#endif // PIDLMGR_H
CPidlMgr类定义了建立,删除,拷贝,和浏览PIDL的方法,进一步,这个类还包含数据成员来存储对由SHGetMalloc()返回的Shell分配器对象的引用。这个类中的关键函数是:
Create(), 构建一个新的PIDL
HasChildren(), 确定是否窗口是一个文件夹
GetData(), 分解PIDL,以抽取有用的信息
GetPidlPath(), 返回PIDL的显示名
其它大部分函数相对于上面四个则是次要的。下面我们将更详细的检测这些方法。
建立PIDL
用HWND联系PIDL并不是说我们可以在任何需要PIDL的地方简单的使用HWND。PIDL是一个结构,必须输出标准接口使探测器能够浏览它,无论它内部包含什么。因而,HWND只是PIDL的内容,最终建立的PIDL则是建立封装这个Handle的包装。正如我们早期解释的,PIDL是一个指向SHITEMID变量列表的指针。一开始我们就定义了一个PIDLDATA数据结构,这是由SHITEMID结构的abID成员指向的东西:
struct PIDLDATA
{
HWND hwnd;
};
typedef PIDLDATA* LPPIDLDATA;
这是一般的处理方法:建立客户结构并用需要在实际包含它的文件夹内标识项的任何数据充填。对于单个PIDL,没有必要是全程唯一的,象HWND那样。重要的是在子文件夹内的每一个PIDL是唯一的。全程唯一性通过连接各个PIDLs从桌面到这个项形成路径来达到。换句话说,这个工作十分类似于文件和目录的工作。可以有两个文件具有相同的名字在不同的目录下,或具有相同的路径在不同的驱动器上,此时仍能确定它们为不同的对象。
在这个特殊的例子中,我们不需要使用更多的HWND来识别文件夹项。在其它情况下,你可能需要更多的信息,然而,只需要在PIDLDATA结构中加入新的字段即可。
在建立新PIDL时,你必须通过IMalloc接口分配足够的内存。这使 Shell 释放这个内存成为可能。IMalloc接口是由SHGetMalloc()返回的,这个调用在CPidlMgr类的构造中进行。实际要分配的存储量必须等于PIDL本身的尺寸加上一个空结构的尺寸。下面是正确的尺寸定义:
USHORT uSize = sizeof(ITEMIDLIST) + sizeof(PIDLDATA);
正如我们所看到地,它是由ITEMIDLIST尺寸加上表示项的数据尺寸给出的。此外还不能忘了最后的NULL,ITEMIDLIST使Shell知道这是一个完整的链表:
LPITEMIDLIST CPidlMgr::Create(HWND hwnd)
{
// PIDL的完整尺寸,包括 SHITEMID
USHORT uSize = sizeof(ITEMIDLIST) + sizeof(PIDLDATA);
// 分配存储还要包括最后的空结构ITEMIDLIST,注意必须使用 IMalloc 。
LPITEMIDLIST pidlOut = reinterpret_cast
m_pMalloc->Alloc(uSize + sizeof(ITEMIDLIST)));
if(pidlOut)
{
LPITEMIDLIST pidlTemp = pidlOut;
// 准备用实际数据充填这个PIDL
pidlTemp->mkid.cb = uSize;
LPPIDLDATA pData = reinterpret_cast
// 充填 PIDL
pData->hwnd = hwnd;
//
// 添加更多的字段如果需要...
//
// 一个有0尺寸的PIDL是这个链的终止块
pidlTemp = GetNextItem(pidlTemp);
pidlTemp->mkid.cb = 0;
pidlTemp->mkid.abID[0] = 0;
}
return pidlOut;
}
PIDL必须是平面顺序字节,也就是说,你不能使用指针。如果试图这样做,指针将被看作为32位数字,并且引用的地址将丢失。
从PIDL中抽取信息
所有Shell API函数都使用PIDLs,而且每一个项或文件夹都以PIDLs的形式来引用。然而,总是能从PIDL中抽取你实际需要处理的信息。在这种情况下,你需要知道HWND,和是否这个窗口有子窗口。
BOOL CPidlMgr::HasChildren(HWND hWnd)
{
// 确定窗口是否有子窗口
HWND h = GetWindow(hWnd, GW_CHILD);
return (h != NULL);
}
HWND CPidlMgr::GetData(LPCITEMIDLIST pidl)
{
if(!pidl)
return NULL;
// 取得PIDL的最后项,以确保在多重嵌套下获得正确的HWND
LPITEMIDLIST p = GetLastItem(pidl);
LPPIDLDATA pData = reinterpret_cast
return pData->hwnd;
}
我们知道,PIDL是一个指向由两个成员组成的结构的指针,头一个成员表示后面成员的尺寸,因此,探测器知道正确遍历这个链表时它需要跳过的字节数。同样,命名空间扩展也可以使用这个字节数,和适当地解释这个结构。为了简单起见,PIDL管理器应该定义一些函数来支持PIDL的‘遍历’操作:
LPITEMIDLIST CPidlMgr::GetNextItem(LPCITEMIDLIST pidl)
{
if(pidl)
return reinterpret_cast
const_cast
else
return NULL;
}
LPITEMIDLIST CPidlMgr::GetLastItem(LPCITEMIDLIST pidl)
{
LPITEMIDLIST pidlLast = NULL;
// 取得链表中最后一项PIDL
if(pidl)
{
while(pidl->mkid.cb)
{
pidlLast = const_cast
pidl = GetNextItem(pidl);
}
}
return pidlLast;
}
PIDL管理器类的另一个任务是提供对象的显示名。PIDL只是一个二进制字节序列,因此,在Shell需要的时候,它将请求文件夹管理器(实现IShellFolder的对象)提供每一个项的显示名。因而,在某个地方(如果不是在PIDL管理器类中)应该有一段代码能够接受PIDL,并返回一个显示名串。一般来讲,这个函数应该遍历引用的PIDLs链表,建立一个增长的串。如果你浏览进入文件夹和子文件夹,则最内部的项带有的PIDL包含所有父文件夹。此后,你应该提供整个串。(在文件和目录情况下,路径名作为显示名)。其想法是要你来确定对于单个PIDL应该显示什么,以及在遍历这个联系各种块的链之后是用逗号,分号,斜线还是其它什么来分隔它们。
DWORD CPidlMgr::GetPidlPath(LPCITEMIDLIST pidl, LPTSTR lpszOut)
{
HWND hwnd = GetData(pidl);
TCHAR szClass[100], szTitle[100];
GetWindowText(hwnd, szTitle, 100);
GetClassName(hwnd, szClass, 100);
// 加一个描述到桌面窗口(类 "#32769")
if(!lstrcmpi(szClass, __TEXT("#32769")))
lstrcpy(szClass, __TEXT("Desktop"));
// 以 "标题 [类]"形式返回串
if(lstrlen(szTitle))
wsprintf(lpszOut, __TEXT("%s [%s]"), szTitle, szClass);
else
wsprintf(lpszOut, __TEXT("[%s]"), szClass);
// 返回串尺寸
return lstrlen(lpszOut);
}
正如上面代码所示,在这种情况下操作是容易的,因为返回的是当前窗口的名,我们不需要考虑到达它所穿过的窗口列表,只需找到HWND,并返回适当的信息即可。然而传递给我们的是一个PIDL,所以首先要做的是用GetData()转换成窗口,然后返回包含这个窗口标题和类名的格式化串。这个串在每次窗口被选中时都显示在地址栏和状态条中。GetPidlPath()方法由文件夹管理器在GetDisplayNameOf()方法中调用。
在我们的命名空间扩展中所有类都需要处理PIDLs,所以,每一个都将包含LPITEMIDLIST类型的数据成员和PIDL管理器实例。
Windows枚举器
现在让我们调查一下枚举器的主要特征。这是一个从IEnumIDList导出的类,它提供对在这个类建立以后系统所有打开的窗口的访问操作。为了运行,它内部定义了PIDLs链表并使用它来存储所有文件夹项。在类构造器被调用时,所有在作为变量传递过来的文件夹内的项都被枚举并添加到这个链表中。而后这个接口的方法Next()在这个链表上工作。无论实现细节如何,这都是实现IEnumIDList接口的类的一般行为。
CEnumIDList::CEnumIDList(HWND hwnd, DWORD dwFlags, HRESULT* pResult)
{
if(pResult)
*pResult = S_OK;
m_pFirst = NULL;
m_pLast = NULL;
m_pCurrent = NULL;
// 建立 PIDL 管理器
m_pPidlMgr = new CPidlMgr();
if(!m_pPidlMgr)
{
if(pResult)
*pResult = E_OUTOFMEMORY;
delete this;
return;
}
// 取得Shell内存管理器
if(FAILED(SHGetMalloc(&m_pMalloc)))
{
if(pResult)
*pResult = E_OUTOFMEMORY;
delete this;
return;
}
// 建立项目链表
if(!CreateEnumList(hwnd, dwFlags))
{
if(pResult)
*pResult = E_OUTOFMEMORY;
delete this;
return;
}
m_ObjRefCount = 1;
g_DllRefCount++;
}
上面清单是这个类的构造器代码,它接受一个要枚举的窗口Handle和要考虑的标志。这个标志是由Shell在调用IShellFolder::EnumObjects()时指定的,我们在这一章的前面已经解释过了。
在CreateEnumList()方法中,链表通过使用EnumChildWindows() SDK函数枚举窗口而建立。如果基窗口为NULL,说明要枚举最顶层窗口,这是桌面窗口。这一层上没有其它窗口,因此不需要枚举,只需简单地通过GetDesktopWindow()获得桌面窗口的Handle即可,并添加新项到链表中。在其它场合我们需要枚举子窗口和在必要时建立新项:
typedef struct tagENUMWND {LPARAM lParam;
HWND hwndParent;
DWORD dwFlags;
} ENUMWND, FAR* LPENUMWND;
BOOL CEnumIDList::CreateEnumList(HWND hWndRoot, DWORD dwFlags)
{
// 取得桌面窗口
if(hWndRoot == NULL)
{
//如果我们必须考虑根窗口的话(桌面),我们不需要枚举任何东西,只要获得桌面的HWND,
//并添加新元素到链表中即可,这就是NewEnumItem()要做的。
hWndRoot = GetDesktopWindow();
NewEnumItem(hWndRoot);
return TRUE;
}
// 枚举指定窗口的子窗口
ENUMWND ew;
ew.lParam = reinterpret_cast
ew.hwndParent = hWndRoot;
ew.dwFlags = dwFlags;
//我们需要仅考虑制定窗口的直接子窗口函数,对子窗口的子窗口不感兴趣。在
//回调代码中我们将修改EnumChildWindows的行为。
numChildWindows(hWndRoot, AddToEnumList, reinterpret_cast
return TRUE;
}
BOOL CALLBACK CEnumIDList::AddToEnumList(HWND hwndChild, LPARAM lParam)
{
LPENUMWND lpew = reinterpret_cast
//避开不是指定窗口的子窗口的窗口,这就是要跳过那些不是指定窗口的直接子窗口
//的窗口。这个检查是由EnumChildWindows()的枚举子窗口和孙窗口属性给出的,
//我们避开孙窗口。
HWND h = GetParent(hwndChild);
if((h != NULL) && (h != lpew->hwndParent))
return TRUE;
// 保存lParam 变量中的指针
CEnumIDList* pEnumIDList = reinterpret_cast
// 重点: 在这里我们确定什么是一个文件夹和什么是它的叶子,对于窗口,看它是否有子窗口
//探测器希望非文件夹项
if(lpew->dwFlags & SHCONTF_NONFOLDERS)
return pEnumIDList->NewEnumItem(hwndChild);
//探测器希望文件夹项
if(lpew->dwFlags & SHCONTF_FOLDERS)
{
// 如果没有子窗口,扔掉它,因为它已经被添加了。
if(!pEnumIDList->m_pPidlMgr->HasChildren(hwndChild))
return TRUE;
else
pEnumIDList->NewEnumItem(hwndChild);
}
return TRUE;
}
BOOL CEnumIDList::NewEnumItem(HWND hwndChild)
{
LPENUMLIST pNew = NULL;
pNew = reinterpret_cast
if(pNew)
{
// 为新元素建立新的PIDL
pNew->pNext = NULL;
pNew->pidl = m_pPidlMgr->Create(hwndChild);
// 者是否为列表中的第一项
if(!m_pFirst)
{
m_pFirst = pNew;
m_pCurrent = m_pFirst;
}
// 添加新项到列表尾
if(m_pLast)
m_pLast->pNext = pNew;
// 更新最后项指针
m_pLast = pNew;
return TRUE;
}
return FALSE;
}
就我们的目的而言,EnumChildWindows()函数有一个缺陷是我们应该克服的,它返回指定窗口的所有子窗口,甚至是子窗口的子窗口。而我们希望的是直接子窗口。然而EnumChildWindows()函数可以传递一个回调函数对每一个枚举的窗口进行操作。在我们的例子中这个函数是AddToEnumList(),它检查枚举窗口实际的父窗口,如果父窗口与期望的不匹配,则拒绝它。
对每一个窗口我们都需要建立PIDL,并把它加到列表中,在上面的清单中这是由NewEnumItem()辅助函数来完成的。还要注意回调函数仔细地根据Shell需求枚举窗口:仅仅是文件夹,还是文件夹和项。
取得下一项
一旦建立了列表窗口,返回其中的 n 个项到调用者就是遍历这个列表并充填一个数组。注意,我们要求建立和返回一个PIDL的新拷贝,Shell将使用和释放它们。
STDMETHODIMP CEnumIDList::Next(DWORD dwElements, LPITEMIDLIST apidl[],
LPDWORD pdwFetched)
{
DWORD dwIndex;
HRESULT hr = S_OK;
if(dwElements > 1 && !pdwFetched)
return E_INVALIDARG;
for(dwIndex = 0 ; dwIndex < dwElements ; dwIndex++)
{
// 是否为列表中的最后一个元素
if(!m_pCurrent)
{
hr = S_FALSE;
break;
}
// 拷贝 PIDLs
apidl[dwIndex] = m_pPidlMgr->Copy(m_pCurrent->pidl);
m_pCurrent = m_pCurrent->pNext;
}
// 返回取得项目数
if(pdwFetched)
*pdwFetched = dwIndex;
return hr;
}
文件夹管理器
这个与Shell联系最紧密的类是实现IShellFolder接口的类。它必须建立枚举器和观察,以及通过建立新的CShellFolder类实例绑定子文件夹。它还必须提供每一个PIDL的显示名,和指向附加接口IContextMenu 和 IExtractIcon 的指针。
BindToObject()是用于绑定子文件夹的方法。它简单地新建一个由IShellFolder接口导出的类,并传递接收的PIDL作为构造器的变量。其结果是一个新的Shell子文件夹被建立。
STDMETHODIMP CShellFolder::BindToObject(LPCITEMIDLIST pidl, LPBC pbcReserved,
REFIID riid, LPVOID* ppvOut)
{
CShellFolder* pShellFolder = new CShellFolder(this, pidl);
if(!pShellFolder)
return E_OUTOFMEMORY;
HRESULT hr = pShellFolder->QueryInterface(riid, ppvOut);
pShellFolder->Release();
return hr;
}
下面代码段显示文件夹对象是怎样建立它的观察和枚举器对象的。对于枚举器首先要抽取要枚举窗口的HWND。然后才能建立枚举器对象:
STDMETHODIMP CShellFolder::CreateViewObject(HWND hwndOwner, REFIID riid,
LPVOID* ppvOut)
{
CShellView* pShellView = new CShellView(this, m_pidl);
if(!pShellView)
return E_OUTOFMEMORY;
m_pShellView = pShellView;
HRESULT hr = pShellView->QueryInterface(riid, ppvOut);
pShellView->Release();
return hr;
}
STDMETHODIMP CShellFolder::EnumObjects(HWND hwndOwner, DWORD dwFlags,
LPENUMIDLIST* ppEnumIDList)
{
// hwndOwner 只是用于任何消息框的父窗口的 HWND
HRESULT hr;
*ppEnumIDList = NULL;
HWND hWnd = m_pPidlMgr->GetData(m_pidl);
*ppEnumIDList = new CEnumIDList(hWnd, dwFlags, &hr);
if(*ppEnumIDList == NULL)
return hr;
return S_OK;
}
对于显示名,文件夹管理器最终调用PIDL管理器类,但是在返回之前必须转换串到Unicode字符。GetDisplayNameOf()实际返回STRRET,它是一个展示三种可能类型串的一种联合类型数据结构:Unicode串,ANSI串,串的偏移(见第5章)。此时我们选择使用Unicode串:
STDMETHODIMP CShellFolder::GetDisplayNameOf(LPCITEMIDLIST pidl,
DWORD dwFlags, LPSTRRET lpName)
{
TCHAR szText[MAX_PATH] = {0};
// 取得作窗框,地址栏等的显示名
m_pPidlMgr->GetPidlPath(pidl, szText);
// 必须转换串到Unicode,所以分配宽字符串
int cchOleStr = lstrlen(szText) + 1;
lpName->pOleStr = reinterpret_cast
sizeof(WCHAR)));
if(!lpName->pOleStr)
return E_OUTOFMEMORY;
lpName->uType = STRRET_WSTR;
mbstowcs(lpName->pOleStr, szText, cchOleStr);
return S_OK;
}
然而,文件夹管理器活动的最值得关注的部分是比较项和返回项属性操作。
比较项
比较项的功能实现起来相对比较简单一点,但是它有极其重要的作用。如果不能保证它绝对正确地实现,最好的办法就是不支持它。在开发命名空间扩展的前期阶段,我们部分地实现IShellFolder::CompareIDs()方法,你必须想到有多少错误行为是我们必须克服的。更坏的情况是几乎只有很少的工作需要项的比较例程来做。仅在决定适当地安排它工作时——差不多是梦幻般地——突然开始工作。
CompareIDs()成员函数从Shell接收两个PIDLs参数,并返回表示那一个项较大的值。(非0)的正值表示第一个大于第二个,而负值表示相反,空值说明项是相等的。
从在线资料中你可能已经证实,CompareIDs()还有第三个参数,lParam,这表示使用的分类规则。习惯上,0值是指你应该通过文件夹内容的名字排序,非0值表示文件夹专有规则。在这个应用中,当用户点击列表观察顶部的列标题控件时,CompareIDs()方法获得调用。(这是探测器用户界面的典型行为)。我们在Shell的观察代码中处理这个事件。所以完全由我们来决定通过lParam传递什么值。
在这个实现中,我们决定忽略lParam的选择,代之使用CShellFolder类的数据成员m_uSortField来表示怎样排序内容。理由是我们需要保持当前排序字段的踪迹。因此这个数据成员无论如何是需要的。更进一步,我们希望能升序或降序排列——如果一个列已经被分类,则再次点击将反序。下面清单显示我们是怎样做的:
STDMETHODIMP CShellFolder::CompareIDs(LPARAM lParam, LPCITEMIDLIST pidl1,
LPCITEMIDLIST pidl2)
{
//这个函数总是用0设置lParam来调用。习惯上这是用名字分类,非0值表示特殊的文件夹分类规则
//注意,这里我们使用m_uSortField数据成员作为对lParam的替换。
HWND hwnd1 = m_pPidlMgr->GetData(pidl1);
HWND hwnd2 = m_pPidlMgr->GetData(pidl2);
// 由子文件夹分类 CHILDREN
if(m_uSortField == 1 || m_uSortField == -1)
{
int fChildren1 = m_pPidlMgr->HasChildren(hwnd1);
int fChildren2 = m_pPidlMgr->HasChildren(hwnd2);
if(fChildren1 < fChildren2)
return m_uSortField;
else if(fChildren1 > fChildren2)
return m_uSortField * -1;
else
return 0;
}
// 按类分类 CLASS
if(m_uSortField == 2 || m_uSortField == -2)
{
// BUFSIZE 是符号内容尺寸,设置为100
TCHAR szClass1[BUFSIZE];
TCHAR szClass2[BUFSIZE];
GetClassName(hwnd1, szClass1, BUFSIZE);
GetClassName(hwnd2, szClass2, BUFSIZE);
return m_uSortField * lstrcmpi(szClass1, szClass2);
}
// 按标题分类 TITLE
if(m_uSortField == 4 || m_uSortField == -4)
{
TCHAR szTitle1[BUFSIZE];
TCHAR szTitle2[BUFSIZE];
GetWindowText(hwnd1, szTitle1, BUFSIZE);
GetWindowText(hwnd2, szTitle2, BUFSIZE);
return m_uSortField * lstrcmpi(szTitle1, szTitle2);
}
// 按Handle分类 HWND
if(hwnd1 < hwnd2)
return m_uSortField;
else if(hwnd1 > hwnd2)
return m_uSortField * -1;
else
return 0;
}
这个函数使我们可以用四种方法对观察中的项目进行分类:用子窗口,用类名,用Handle和用标题。更进一步,通过使m_uSortField值为正或负,以及乘以比较结果,可以转换分类顺序。依据问题中的比较逻辑,我们或者比较HWND,或者借助lstrcmpi()来比较串。
文件夹属性
要使命名空间扩展在探测器内正常工作,返回正确的文件夹属性是至关重要的。例如,在打开主节点时,你希望看到它包含的所有文件夹。在窗口的概念下,当你展开相对于桌面的窗口节点时,你应该看到所有顶层窗口和所有没有父窗口的窗口,此外没有其它的东西。而后,在这些二级窗口上点击时,希望看到窗口的所有子窗口。
GetAttributesOf()方法期望对一组由PIDLs指定的项返回正确的特征。为了指派特征,使用SFGAO_XXX常量,这是我们在第4章所看到的。记住,可以同时有多个PIDLs传递给你,要求它们的多个特征。
STDMETHODIMP CShellFolder::GetAttributesOf(UINT uCount, LPCITEMIDLIST aPidls[],
LPDWORD pdwAttribs)
{
*pdwAttribs = -1;
for(UINT i = 0 ; i < uCount ; i++)
{
DWORD dwAttribs = 0;
// 这个项是一个窗口吗?
HWND hwnd = m_pPidlMgr->GetData(aPidls[i]);
if(IsWindow(hwnd))
{
//这里以普通的方式分配你想要这个项所具有的风格,当然最终是你来适当地管理它们。
if(m_pPidlMgr->HasChildren(hwnd))
{
dwAttribs |= SFGAO_FOLDER;
if(m_pPidlMgr->HasChildrenOfChildren(hwnd))
dwAttribs |= SFGAO_HASSUBFOLDER;
}
}
*pdwAttribs = dwAttribs;
}
return S_OK;
}
GetAttributesOf()函数接收PIDLs列表以恢复其特征。我们说每一个有子窗口的窗口是文件夹,所以很容易确定什么时候需要SFGAO_FOLDER属性。如果我们在这一点上停止,Shell就会漏掉使这个节点可展开的特征,这使它不可能浏览超过一层。因此,我们需要表示不仅能指定节点是否有子节点,还要给出是否这些子节点有它自己的子节点。这个问题要回答是,可以使用SFGAO_HASSUBFOLDER标志。
回到PIDL管理类,HasChildrenOfChildren()函数简单地枚举子窗口并检查其中是否有子窗口:
BOOL CPidlMgr::HasChildrenOfChildren(HWND hWnd)
{
// 确定是否窗口有子窗口
BOOL b = FALSE;
EnumChildWindows(hWnd, WindowHasChildren, reinterpret_cast
return b;
}
BOOL CPidlMgr::WindowHasChildren(HWND hwnd, LPARAM lParam)
{
BOOL* pB = reinterpret_cast
// 如果这个窗口是一个子窗口,返回的HWND不是NULL
HWND h = GetWindow(hwnd, GW_CHILD);
*pB = (h != NULL); // TRUE 如果至少有一个孙窗口存在
return(h == NULL); // 需要返回FALSE 来终止枚举
}
这段代码有几个重点,要检查给定窗口是否有孙窗口,必须枚举所有子窗口。EnumChildWindows()是做这个工作的API函数。与任何其它枚举一样,从第一个窗口开始,并终止于最后一个窗口。每一个被找到的窗口为进一步处理都被传递给指定的回调函数,在此时是WindowHasChildren()函数。
这是因为我们需要知道是否至少有一个孙窗口,所以只要发现一个,我们就通过在回调函数中返回FALSE停止枚举。反过来我们还需要通知调用函数HasChildrenOfChildren(),枚举由于找到孙窗口而完成。传递给EnumChildWindows()的布尔值就用于这个结果的缓冲:
EnumChildWindows(hWnd, WindowHasChildren, reinterpret_cast
较简单的方法是每次设置SFGAO_FOLDER时都设置SFGAO_HASSUBFOLDER标志,这可以有效地工作,但有一个小bug:所有节点都将显示可展开,无论它是否有子文件夹。通过适当的操作,我们得到如图所示的树观察:
窗口观察
本质上,观察是一个窗口。正常情况,你建立两个窗口:一个父窗口包含一个子窗口。这个子窗口就是用户看到的和与之交互的窗口。我们在这里建立的观察将是一个客户类窗口以避免子类化。另一项可以使用的技术要求用一些要素控件定义非模式对话框。
在这种情况下,子窗口是一个列表观察,之所以这样选择是想要使它看上去象传统的文件夹观察。它小心的响应某些来自系统的消息,包括WM_CREATE,WM_SIZE,WM_SETFOCUS,和 WM_NOTIFY这些重要消息。注意,在你建立实现这个观察的类时,WndProc()(观察窗口的窗口过程)必须是静态成员。当然,静态成员不能获得this指针,因而,它不能访问其它类成员。所以我们需要找到其它办法来把this指针传递给WndProc()代码。我们的方案是在IShellView::CreateViewWindow()中建立观察窗口时,传递this作为CreateWindowEx()的最后一个变量:
hWnd = CreateWindowEx(0, NS_CLASS_NAME, NULL,
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS,
prcView->left, prcView->top,
prcView->right - prcView->left,
prcView->bottom - prcView->top,
m_hwndParent, NULL, g_hInst, this);
这使得this指针通过WM_NCCREATE消息传递给窗口过程。然而在类的静态成员中访问this还不够:我们还需要使它永久地保持,以便完成我们需要窗口过程产生的各个调用。解决这个问题的典型方案是把这个指针保存到窗口的额外子节——与每一个窗口关联的,完全由程序员支配的32位缓冲,下面代码说明了应该怎样设置它:
SetWindowLong(hWnd, GWL_USERDATA, reinterpret_cast
下面一行说明怎样读出它:
CShellView* pThis = reinterpret_cast
GWL_USERDATA));
有了pThis指针,我们就可以从不是类成员的过程内部调用类的所有公共成员。(静态类成员从语法角度看属于类,但是它实际是一个依附于类的全程函数)。下面是观察窗口过程的代码:
LRESULT CALLBACK CShellView::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam,
LPARAM lParam)
{
CShellView* pThis = reinterpret_cast
GWL_USERDATA));
switch(uMsg)
{
case WM_NCCREATE:
{
LPCREATESTRUCT lpcs = reinterpret_cast
pThis = reinterpret_cast
SetWindowLong(hWnd, GWL_USERDATA, reinterpret_cast
pThis->m_hWnd = hWnd;
}
break;
case WM_CONTEXTMENU:
return pThis->OnContextMenu();
case WM_MENUSELECT:
return pThis->OnMenuSelect(LOWORD(wParam));
case WM_SIZE:
return pThis->OnSize(LOWORD(lParam), HIWORD(lParam));
case WM_CREATE:
return pThis->OnCreate();
case WM_SETFOCUS:
return pThis->OnSetFocus();
case WM_KILLFOCUS:
return pThis->OnKillFocus();
case WM_ACTIVATE:
return pThis->OnActivate(SVUIA_ACTIVATE_FOCUS);
case WM_COMMAND:
return pThis->OnCommand(GET_WM_COMMAND_ID(wParam, lParam),
GET_WM_COMMAND_CMD(wParam, lParam),
GET_WM_COMMAND_HWND(wParam, lParam));
case WM_NOTIFY:
return pThis->OnNotify(wParam, reinterpret_cast
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
为了保证选中项与菜单和工具条状态之间有完美的结合,处理WM_SETFOCUS 和 WM_KILLFOCUS是特别重要的:
LRESULT CShellView::OnSetFocus()
{
// 告诉浏览器我们有焦点
m_pShellBrowser->OnViewWindowActive(this);
OnActivate(SVUIA_ACTIVATE_FOCUS);
return 0;
}
// OnKillFocus
LRESULT CShellView::OnKillFocus()
{
OnActivate(SVUIA_ACTIVATE_NOFOCUS);
return 0;
}
我们在WM_CREATE消息期间建立这个列表观察,并在WM_SIZE期间调整它的尺寸以便使观察覆盖整个可用的区域。
列表观察风格
在响应WM_CREATE时列表观察的创建要设置下列风格:
WS_TABSTOP WS_VISIBLE WS_CHILD WS_BORDER
LVS_SINGLESEL LVS_REPORT LVS_SHAREIMAGELISTS
此外,由于要求Windows95作为最小平台,还需要加入一些新的列表观察风格。这些必须通过新的宏进行设置的风格称为扩展风格。
BOOL CShellView::CreateList()
{
DWORD dwStyle = WS_TABSTOP | WS_VISIBLE | WS_CHILD | WS_BORDER |
LVS_SINGLESEL | LVS_REPORT | LVS_SHAREIMAGELISTS;
// 建立列表观察
m_hwndList = CreateWindowEx(WS_EX_CLIENTEDGE, WC_LISTVIEW, NULL, dwStyle,
0, 0, 0, 0, m_hWnd,reinterpret_cast
if(!m_hwndList)
return FALSE;
// 设置扩展风格
DWORD dwExStyle = LVS_EX_TRACKSELECT | LVS_EX_UNDERLINEHOT |
LVS_EX_FULLROWSELECT | LVS_EX_HEADERDRAGDROP;
ListView_SetExtendedListViewStyle(m_hwndList, dwExStyle);
return TRUE;
}
由这些标志设置的风格说明现在使用鼠标选择是整行选择(不是只选择头一项),可以通过拖拽移动列。下面是在列表观察建立后运行的源代码。它定义了四个列,并使用项填写这个列表:
BOOL CShellView::InitList()
{
TCHAR szString[MAX_PATH] = {0};
// 清空列表观察
ListView_DeleteAllItems(m_hwndList);
// 初始化列
LV_COLUMN lvColumn;
lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
lvColumn.fmt = LVCFMT_LEFT;
lvColumn.pszText = szString;
lvColumn.cx = g_nColumn1;
LoadString(g_hInst, IDS_COLUMN1, szString, MAX_PATH);
ListView_InsertColumn(m_hwndList, 0, &lvColumn);
RECT rc;
GetClientRect(m_hWnd, &rc);
lvColumn.cx = g_nColumn2;
LoadString(g_hInst, IDS_COLUMN2, szString, MAX_PATH);
ListView_InsertColumn(m_hwndList, 1, &lvColumn);
lvColumn.cx = g_nColumn3;
LoadString(g_hInst, IDS_COLUMN3, szString, MAX_PATH);
ListView_InsertColumn(m_hwndList, 2, &lvColumn);
lvColumn.cx = g_nColumn4;
LoadString(g_hInst, IDS_COLUMN4, szString, MAX_PATH);
ListView_InsertColumn(m_hwndList, 3, &lvColumn);
ListView_SetImageList(m_hwndList, g_himlSmall, LVSIL_SMALL);
return TRUE;
}
这个列表观察然后用从文件夹的枚举器中使用其Next()方法获得的信息进行填充。枚举器通过观察的m_pSFParent元素,一个指向IShellFolder的指针来构建,这在其构造时就被取得并存储了。
void CShellView::FillList()
{
LPENUMIDLIST pEnumIDList = NULL;
//获得文件夹的枚举器对象。通过CShellView构造器接收的指针调用EnumObjects()
HRESULT hr = m_pSFParent->EnumObjects(m_hWnd,
SHCONTF_NONFOLDERS | SHCONTF_FOLDERS, &pEnumIDList);
if(SUCCEEDED(hr))
{
LPITEMIDLIST pidl = NULL;
// 停止重绘以避免抖动
SendMessage(m_hwndList, WM_SETREDRAW, FALSE, 0);
// 添加项
DWORD dwFetched;
while((pEnumIDList->Next(1, &pidl, &dwFetched) == S_OK) && dwFetched)
{
LV_ITEM lvi;
ZeroMemory(&lvi, sizeof(LV_ITEM));
lvi.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM;
lvi.iItem = ListView_GetItemCount(m_hwndList);
// 存储PIDL 到 HWND, 使用项的lParam成员
HWND h = m_pPidlMgr->GetData(pidl);
lvi.lParam = reinterpret_cast
// 列 1: 状态 (也设置图像)
TCHAR szState[30] = {0};
if(m_pPidlMgr->HasChildren(h))
{
lvi.iImage = 0;
LoadString(g_hInst, IDS_CHILDREN, szState, 30);
}else{
lvi.iImage = 1;
LoadString(g_hInst, IDS_NOCHILDREN, szState, 30);
}
lvi.pszText = szState;
// 添加项
int i = ListView_InsertItem(m_hwndList, &lvi);
// 填写子项 2: HWND
TCHAR szBuf[MAX_PATH] = {0};
wsprintf(szBuf, __TEXT("0x%04X"), h);
ListView_SetItemText(m_hwndList, i, 2, szBuf);
// 填写子项 3: 标题
GetWindowText(h, szBuf, MAX_PATH);
ListView_SetItemText(m_hwndList, i, 3, szBuf);
// 填写子项 1: 类
GetClassName(h, szBuf, MAX_PATH);
ListView_SetItemText(m_hwndList, i, 1, szBuf);
}
// 初始用 HWND 分类项
ListView_SortItems(m_hwndList, CompareItems,
reinterpret_cast
// 重绘列表观察
SendMessage(m_hwndList, WM_SETREDRAW, TRUE, 0);
InvalidateRect(m_hwndList, NULL, TRUE);
UpdateWindow(m_hwndList);
pEnumIDList->Release();
}
}
注意,在列表观察中我们保存了项的PIDL的拷贝,并把它存储在项的lparam成员中,如果觉得奇怪为什么需要这样,因为我们后面将用到它。
列分类
当鼠标在列表观察的标题上点击时,我们希望引起观察中的项用IShellFolder::CompareIDs()方法在后台分类。在任何列上分类都是可能的,而实际安排这样做的则是我们的另一个任务。首先,我们需要感觉用户在列上的点击,这是容易的:通过WM_NOTIFY消息这个事件通知到观察。我们可以在主窗口过程中解释这个事件,并使用下面代码处理它.
LRESULT CShellView::OnNotify(UINT CtlID, LPNMHDR lpnmh)
{
switch(lpnmh->code)
{
case NM_SETFOCUS:
OnSetFocus();
break;
case NM_KILLFOCUS:
OnDeactivate();
break;
case HDN_ENDTRACK:
g_nColumn1 = ListView_GetColumnWidth(m_hwndList, 0);
g_nColumn2 = ListView_GetColumnWidth(m_hwndList, 1);
g_nColumn3 = ListView_GetColumnWidth(m_hwndList, 2);
g_nColumn4 = ListView_GetColumnWidth(m_hwndList, 3);
return 0;
case HDN_ITEMCLICK:
{
NMHEADER* pNMH = reinterpret_cast
// 需要保留顺序码?
if(m_pSFParent->m_uSortField == 1 + pNMH->iItem)
m_pSFParent->m_uSortField = (-1) * (1 + pNMH->iItem);
else
m_pSFParent->m_uSortField = 1 + pNMH->iItem;
ListView_SortItems(m_hwndList, CompareItems,
reinterpret_cast
}
return 0;
case LVN_ITEMACTIVATE:
{
LV_ITEM lvItem;
ZeroMemory(&lvItem, sizeof(LV_ITEM));
lvItem.mask = LVIF_PARAM;
LPNMLISTVIEW lpnmlv = reinterpret_cast
lvItem.iItem = lpnmlv->iItem;
ListView_GetItem(m_hwndList, &lvItem);
m_pShellBrowser->BrowseObject(
reinterpret_cast
SBSP_DEFBROWSER | SBSP_RELATIVE);
return 0;
}
}
return 0;
}
WM_NOTIFY消息主要用于处理列的尺寸变化(HDN_ENDTRACK),列标题点击(HDN_ITEMCLICK),和双击(或单击,如果设置了文件夹的Web风格选项)单个项(LVN_ITEMACTIVATE)。要分类特定字段,我们只需点击标题即可。系统传递来的是从0开始的列索引。如果这个值(pNMH->iItem)与m_uSortField值一致(即,一个从一开始的列索引,是当前分类列),则保留排序,在我们的代码中简单地是用-1乘以 m_uSortField值。与IShellFolder::CompareIDs()方法组合将导致我们期望的结果。
if(m_pSFParent->m_uSortField == 1 + pNMH->iItem)
m_pSFParent->m_uSortField = (-1) * (1 + pNMH->iItem);
else
m_pSFParent->m_uSortField = 1 + pNMH->iItem;
要实际分类列表观察,必须调用
ListView_SortItems(m_hwndList, CompareItems,
reinterpret_cast
这里CompareItems()是用户定义的全程函数,它最终调用IShellFolder::CompareIDs():
int CALLBACK CompareItems(LPARAM lParam1, LPARAM lParam2, LPARAM lpData)
{
CShellFolder* pFolder = reinterpret_cast
if(!pFolder)
return 0;
return pFolder->CompareIDs(0, reinterpret_cast
reinterpret_cast
}
下图显示了项通过HWND分类的观察,递减顺序:
在类名列上点击,我们可以得到下面结果:
浏览窗口
在任何给出的文件夹观察中,通常都应该有子文件夹和‘叶’窗口。在处理实际文件夹时,探测器的用户界面使你能通过双击子文件夹浏览进它的内部。然而在我们写的命名空间扩展中这个特征不是自动的,我们必须自己实现它。有两点需要解决:
取得选定列表观察项的PIDL
打开新的文件夹
项的PIDL是容易取得的,因为我们特意把它与项存储到一起了。下面是怎样取得它的代码,这是在CShellView::OnNotify()函数的LVN_ITEMACTIVATE处理段的代码:
LV_ITEM lvi;
ZeroMemory(&lvi, sizeof(LV_ITEM));
lvi.mask = LVIF_PARAM;
LPNMLISTVIEW lpnmlv = reinterpret_cast
lvi.iItem = lpnmlv->iItem;
ListView_GetItem(m_hwndList, &lvi);
m_pShellBrowser->BrowseObject(reinterpret_cast
SBSP_DEFBROWSER | SBSP_RELATIVE);
首先,我们准备一个LV_ITEM结构,以便由ListView_GetItem()对其进行填写。在屏蔽成员上,我们设置标志表示需要的信息类型——lParam值与之相关。此后调用IShellBrowser::BrowseObject(),传递要浏览的PIDL。特别要注意的是这个方法的第二个参数:
HRESULT IShellBrowser::BrowseObject(LPCITEMIDLIST pidl, UINT wFlags);
这个UNIT数用于驱动BrowseObject()行为,我们指定的第一个标志是SBSP_DEFBROWSER,意思是希望用当前观察的相同选项打开新文件夹:不是新窗口,不是不同观察设置。对于调用这个方法的函数这一定是最一般的选择。第二个标志SBSP_RELATIVE说明PIDL是相对于当前文件夹的。在这种情况下,这是明智的选择,因为我们在项的lparam成员中存储的是相对的PIDL——它仅仅涉及特定的窗口,忽略任何关于其父窗口的信息。因此,如果使用SBSP_ABSOLUTE,将产生新的空文件夹。
给出用户界面
在我们的命名空间扩展得到焦点后,它可以改变探测器的菜单和工具条。这种改变应该在扩展接收聚焦时产生,并且在失去焦点时消除。
IShellView::UIActivate()方法获得Shell调用来通知你的扩展它是活动的还是不活动的,因此可以在这个函数中产生对菜单的改变和对用户界面其它设置的修改。在下面的代码中我们也注意到了状态条,而且这也是改变工具条的正确地点,在前面我们已经讨论了这一点。
STDMETHODIMP CShellView::UIActivate(UINT uState)
{
// 如果遇上一次比较状态没有改变,则退出
if(m_uState == uState)
return S_OK;
// 修改菜单
OnActivate(uState);
// 修改状态条
if(uState != SVUIA_DEACTIVATE)
{
TCHAR szName[MAX_PATH] = {0};
// 如果需要添加更多部分, 它等价于 SB_SIMPLE
int aParts[1] = {-1};
// 设置部分数
m_pShellBrowser->SendControlMsg(FCW_STATUS, SB_SETPARTS, 1,
reinterpret_cast
m_pPidlMgr->GetPidlPath(m_pidl, szName);
m_pShellBrowser->SendControlMsg(FCW_STATUS, SB_SETTEXT, 0,
reinterpret_cast
}
return S_OK;
}
状态条是一个带,通常放在顶层窗口的底部——在探测器场合。它可以划分成几部分,每一部分都有它们自己的外观设置(插入,平面,凸起),要设置状态条的各个部分,是用SB_SETPARTS消息,它是用整数组来表示各个部分的右边缘(lparam 变量),另一个整数表示部分数(wparam 变量)。
描述每一节右边缘的数组值以客户坐标表示。如果一个值是 -1(如我们的例子),这一部分是要扩展到驻在窗口的右边缘的。一个只有单个部分的状态条实际是由SB_SIMPLE消息定义的更简单的状态条,它使所有已存在的部分被删除。使用部分数组则是:
int aParts[1] = {-1};
发送消息SB_SIMPLE将产生与上面代码同样的效果。然而,如果我们确实在这里这样做的话,你将不会看到关于状态条的这些细节信息。
菜单修改
修改菜单要求三步操作,象我们早期描述的,首先需要建立新菜单,然后把它与菜单描述符一起传递给探测器。而后探测器以标准方式填写菜单,此时,你可以选择修改它。
在我们的例子中,需要添加‘窗口观察’顶层菜单。进一步,我们不需要‘编辑’菜单(因为对于打开窗口的列表不需要操作),和改变‘帮助|关于…’窗口。
LRESULT CShellView::OnActivate(UINT uState)
{
// 上次调用之后状态是否改变?
if(m_uState == uState)
return S_OK;
// 销毁所有以前对菜单的改变
OnDeactivate();
// 如果活动...
if(uState != SVUIA_DEACTIVATE)
{
// 步骤 1: 建立新菜单
m_hMenu = CreateMenu();
if(m_hMenu)
{
// 步骤 2: 通过菜单组描述符与Shell共享它
OLEMENUGROUPWIDTHS omw = {0, 0, 0, 0, 0, 0};
m_pShellBrowser->InsertMenusSB(m_hMenu, &omw);
// 步骤 3: 改变菜单
// 步骤 3.1: 建立并插入 '窗口观察' 顶层菜单
TCHAR szText[MAX_PATH] = {0};
LoadString(g_hInst, IDS_MI_WINVIEW, szText, MAX_PATH);
MENUITEMINFO mii;
ZeroMemory(&mii, sizeof(MENUITEMINFO));
mii.cbSize = sizeof(mii);
mii.fMask = MIIM_SUBMENU | MIIM_TYPE | MIIM_STATE;
mii.fType = MFT_STRING;
mii.fState = MFS_ENABLED;
mii.dwTypeData = szText;
mii.hSubMenu = BuildWinViewMenu();
if(mii.hSubMenu)
InsertMenuItem(m_hMenu, FCIDM_MENU_HELP, FALSE, &mii);
// 步骤 3.2: 取得帮助菜单,并合并
ZeroMemory(&mii, sizeof(MENUITEMINFO));
mii.cbSize = sizeof(MENUITEMINFO);
mii.fMask = MIIM_SUBMENU;
if(GetMenuItemInfo(m_hMenu, FCIDM_MENU_HELP, FALSE, &mii))
MergeHelpMenu(mii.hSubMenu);
// 步骤 3.3: 删除‘编辑’菜单
DeleteMenu(m_hMenu, FCIDM_MENU_EDIT, MF_BYCOMMAND);
// 步骤 3.4: 如果有焦点,加项到文件菜单
if(uState == SVUIA_ACTIVATE_FOCUS)
{
// 获得‘文件’菜单 并合并
ZeroMemory(&mii, sizeof(MENUITEMINFO));
mii.cbSize = sizeof(MENUITEMINFO);
mii.fMask = MIIM_SUBMENU;
if(GetMenuItemInfo(m_hMenu, FCIDM_MENU_FILE, FALSE, &mii))
MergeFileMenu(mii.hSubMenu);
}
// 设置新菜单
m_pShellBrowser->SetMenuSB(m_hMenu, NULL, m_hWnd);
}
}
// 保存当前状态
m_uState = uState;
return 0;
}
注意,我们应该怎样用在shlobj.h中预定义的常量来引用系统菜单。例如,在‘帮助’菜单前加新菜单,我们使用:
InsertMenuItem(m_hMenu, FCIDM_MENU_HELP, FALSE, &mii);
用这个调用,InsertMenuItem()插入新菜单到第二个参数指定的菜单之前。BuildWinViewMenu()是有如下形式的辅助函数:
HMENU CShellView::BuildWinViewMenu()
{
HMENU hSubMenu = CreatePopupMenu();
if(hSubMenu)
{
TCHAR szText[BUFSIZE] = {0};
MENUITEMINFO mii;
// 加 "属性" 到 "窗口观察"
LoadString(g_hInst, IDS_MI_PROPERTIES, szText, BUFSIZE);
ZeroMemory(&mii, sizeof(MENUITEMINFO));
mii.cbSize = sizeof(MENUITEMINFO);
mii.fMask = MIIM_TYPE | MIIM_ID | MIIM_STATE;
mii.fType = MFT_STRING;
mii.fState = MFS_ENABLED;
mii.dwTypeData = szText;
mii.wID = IDM_WIN_PROPERTIES;
// 加到菜单尾部
InsertMenuItem(hSubMenu, static_cast
// 加 "进程观察" 到 "窗口观察"
LoadString(g_hInst, IDS_MI_PROCESSVIEW, szText, BUFSIZE);
ZeroMemory(&mii, sizeof(MENUITEMINFO));
mii.cbSize = sizeof(MENUITEMINFO);
mii.fMask = MIIM_TYPE | MIIM_ID | MIIM_STATE;
mii.fType = MFT_STRING;
mii.fState = MFS_ENABLED;
mii.dwTypeData = szText;
mii.wID = IDM_WIN_PROCESS;
// 加到菜单尾部
InsertMenuItem(hSubMenu, static_cast
}
return hSubMenu;
}
默认情况下,在‘文件’菜单下的新项(以及任何我们建立的工具条按钮)仅当扩展有焦点时出现。如果在右窗框中选择项,它仍然没有焦点,所以你必须等待合并你的客户项到‘文件’菜单。在扩展丢失焦点时,好的方法是指令我们删除所有痕迹:
void CShellView::OnDeactivate()
{
if(m_uState != SVUIA_DEACTIVATE)
{
if(m_hMenu)
{
m_pShellBrowser->SetMenuSB(NULL, NULL, NULL);
m_pShellBrowser->RemoveMenusSB(m_hMenu);
DestroyMenu(m_hMenu);
m_hMenu = NULL;
}
m_uState = SVUIA_DEACTIVATE;
}
}
显示帮助文字
下图显示当窗口观察扩展活动时探测器的新菜单。
注意出现在状态条中的帮助文字。为了允许这个特征,你简单地需要在窗口过程中处理WM_MENUSELECT消息。就象在老Windows编程时所做的那样。有两种方法将文字设置到状态条上。使用SendControlMsg()直接发送消息到窗口,或通过调用SetStatusTextSB()自己设置文字。虽然资料中推荐使用后者,IShellBrowser还是输出了这两个函数。这是合理的(这个方法封装了明显的状态条消息,并且在未来也是不变化的),但是这也暴露了一个问题,在涉及到具有多个部分的状态条时,这个函数就是不充分的了:SetStatusTextSB()不允许指定想要设置那一部分。此外,我们早期提到过,SetStatusTextSB()使用Unicode串。
连接关联菜单与项
Shell通过查找IContextMenu接口自动搜索关联菜单。对于这个例子我们实现这个接口,并添加两个项。第一个是拷贝窗口显示名到剪裁板,第二个是显示窗口属性的对话框:
如图所示,这个对话框传达的信息包括建立窗口的可执行程序名,以及图标。特别为了得到图标,我们使用了下面代码:
//返回指定窗口的大/小图标备份,或标准图标,如果没有属于这个窗口类的图标。
HICON GetWindowIcon(HWND hwnd, BOOL fBig)
{
HICON hIcon = NULL;
//首先搜索指定到窗口类的图标。如果没有找到,在试图通过WM_GETICON取得指
//派给特定窗口的图标。如果失败,使用标准图标。
if(fBig)
{
// 要求大图标
hIcon = reinterpret_cast
if(hIcon == NULL)
hIcon = reinterpret_cast
ICON_BIG, 0));
}else{
hIcon = reinterpret_cast
if(hIcon == NULL)
hIcon = reinterpret_cast
ICON_SMALL, 0));
}
if(hIcon == NULL)
hIcon = LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_PARWND));
// 返回图标备份
return CopyIcon(hIcon);
}
首先检查类图标,如果没有找到,则改为发送WM_GETICON消息到窗口。如果这个方法也失败,则使用标准图标。无论什么结果,我们都返回一个图标的备份,而不是初始图标。调用者负责释放这个图标。
对于找出建立给定窗口的可执行程序名,我们使用一个WDJ文章中给出的窍门。它使用Winodws9x下的‘ToolHelp’API,以及WindowsNT4.0下的PSAPI库。在第15章也涉及到了这个科目。
联接关联菜单与项就象实现IContextMenu接口函数那样容易:
STDMETHODIMP CContextMenu::InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi)
{
WORD wCmd = LOWORD(lpcmi->lpVerb);
switch(wCmd)
{
case 1: // 属性
ShowProperties();
break;
case 0: // 拷贝
CopyTextToClipboard();
break;
}
return S_OK;
}
STDMETHODIMP CContextMenu::GetCommandString(UINT, UINT, UINT*, LPSTR, UINT)
{
return E_NOTIMPL;
}
STDMETHODIMP CContextMenu::QueryContextMenu(HMENU hmenu,UINT indexMenu,
UINT idCmdFirst, UINT idCmdLast, UINT uFlags)
{
UINT idCmd = idCmdFirst;
// 添加新项,从资源中装入串
TCHAR szItem[BUFSIZE] = {0};
LoadString(g_hInst, IDS_MI_COPY, szItem, BUFSIZE);
InsertMenu(hmenu, indexMenu++, MF_STRING | MF_BYPOSITION, idCmd++, szItem);
LoadString(g_hInst, IDS_MI_PROPERTIES, szItem, BUFSIZE);
InsertMenu(hmenu, indexMenu++, MF_STRING | MF_BYPOSITION, idCmd++, szItem);
return MAKE_SCODE(SEVERITY_SUCCESS, FACILITY_NULL, idCmd - idCmdFirst);
}
所有这些在探测器左窗框中都能很好地工作。如果你想要在观察中捕捉右击(即,右窗框),则必须自己做所有的工作。在观察窗口过程中感觉WM_CONTEXTMENU消息是一种方法。感觉到这个消息之后,你就可以飞快地建立菜单,从资源中装入模版。另外,如果在观察类中有一个IShellFolder指针可用,你就可以调用GetUIObjectOf(),使IContextMenu接口帮助你构造要显示的菜单。然而此时你必须识别那一个窗框调用这个菜单:是左还是右。一般我们认为应该采用建立新菜单的方案。注意,微软注册表观察示例中没有对关联菜单的支持。
较好的关联菜单代码
尽管上面的代码肯定能工作,但是它并没有考虑到菜单应该适应文件对象所有可能的特征。例如,如果文件夹项设置了SFGAO_CANRENAME位,关联菜单就应该提供‘重命名’命令(局部地,如果需要)。IContextMenu::QueryContextMenu()方法的uFlags变量可以采用一定数量的值,这在第15章中已经部分地讨论过,现在需要在命名空间扩展中再次回顾它们。
标志 |
描述 |
CMF_EXPLORE |
这个菜单被显示在探测器树观察窗口。添加探测器命令。 |
CMF_RENAME |
文件对象可以重命名。添加重命名命令。 |
CMF_DEFAULTONLY |
命名空间扩展应该只加默认项,如果有的话。 |
CMF_NODEFAULT |
命名空间扩展不应该定义任何默认项 |
更精确的代码可以从前面QueryContextMenu()实现代码中通过添加一小段代码获得。如下代码,检测CMF_CANRENAME标志,和添加新命令到菜单。
if(uFlags & CMF_CANRENAME)
{
LoadString(g_hInst, IDS_MI_RENAME, szItem, BUFSIZE);
InsertMenu(hmenu, indexMenu++, MF_STRING | MF_BYPOSITION, idCmd++, szItem);
}
要处理这个命令,你可以使用SendControlMsg()和FCW_TREE常量直接发送消息到树观察。相反,探测器命令的实现,你可以使用ShellExecuteEx(),传递PIDL作为文件对象。
一般情况下,实现QueryContextMenu()应该考虑所有可能的标志,但有时这并不是总有意义。在我们的例子中,要想描述对于窗口编辑应该做什么就有点问题——我们的脑子里只有窗口标题。
连接图标与项
IExtractIcon是一个有用的接口,它使你能把图标与文件夹项关联到一起,但是再一次注意,它仅仅操作显示在左窗框上的图标。这与关联菜单的理由是一样的:Shell不知道什么时候客户观察需要图标。显示在列表观察中的图标显然是由充填列表观察的代码确定的。
对于显示在地址栏和树观察节点上的图标,你需要实现IExtractIcon接口的下面函数:
STDMETHODIMP CExtractIcon::GetIconLocation(UINT uFlags,LPTSTR szIconFile,
UINT cchMax, LPINT piIndex, LPUINT puFlags)
{
*puFlags = GIL_DONTCACHE | GIL_PERINSTANCE;
return S_FALSE;
}
STDMETHODIMP CExtractIcon::Extract(LPCTSTR pszFile, UINT nIconIndex,
HICON* phiconLarge, HICON* phiconSmall, UINT nIconSize)
{
HWND hwnd = m_pPidlMgr->GetData(m_pidl);
if(hwnd == GetDesktopWindow() ||
hwnd == FindWindow(__TEXT("shell_traywnd"), NULL))
{
*phiconLarge = LoadIcon(NULL, MAKEINTRESOURCE(IDI_WINLOGO));
*phiconSmall = LoadIcon(NULL, MAKEINTRESOURCE(IDI_WINLOGO));
return S_OK;
}
*phiconLarge = LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_WINVIEW));
*phiconSmall = LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_WINVIEW));
return S_OK;
}
除了两种情况之外,我们都使用相同的图标:在窗口是桌面,或者任务条时。在这两种情况下,我们使用类似于Windows商标的图标。象第9章的例子,任务条的窗口类是shell_traywnd,这是我们使用Spy++搜查时发现的。在命名空间扩展中使用图标时,我们还应该考虑使用IShellIcon,而不是IExtractIcon,因为它是一个更快地获得图标的方法。然而它的能力比较弱,IShellIcon只输出一个函数GetIconOf():
HRESULT IShellIcon::GetIconOf(LPCITEMIDLIST pidl,
UINT flags,
LPINT lpIconIndex);
Flags变量起着与IExtractIcon接口的GetIconLocation()方法的第一个变量同样的作用。合法的值是GIL_FORSHELL 和 GIL_OPENICON(见第15章)。这个函数在lpIconIndex参数中返回索引,是相对于系统图标图像列表的。如果返回的图标没在系统图像列表上,开发者负责在返回索引之前将其插入,当然应该确保只插入一次。
IShellIcon的好处是你不需要在每次需要图标时都建立它的一个实例,代之的是在实现它的时候,你自动使Shell请求它返回给定PIDL的系统图像列表索引。在整个会话期间,它仅仅运行一个实例。就象一种接受PIDL和返回图标索引的服务器一样工作。
反过来,使用IExtractIcon,你有一个展示出现在Shell文件夹某些地方的单独的,个别图标的接口。也就是说,每次涉及到不同图标时,你都需要一个新的实例。当在树观察窗口绘制图标时,探测器首先搜索IShellIcon,并且仅当没有找到它时才通过GetUIObjectOf()求助于IExtractIcon。
安装命名空间扩展
讨论到这此时,我们已经完成并编译了我们的命名空间扩展。下一步是安装和使其适当工作。我们需要作一些操作:
注册命名空间作为COM服务器,指定Apartment线程模型,图标,和扩展名
注册它成为被认可的扩展,使它也能在Windows NT下工作
定义你的连接点到系统。
头两点是容易实现的:
REGEDIT4
; 在同一行上写注册表条目是绝对必要的...
; 注册服务器和它的线程模型
[HKEY_CLASSES_ROOT\CLSID\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}\InProcServer32]
@= "C:\\WinView\\winview.dll"
"ThreadingModel" = "Apartment"
; 注册扩展名
[HKEY_CLASSES_ROOT\CLSID\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}]
@= "Windows View"
; 注册图标
[HKEY_CLASSES_ROOT\CLSID\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}\DefaultIcon]
@= "C:\\WinView\\winview.dll,0"
; 在NT下注册扩展
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\ShellExtensions\Appr
ved\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}]
@= "Windows View"
这里{F778AFE0-2289-11d0-8AEC-00A0C90C9246}是我们的命名空间扩展的CLSID,象在Guid.h中定义的一样。这个清单的最后三项实际由你自己决定,在这种情况下,我们的扩展不使用文件做任何操作,因此,对给定的文档类型,它不能理解。一个更好的想法是使用目录作为连接点,通过建立:
My Windows.{F778AFE0-2289-11d0-8AEC-00A0C90C9246}
文件夹,你就能获得下面对结果:
最后,我们决定另外的方法:把扩展放到桌面命名空间中。
HKEY_LOCAL_MACHINE
\Software
\Microsoft
\Windows
\CurrentVersion
\Explorer
\Desktop
\NameSpace
\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}
我们简单地在NameSpace下用扩展的CLSID建立了一个新键。对于出现在桌面上的图标和探测器树观察新接点这是充分的。
然而,还有另外的设置也应该保存:这个文件夹的默认标志。我们在下面路径上存储新的ShellFolder键:
HKEY_CLASSES_ROOT
\CLSID
\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}
\ShellFolder
Attributes条目则被设定为 SFGAO_FOLDER | SFGAO_HASSUBFOLDER:
这正是要讨论ShellFolder键的时候。过一会我们将告诉你它们更多的事情。
桌面上的节点
窗口的桌面包含了一些图标,它们引用系统特殊的文件夹。用上面的技术,你可以加入具有新标记的系统文件夹,无论你想要它有何种行为。‘窗口观察’图标既不是一个快捷方式,也不是一个直接拷贝到桌面目录上的程序。它是一个客户化的系统文件夹,定位在桌面上。如果我们现在想要打开它,则可得到一个根观察:
附加信息标签
你一定注意到了几乎所有出现在桌面上的系统文件夹都有一个工具标签(我们也称之为信息标签),当鼠标在其上游动一会,这个标签就被显示。下图显示了‘网上邻居’的情形:
你会感到奇怪,为什么信息标签不同于工具标签,我们将告诉你:这只是命名的差异。工具标签是由‘工具’和‘标签’组合而成,表示工具的辅助说明。‘数据标签’,‘标题标签’和‘信息标签’都是同义的。
加一个信息标签到我们自己的命名空间扩展上是可能的,在通读了可用的资料后,我们建议你获取这个信息标签必须使用新的IQueryInfo接口,然而,事实上有更简单和更可靠的技术。就是在注册表的CLSID键下添加一个新值,在我们的例子中是:
HKEY_CLASSES_ROOT
\CLSID
\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}
值是InfoTip,其内容则是要显示的串:
我们并没有找到关于这一点任何知识库文章和官方资料。只是简单地比照‘我的计算机’注册值——一个桌面命名空间扩展。其它扩展也有这个特征。
附加可删除消息
在开发这个扩展期间,有一点是我们想要从桌面上删除它,并重新安装。我自动右击桌面上的这个图标来搜索‘删除’命令。但是没有找到任何东西。没关系(我想),我把它拖到‘回收站’,还是没起作用。因为确实要删除它,所以必须手动地干预注册表。
在上面路径下的CLSID键之前,我查看了一下PC上存储的其它系统文件夹的信息:‘收件箱’,和‘回收站’。尤其是收件箱项定义了一个值得关注的值串:Removal Message。立即返回桌面,右击收件箱图标。它有一个删除命令,选择之后,产生包含删除信息的对话框,这个删除信息是设置在注册表中的Removal Message值的内容。
使文件夹可删除
现在我们知道怎样设置客户可删除消息,但是怎样使删除命令在文件夹的关联菜单中被允许呢?它们都是使用文件夹的特征和注册表中的Attributes键。为了使文件夹可删除,只要通过添加SFGAO_CANDELETE属性告诉Shell就可以了。我们改变存储在注册表中的文件夹属性,再次注册这个扩展,改变立即发生了,下图就说明了最终的结果:
附加文件夹属性
不需要更多的研究你就可以通过添加特征条目的其它值实现文件夹的‘重命名’和‘属性’这两个关联菜单项,如SFGAO_CANRENAME 和 SFGAO_HASPROPSHEET。重命名完全由系统来处理——用户所要做的就是在保持图标选中的情况下点击图标或按下F2键。
实现属性命令需要作更多的工作。所需要的是提供IShellPropSheetExt接口的COM模块,就像第15张中Shell扩展那样,在点击标准的‘属性’项时,系统自动搜索被点击对象的这个接口。你需要注册它在Shellex\PropertySheetHandlers键下:
HKEY_CLASSES_ROOT
\CLSID
\{CLSID}
浏览客户文件夹
通过命名空间扩展展示的客户文件夹没必要显示在‘打开’或‘另存为’通用对话框中。为了能在通用对话框中出现,你需要声明文件夹为文件系统的一部分,因为这些对话框仅仅允许文件系统的文件夹显示。技巧是添加另外两个SFGAO_XXX常量:SFGAO_BROWSABLE 和 SFGAO_FILESYSTEM。
然而,即使我们在标准打开对话框中显示了这个文件夹,也没有太多的用途,因为这个文件夹并不响应触发信息,绝不进入下一层。还有,这个对话框根本不响应点击和回车键。
有一个接口似乎可用于在通用对话框中显示客户文件夹,使其具有普通文件夹的能力:ICommDlgBrowser。然而,它是由通用对话框实现的。不幸的是微软并不支持这个对话框浏览任何第三方命名空间。为了从客户文件夹查看和选择项,你需要借助我们在第5章遇到的函数SHBrowseForFolder()。
使这个例子工作
从Web站点上下载和重新编译了这个例子之后,安装这个扩展所要做的就是注册这个控件。必要的设置已经写进了模块的DllRegisterServer()函数,在这个生成的DLL上调用regsvr32.exe,然后刷新探测器,将能使每一件事情都能像描述的那样正常工作。
卸载这个示例
如果你想卸载这个例子,最简单的方法就是在桌面上右击这个图标,然后选择‘删除’。这将从注册表的‘desktop’节点删除其CLSID键。如果你愿意,也可以通过注册表编辑器来删除:
HKEY_LOCAL_MACHINE
\Software
\Microsoft
\Windows
\CurrentVersion
\Explorer
\Desktop
\NameSpace
\{F778AFE0-2289-11d0-8AEC-00A0C90C9246}
从上键中删除最后一项,然后返回到桌面,按F5刷新屏幕。但是要说明,使用菜单驱动的方法是更好的。在选择删除时,将有下面信息出现:
确认后,图标将从桌面消失。注意,删除过程绝对与系统任何其它文件夹的删除过程一样。
命名空间扩展总结
命名空间扩展在Windows95和Windows NT版被引进,而且在以后的版本中其编程规则没有变化。但是,自从Shell4.71之后你就可以写在两种观察模式之间变化的命名空间扩展了:典型的和web观察的。
典型观察就是标准观察,对于此时的文件夹,系统是通过列表观察展示的典型观察。相反,对于Web观察,它是同样内容基于HTML的观察。对于程序员的挑战不仅要建立这两种观察,而且还要使它们可交互。
在编写命名空间扩展时,你应该考虑加入支持Web观察的能力。允许用户在典型观察和Web观察之间转换的方法是探测器的‘观察 | 作为Web页’菜单。
这一章剩下的部分,我们将描述两种技术来组合你的文件夹与HTML。首先,我们提供关于web观察的概览,并计划在实现web观察的命名空间扩展时所产生的的设计结果。其次我们展示怎样使用已存在的Shell观察结构定制用于显示普通文件夹数据的模版。这是由文件夹关联菜单上的命令‘定制文件夹’所允许的特征。
什么是Web观察
对于Windows95和NT上的活动桌面和后来的Windows98 ,引进了文件夹的web观察。基本上讲,这是一个动态HTML页面,其中包含了作为部件的典型观察,一些其它小部件如GIF图像,以及微小的观察控件。为了给出更形式化的定义,我们首先要解释:
对于命名空间扩展其‘典型观察’是用于显示文件夹内容的窗口。
现在我们定义web观察:
对于命名空间扩展其‘web观察’是显示动态HTML页面的浏览器窗口,其中包含了
典型观察作为一个部件。
在建立了一个工作的命名空间扩展之后,添加web观察支持就是要输出它的观察窗口(典型观察)作为一个嵌入到动态HTML页面上的部件。此外,你必需准备在两个观察之间转换,而且将来如果不够,还要考虑观察的数目可能多于两个。典型的Shell观察已经提供了同一数据的四种观察:大小图标方式,细节和列表方式,这些只是用于表述普通数据控件——列表观察的能力——不是确切地由提供文件夹行为的代码给出的。Web观察有点不同,它实际要求不同的窗口和部件。这些新行为参与者必须适合已存在的规则并与环境很好地结合。
从命名空间扩展的观点上看,使用web观察要求两个新的接口:IShellView2和 IPersistFolder2。现在这些仅仅在Windows98以及后来的版本中才支持。Web允许的命名空间扩展具有我们到现在为止所看到的所有特征,以及支持‘观察 | Web页面’菜单命令的能力。在这个命令激活时,期望扩展显示和以前相同的内容,但是是使用HTML模版来显示。在两种观察之间转换的能力暴露出一些附加的问题,要稍微修改一下命名空间扩展的整体结构。开始写作时这方面的资料刚开始出现,最好的是引用平台SDK资料。这一节我们就给出一些关于web观察的注释。
Shell观察ID
如果你查看最近的shlobj.h头文件源码,就会发现下面的定义:
typedef GUID SHELLVIEWID;
看上去并不熟悉,但是这引出命名空间扩展中的一个概念:Shell观察ID。这是标识一种Shell观察的唯一号码,在绝大多数场合它等同于实现这个扩展的COM对象CLSID。
这个新标识符由IShellView2接口的函数使用,以在可能的观察之间转换。实际上,当你在浏览器的菜单或工具条上选择或放弃Web观察选项时,探测器用不同的Shell观察ID调用IShellView2::CreateViewWindow2(),产生典型的活web观察。
默认的观察
在内部,CreateViewWindow2()必须区别和实际建立不同的观察窗口。值得关注的是它不能返回不同的窗口Handle到探测器。换句话说,探测器不与特定的观察交谈,无论它是列表观察还是Web浏览控件的实例,代之,它总是涉及相同的窗口,这个窗口称为默认观察。这是一个SHELLDLL_DefView类窗口。
下图显示了同一个文件夹通过典型或web观察调用时两个窗口之间的差别。注意默认观察在二者中都出现:
左图是典型观察,右图是同一个文件夹的web观察。Internet Explorer_Server是这个窗口的Web浏览器控件的类名。我们可以推论,Web允许的命名空间扩展必须建立web观察窗口作为其典型观察窗口的姊妹。下图显示了探测器与观察之间的关系:
开始默认观察应该指定一个可用的观察(一般是典型观察)。而后,每当探测器调用CreateViewWindow2()时,命名空间扩展都将获得Shell观察ID,并在默认观察窗口内显示由这个ID标识特定的观察。
IShellView2接口的新函数
在IShellView2中有四个新函数:
函数 |
描述 |
CreateViewWindow2() |
允许在不同的观察之间转换,包括web观察和典型观察。 |
GetView() |
返回由定义常量指定观察的Shell观察ID,一般情况下仅有两种可能性:默认观察SV2GV_DEFAULTVIEW,和当前观察SV2GV_CURRENTVIEW。 |
HandleRename() |
允许改变给定项的PIDL。它接收PIDL,并用新的一个置换它。 |
SelectAndPositionItem() |
这个函数接受PIDL,某些标志,和指向POINT结构的指针。它的用途是:把选择项放到观察的任何地方,对于文件夹的客户化的HTML观察,这是合理的。 |
CreateViewWindow2()方法的输入仅有一个指向SV2CVW2_PARAMS结构的指针,其定义如下:
typedef struct _SV2CVW2_PARAMS
{
DWORD cbSize;
IShellView* psvPrev;
FOLDERSETTINGS const* pfs;
IShellBrowser* psbOwner;
RECT* prcView;
SHELLVIEWID const* pvid;
HWND hwndView;
} SV2CVW2_PARAMS;
让我们来比较一下它与IShellView::CreateViewWindow()。
HRESULT CreateViewWindow(LPSHELLVIEW pPrevView,
LPCFOLDERSETTINGS lpfs,
LPSHELLBROWSER psb,
LPRECT prcView,
HWND* phWnd);
如上所示,结构中实际包含了相同类型的变量,以及一个Shell观察ID和典型的尺寸成员。显然CreateViewWindow2()是CreateViewWindow()的重定义,并且引进了对Web观察的支持,除了cbSize字段,用于微软的内部目地以外,Shell观察ID是唯一的变化。
IPersistFolder2接口的新特点
IPersistFolder2接口通过增加一个GetCurFolder()新函数扩充了IPersistFolder。它的原型为:
HRESULT IPersistFolder2::GetCurFolder(LPITEMIDLIST* ppidl);
它使用一个‘输出’参数来返回当前对象的PIDL,以使对于外部世界,这个当前被观察文件夹的PIDL是可用的。实际上,它是在命名空间扩展初始加载时由IPersistFolder::Initialize()方法传递的同一个PIDL。
怎样构造Web观察
如果你想要处理给定文件夹的web观察,就必须至少实现下面三步:
定义默认观察对象作为其它观察对象的容器
定义典型观察对象用文件夹项提供标准的观察
定义web观察对象,它包括典型观察作为部件
你的默认观察应该能根据接收的Shell观察ID返回典型观察或web观察对象。此外,Web观察必须包含一个Web浏览控件,以使其导出动态HTML页面提供的模版。这个模版应该包含典型观察对象作为ActiveX部件。如果混淆了的话,下图能帮助你搞清楚这个问题:
接触典型观察对象
在上图中,web观察对象含有一个Web浏览控件,这个控件指向一个动态HTML页面。依次这个页面通过CLSID含有一个典型观察作为其一个部件。事情是web观察需能驱动典型观察对象,例如,它需要通知这个观察显示正确的文件夹内容。为了管理,web观察对象需要通过调用IWebBrowser2::get_Document()从Web浏览器中获得‘文档’特征。
HRESULT IWebBrowser2::get_Document(IDispatch** ppDisp);
这个文档对象是动态HTML对象模型的根。在一个ActiveX文档驻留到ActiveX容器,如Web浏览器中时,文档特征返回其指向文档对象模型的入口(IE4.x把HTML文件作为ActiveX文档)。通过枚举页面中的标记集,你应该能够获得典型观察对象的IDispatch接口指针。典型观察通过标记方式嵌入到动态HTML模版中,形式如下:
由于web观察对象知道典型观察对象的CLSID,所以能够容易确定哪一个枚举对象是正确的。一旦取得了IDispatch指针,就有了使web观察和典型观察通讯的方法。
Web观察模版
Web观察由HTML页面特征化,这个页面可以驻留在任何我们能找到的地方:可以是与实现这个扩展的DLL同路径的文件,或者是存储在系统文件夹下的文件。在第5章中我们看到过,有一个系统文件夹是为模版准备的,过一会我们还会遇到它。一般情况,你必须给出不同的文件,如果这个文件消失,可能引起问题。
对于web观察,一个好的方案是把HTML文件嵌入到扩展的资源中,然后建立本地拷贝,或直接采用‘res://’协议读出这个资源,这个URL协议使你能从可执行文件的资源直接加载HTML文件(一般情况下,任何Win32资源)。使用这个方法,你就可以嵌入HTML模版到资源中,因此而忽略任何问题。例如,如果命名空间扩展的资源中有下面一行代码:
MYTEMP.HTT HTML "myTemp.htt"
你就可以请求Web浏览器导航下面的URL:
res://
在这里还有一个潜在的问题:我们怎样来处理所有杂项如GIF,控件,和脚本程序。简单的规则是每一件东西都可以通过嵌入到资源中的不同文件名加以识别,并且可以通过res://协议在HTML页中进行引用。
Res://协议也在VC++ 6以后的MFC各版本的钩子下被使用,用来实现一些 CHtmlView类,它们是对WebBrowser的一种封装。
触发事件
除非你的命名空间扩展是非常不寻常的,否则,用户界面总有一些事情需要选择。这就是典型观察和web观察需要通讯的一般情况。通过HTML,这些事情如下解决。典型观察嵌入成为触发事件的部件——设为SelectionChanged。对于web观察,定义在模版中的代码解释这些通知,并作出适当的响应。
从定制到客户化文件夹
Web观察是以定制方式展示文件夹的方法。改变给定文件夹展示方法的人是程序员,而且一旦安装了扩展,用户仅仅可以在典型与web观察之间转换。
通常文件型文件夹由系统提供web观察,兴趣所在是这些观察可以被客户化。换言之,只要你从正常的目录转换到web观察,就能根本上改变这个web观察的模版。这个过程叫做文件夹客户化。
如果在自己的扩展中希望文件夹客户化,简单地保持它所驱动的HTML模版可视就行了。否则可以通过把它们潜入到可执行文件的资源中来隐藏它们,并使用res://协议恢复它们。
文件夹客户化本身并不是命名空间扩展,但是它们可以被看作是超简单的模仿,实际上这可能有点滑稽:有时,客户化文件夹可能实际作为控制台来表示客户行为的数据,然而不要忘了,客户化文件夹是基于简单的HTML模版形成的,完全依赖于文件型文件夹的web观察扩展的存在和工作。
文件夹客户化
Shell版本4.71以后,任何用户都有机会客户化每一个文件夹的外观。如果你在文件夹上右击,并选择‘客户化文件夹’项,则有下图出现:
这个向导非常简单,并最终用folder.htt打开记事本。.htt文件是一个HTML文件,它定义了文件夹的外观。如果接收并保存标准文件内容,这个观察有如下外观:
这个页面展示的结构如下图所述:
正如所想象的,这个页不是静态的:其大部分内容都动态决定。目录名,图标,文件信息,以及预览全部都是运行时生成的。尤其是页面嵌入的三个ActiveX控件:一个是抽取图标,一个是提供当前选择文件的预览(仅支持几种类型),再有就是实际文件列表。相反,目录名由一个宏%THISDIRNAME%给出,它在运行时展开。
默认模版
作为客户化过程的结束,每一个文件夹都含有一个隐藏的folder.htt。对于文件夹查询和文件夹本身的行为,这是合理的,向导拷贝到指定目录下的仅仅是默认的模版。它们遵从上面显示的外观。你可以通过涂改,引进任何HTML页元素:帧,表,图像,脚本,Java小程序等重写它。
默认模版来自Windows\Web目录。在那里存有所有标准的HTML模版。如果打开它,你能发现某些系统文件夹的外观稍有不同:‘我的计算机’(mycomp.htt),‘打印机’ (printers.htt),‘控制板’ (controlp.htt),桌面 (deskmvr.htt),以及‘页面模版’(safemode.htt)。
从这里不难看出folder.htt严格的并不是所有文件夹的模版,我们可以看一下,在完成了客户化向导操作后文件夹模版变成什么。
Desktop.ini文件
初始时向导在文件夹下建立两个隐藏文件folder.htt 和 desktop.ini。前者我们前面已经讲到了,所以我们主要讨论后者。对于我们,Desktop.ini不是新名字,因为在这一章中已经遇到过它了,即讨论命名空间连接点的时候。
一般,Desktop.ini是一个基于文件夹的信息文件,探测器或程序员添加需要这个文件夹记住的信息,下面就是这个文件的典型内容:
[ExtShellFolderViews]
Default={5984FFE0-28D4-11CF-AE66-08002B2E1262}
{5984FFE0-28D4-11CF-AE66-08002B2E1262}={5984FFE0-28D4-11CF-AE66-08002B2E1262}
[{5984FFE0-28D4-11CF-AE66-08002B2E1262}]
PersistMoniker=file://folder.htt
[.ShellClassInfo]
ConfirmFileOp=0
各个条目的实际意义仍然是微软声明的,但是有一样是一定的:如果你改变了这个.htt文件的名字,探测器将搜索新的一个。CLSID必须一致于装入这个HTT文件的模块。这个文件的内容并不固定。例如,在文件夹属性对话框中选中‘Enable thumbnail’观察框:
desktop.ini的内容将变为:
[ExtShellFolderViews]
Default={5984FFE0-28D4-11CF-AE66-08002B2E1262}
{5984FFE0-28D4-11CF-AE66-08002B2E1262}={5984FFE0-28D4-11CF-AE66-08002B2E1262}
{8BEBB290-52D0-11d0-B7F4-00C04FD706EC}={8BEBB290-52D0-11d0-B7F4-00C04FD706EC}
[{5984FFE0-28D4-11CF-AE66-08002B2E1262}]
PersistMoniker=file://folder.htt
[.ShellClassInfo]
ConfirmFileOp=0
[{8BEBB290-52D0-11d0-B7F4-00C04FD706EC}]
MenuName=T&humbnails
ToolTipText=T&humbnails
HelpText=Displays items using thumbnail view.
Attributes=0x60000000
此外,在‘观察’菜单中出现‘Thumbnails’菜单项。这个项设置了一个新观察,象下图显示的一样:
在web观察打开时,探测器搜索当前目录查找在desktop.ini文件中PersistMoniker条目指定的文件。在失败的情况下,观察被重置。你可以给定任何具有.htt扩展名的文件。奇怪的是,如果使用另一个扩展名——如.htz——你可能得到另一个效果:
目录名——宏%THISDIRNAME%——没有被展开,如果模版文件的扩展名不是.htt。即使你简单地重命名文件,而没有触及它的源码,也是如此。
注意,你可以指定任何协议来访问.htt文件,不必象desktop.ini表示的那样‘file://’。例如,可以是‘http://’指定内网中的一个文件。
建立新模版
作为文件夹客户化的实践,我们看一段置换标准folder.htt模板的完整的客户化代码,下图说明了这个HTT文件的结构:
页面分成两个部分。上部通过2x2表的方法被进一步划分成四块,GIF图像和目录名在第一行上,位图按钮和一条水平线在第二行上。
下部整个由文件列表或某些其它信息占用。重要的是下部这两种显示是相互独立的。位图按钮根据用户点击决定显示哪种内容。实际上这个按钮控制文件列表的显示与关闭。你并不总需要包含文件列表,而且如果必要完全能够隐藏文件夹的实际内容,获得看上去象命名空间扩展所操作的结果。我们所使用的这个模板除了几个动态HTML技术之外,并没有什么特别之处。详细一点讲,我们采用了:
图像褪色
3D绘图效果
事件处理
热跟踪文字(当鼠标通过时改变颜色)
下图说明最终获得的结果:
现在看一下这个wrox.htt的源代码:
.Title {font-Size: 38; font-Family: Verdana; font-Weight: bold; color: #808080;
text-align: center;filter:Shadow(Color=#909090, Direction=135);}
.Small {font-Size: 10; font-Family: Verdana;}
.BookInfo {font-Size: 16; font-Family: Verdana;}
.HiliteSmall {font-Size: 10; font-Family: Verdana; color=red; cursor: hand;}
.Panel {background-color: #C0C0C0;}
.Fade {filter: alpha(opacity=0);}
var strHide = "Hide the file list below.";
var strShow = "Show below the file list.";
// 激活褪色
function init()
{
logo.flashTimer = setInterval("fade()", 100);
}
// 实际改变图像的不透明性产生褪色效果
function fade()
{
if (logo.filters.alpha.opacity < 100)
logo.filters.alpha.opacity = logo.filters.alpha.opacity + 10;
else
clearInterval(logo.flashTimer);
}
// 当鼠标退出页面部件时...
function mouseout()
{
obj = event.srcElement
if(obj.id == "msg")
obj.className = "Small";
}
// 当鼠标在页面部件上时...
function mouseover()
{
obj = event.srcElement
if(obj.id == "msg")
obj.className = "HiliteSmall";
}
// 在点击鼠标时...
function mouseclk()
{
// 如果事件不是由一定元素发起的...
if(event.srcElement.id != "toggle" && event.srcElement.id != "msg")
{
return;
}
// 开/关文件列表
if(toggle.visible == 1)
{
toggle.src = "closed.gif";
msg.innerHTML = toggle.outerHTML + strShow;
toggle.visible = 0;
FileList.width = 1;
FileList.height = 1;
book.style.display = "";
}
else
{
toggle.src = "opened.gif";
msg.innerHTML = toggle.outerHTML + strHide;
toggle.visible = 1;
FileList.width = "100%";
FileList.height = 200;
book.style.display = "none";
}
}
οnmοuseοut="mouseout()" οnclick="mouseclk()">
alt="Toggles the file list" id="toggle"> Hide the file list below.
%THISDIRNAME%
width=100% height=200 classid="clsid:1820FED0-473E-11D0-A96C-00C04FD705A2">
文件列表作为FileList名的ActivX控件被输出。它有自己的属性,方法和事件集,在这个例子中我们完全不管它,但是标准的folder.htt文件可以考虑使用这些属性,这个对象的资料在平台SDK中,可以搜索名为WebViewFolderContents的对象。
虽然这本书不是关于动态HTML的,但是这段代码中有几个使用了的技术需要提一下。褪色效果通过对标记分配一个特定风格来获得,它定义了一个不透明系数,并且每100毫秒增加一次。阴影文字不是位图,而是使用某种绘图效果绘制的串,它只是一种风格,其参数要求颜色和光照方向。
通过分配页面元素的ID,你可以用脚本代码非常好地控制它们。也就是说,你也可以感觉和处理特定元素的事件——如,在特定位图上的点击。而跟踪效果正是这种事件的感知过程。在鼠标进入或退出元素区域时,为了增强亮度,我们简单地写了两个过程来改变文字的颜色(更确切地说是使用不同的风格类)。
通过命名空间扩展来集成应用
作为命名空间扩展这一章的结束,我们来看一下实际应用驻留在Windows探测器中究竟有什么好处。坦白地说,没有要学习的新东西:命名空间扩展是一个允许你定制文件夹的模块,而且我们已经看到怎样建立特殊文件夹并把它们与命名空间扩展连接到一起。所缺少的是应用。关键是观察对象。一个观察对象是一个窗口,有时它可以是一个对话框模板。基于此,任何基于对话框的应用都符合嵌入进定制文件夹的条件。如果有时间修改工具条和菜单,就更有理由这样讲了。这种命名空间扩展是简单的,因为你不用考虑项,PIDLs,图标,关联菜单等。所有功能都从应用导出,它驻留在观察中,这个命名空间扩展必须提供的仅是文件夹管理器,以及建立观察的基本行为。
URL文件夹举例
这种情况的最小示例是URL文件夹的例子,你可以从我们的web站点下载这个例子。它是含有一个按钮的对话框,在Wrox出版社web站点打开浏览器窗口。然而,它的所有控件都是在客户窗口过程中进行处理的,因而也就成了可嵌入应用的原型。下图显示了它的外貌,通过运行下载所包含的.reg来安装它:
小结
在这一章中,我们讨论了命名空间扩展的各个方面,我们的观点是,命名空间扩展是Shell编程非常本质的东西:它允许你的代码紧密结合于探测器,并且允许在各个层次上进行客户化。
从这一点上看,命名空间扩展因此也是相当复杂的。这并不是由于它们有许多内部技术,当然这是一方面,而是由于几方面因素的组合。其中必须实现一定数量的接口,以及所有使模块正常工作所必须的接口(和这些接口的函数),严格地讲你可能并不需要支持这些接口。Windows系统的持续更新,以及资料的贫乏要完成这个工作是很困难的。
再有,凡是有点价值和值得讨论的资料通常都是不完整的,而且缺少的都是主要部分的内容,并且还没有很好的举例说明。仅仅是在建立了Internet客户SDK之后才有了关于非根扩展的值得关注和有意义的示例。
这一章我们覆盖了:
使命名空间扩展正常工作的所有必要接口
怎样建立和管理PIDLs
怎样映射所有理论到实际工作
怎样修改探测器的用户界面
怎样使用无资料说明的属性丰富命名空间扩展,例如信息标签和可删除消息
Web观察扩展的概览
怎样在探测器中驻留应用的提示
文件夹客户化和HTML模板文件
采用动态HTML获得文件夹的交互
最后的思考
这里是这本书的结尾,在这16章中,我们试图弄清相关于Windows Shell的Win32编程方面的所有科目。我们成功了吗?某些人也提出这个类似的问题,然而回答是即使是专家,也只有在犯过所有可能的错误之后才行。使用Windows Shell,在近两年已经花费我们大量的时间,有几次我都认为我面对的是世界上最大的系统代码bug。有很多次,我自己解决了bug ,在这些笔记中我收集了所有我遇到的问题和所有我找到的解答。
毫无疑问用Shell工作是艰难的,它是一个COM与API的混合,C和C++ 的混合,并且有一些资料仅仅是为VB而写的。当然,资料虽不太多,也并不贫乏,因此,这也使我们对含混的描述以及复杂行为的组合效果增强了判断能力。
我们确实希望你能喜欢这本书,从头至尾阅读它。我想做的最后一件事就是通过解释的方式回答两个评论家提出的问题,这也可能是在你脑海中缠绕的问题:为什么说这本书是C和C++ 以及几个ATL而非MFC的混合?我的目标是忠实于Albert Einstein的形式语句,他推荐说任何事情都应该保持尽可能的简单,而不是比较简单。MFC有其自己的特点。为了可靠,你必须知道你在做什么。在底层的表述代码允许我们在各个层面上接触所有要求的步骤。我不知道你的感觉怎样,但是,我相信,一旦你领会了其中的技术——无论它是什么——你都能面对任何在这之上构造的东西。
事情并没有在这里停止。总有新的目标需要实现。Shell是进化的,当然我也希望这本书跟着进化。我十岁的时候就开始在TV上观看UFO和火星人。‘Windows 2000 企业版’更可能作为太空船的名字而不是操作系统的名字,然而谁能想象十年后的操作系统是什么样子,2000年的时候世界将全部是自动的世界了(很不幸,现在还没有实现)。
Windows 2000 被吹捧为操作系统之父:完全的即插即用,象光速一样快,思维驱动设备。是不是都是真的,我们可以说Windows2000 给我们带来了许多要学习的新东西,增强的性能,排除了某些bug,以及其它的变化。有些事情不可避免地影响到Shell。因此调侃一下,准备在下一个千年阅读更多的知识。