从Windows 95开始,微软公司为操作系统引入了新的外壳界面,新的外壳从根本上改变了应用程序同操作系统的结合方式,遗憾的是微软公司对于发布同外壳相关的编程信息方面显得很吝啬,可以得到的资料非常少,而且质量也不高。对于Delphi开发者来说,情况就更为严重了,因为几乎所有的Windows API 文档都是针对C/C++程序员的,但是Nothing is impossible,在本文中,我们将开始外壳编程的历险,就让我们从PIDL开始吧。
新外壳系统中的一个核心概念就是命名空间(namespace),对于DOS来说,命名空间可以理解为就是整个文件系统,它有着树一样的继承关系,它的树根被称为"根目录"。
对于Windows 9x和NT来说,命名空间仍然是树状继承关系的,但它不再一一对应于文件系统了,文件系统变成了一个大的命名空间的一部分,新的命名空间发展了原有的文件夹和文件概念,新的文件夹仍然类似于旧的DOS目录,包含其他的命名空间元素,比如文件夹和外壳对象。而新的外壳对象同旧的DOS文件不同之处在于,所有的系统目录都是文件夹,但并不是所有的文件夹都是目录,所有的文件都是外壳对象,但不是所有的外壳对象都是文件。
新的命名空间的树根就是桌面文件夹,这从资源管理器左边的树视图中就能看到。桌面下包括我的电脑文件夹,其中包括了旧的DOS命名空间-磁盘驱动器。桌面和我的电脑明显不是文件系统的一部分,同样的特殊的文件夹,比如控制面板、打印机、回收站和网络邻居等等都不是原来意义上的文件系统了。
但不管外壳的概念如何变化,它必须是可唯一标识的,每个外壳中的文件夹和对象必须有一个唯一的“名字”,“名字”有两种类型:相对和绝对的“名字”。相对“名字”是指相对一个给定的父对象,它是唯一的,比如我叫张三,我哥哥叫张大和张二,那么对于我的父亲来说,我的名字就可以唯一地确定我的身份了。但如何从全国所有名叫张三的同胞中找出我来呢,这就需要绝对的名字了,这时就应该用中国北京某胡同的张大胡子的儿子张三来唯一地确定我了,对于外壳对象来说,相对于根节点的路径就可以用来唯一确定它的绝对“名字”。
对于老的DOS文件系统,每个文件都有一个唯一确定的路径名,这个路径名就相当于它的绝对名字,它的格式通常就是C:\windows\system\…\8.3文件名,而单独的8.3-样式的文件名字则是相对名。
对于新的Windows 9x系统,这种DOS方式的路径名已经不够用了,它无法描述控制面板这类外壳对象的名字。为此微软公司给出了两个新的数据结构。每个元素的相对名字用一个TShItemID记录来标识,当需要时我们可以合并这些记录,从概念上类似于用”\”连接DOS路径名。而一连串的这些记录就是项目标识符列表(IDL,Item Identifier List),在Delphi中使用TItemIDList来标识它。因为IDL主要是通过指针来进行操作的,因此通常主要使用的是它的指针形式PIDL,在Delphi中定义为PItemIDList。PIDL就是在外壳命名空间确定唯一一个元素的通用方法。所有这些Delphi数据结构都定义在ShlObj单元中。
同DOS-样式的字符串类型的路径不同的是,PIDL是二进制类型的数据,同时TShItemID 和 TItemIDList 是变长的数据类型,其中TShItemID的定义如下:
TShItemID = packed record
cb: Word; // 记录的大小
abID: array[0..0] of Byte; // 外壳对象 ID数据
end;
第一个记录成员是cb,cb 中应该存放整个TShItemID记录的尺寸。而abID 被定义为只有一个元素的字节数组,但这并不意味着数组中只有一个元素,它可以扩展为cb个元素。另外TItemIDList 定义如下:
TItemIDList = packed record
mkid: TShItemID;
end;
它只是有一个TShItemID类型的数据成员构成,需要注意的是这种定义方法意味着记录并不仅是一个TShItemID成员,而是一个TShItemID结构的列表,一个挨着一个,最后要使用一个cb为0的TshItemID标识列表的结束。表2.7中给出了一个TItemIDList的示意图,它由4个TShItemID 记录组成,注意cb 总是比abID的字节大2,除了列表结束的标志记录的cb,这是因为cb 应该包含cb成员本身的字节大小,而它正好是2。
在实际应用中,PIDL经常是在一个模块中被分配,而在另一个模块中被释放,比如外壳API经常会在函数内部分配并返回一个PIDL,这时我们的程序就要负责在使用后进行释放。这意味着内存的分配和释放必须是语言无关的,也就是说可以用C++写PIDL分配模块,而用Delphi写释放模块。
但实际上不同的开发语言的内存管理函数是完全不兼容的,如果使用Delphi的Freemem 过程来释放一些C语言的Malloc函数分配的内存的话,产生的糟糕后果就是会破坏整个堆。为了解决这一问题,操作系统提供了外壳任务分配器(shell task allocator)来统一外壳内存管理。外壳任务分配器是通过IMalloc COM接口实现的。IMalloc实现了一个非常完整的内存分配引擎,它定义在ActiveX单元中,获得一个IMalloc接口实例最简单的办法是使用SHGetMalloc API函数,这个函数定义在ShlObj 单元中,这些声明定义如下:
IMalloc = interface(IUnknown)
['']
function Alloc(cb: Longint): Pointer; stdcall;
function Realloc(pv: Pointer; cb: Longint):
Pointer; stdcall;
procedure Free(pv: Pointer); stdcall;
function GetSize(pv: Pointer): Longint; stdcall;
function DidAlloc(pv: Pointer): Integer; stdcall;
procedure HeapMinimize; stdcall;
end;
function SHGetMalloc(var ppMalloc: IMalloc):HRESULT; stdcall;
下面是一个使用分配引擎的例子:
var
Allocator: IMalloc;
Buffer: Pointer;
begin
// 获得IMalloc 接口
SHGetMalloc(Allocator);
// 分配50个字节的缓冲区
Buffer := Allocator.Alloc(50);
// 扩展缓冲区为100 字节
Buffer := Allocator.Realloc(Buffer,100);
//释放缓冲区
Allocator.Free(Buffer);
end;
如果不需要IMalloc接口提供的全部功能,而只是想分配或释放内存的话,有两个未经公开的函数SHAlloc 和SHFree封装了对IMalloc接口的调用来分配和释放内存,它们在SHELL32.DLL中的索引分别为196和195。当要想释放一个PIDL时,可以使用ILFree 这个未公开的函数,它的索引值为155,三个函数的定义如下:
function SHAlloc(BufferSize: ULONG): Pointer; stdcall;
procedure SHFree(Buffer: Pointer); stdcall;
procedure ILFree(Buffer: PItemIDList); stdcall;
如何将文件系统的路径转化为外壳形式的PIDL呢?微软公司的文档中记载的标准方式是先获得桌面的IShellFolder 接口,然后把要转化的路径名转化为PWideChar 类型的以null结尾的UNICODE字符串,然后作为参数调用桌面的IShellFolder接口的ParseDisplayName 方法才能获得PIDL。实际应用起来太复杂,不过不要紧,有三个未公开的函数可以帮助我们简化这一功能的实现:
function SHILCreateFromPath(Path: Pointer;
PIDL: PItemIDList; var Attributes: ULONG):HResult; stdcall;
function ILCreateFromPath(Path: Pointer):PItemIDList; stdcall;
function SHSimpleIDListFromPath(Path: Pointer):
PItemIDList; stdcall;
SHILCreateFromPath 函数实际上就是对桌面的IShellFolder接口的ParseDisplayName方法进行简单封装,而ILCreateFromPath函数则是对SHILCreateFromPath调用的简单封装,而SHSimpleIDListFromPath函数则实现了整个过程,它们的索引分别是28,157和162。
其中SHSimpleIDListFromPath 相对要快一些,因为它并不校验路径参数的有效性,而SHILCreateFromPath 和ILCreateFromPath 在转化前都要校验路径的有效性。如果提供的路径是无效的,就会返回一个nil。
由于SHSimpleIDListFromPath 不校验路径,所以可以从任何路径获得一个PIDL而不会引起错误,但是有时这个函数返回的PIDL不完全正确,比如用它产生的PIDL来调用SHBrowseForFolder 函数显示浏览对话框的时候,偶尔结果显示的名字和图标是不正确的。
当想从一个绝对PIDL获得一个文件系统路径时,就相对简单多了,有一个公开的函数SHGetPathFromIDList可以实现这一功能,它定义在ShlObj单元中(有AnsiChar和widechar两个版本):
function SHGetPathFromIDList(PIDL: PItemIDList;
Path: PAnsiChar): BOOL; stdcall;
function SHGetPathFromIDListW(PIDL: PItemIDList;
Path: PWideChar): BOOL; stdcall;
注意:path参数对应的指针应该指向一个可以容纳max_path+1个字符的缓冲区,以避免越界读写。
显示名称
如果想要获得一个PIDL对应的显示名称,文档中介绍的方法是使用IShellFolder接口的GetDisplayNameOf方法来完成,另外使用SHGetFileInfo API函数也能获得显示名。不过有一个未公开的API调用ILGetDisplayName函数使用起来是最方便的,它实际上就是调用桌面的IShellFolder接口的GetDisplayNameOf 方法,同时调用的标志值为SHGDN_FORPARSING。ILGetDisplayName 函数的索引值为15。不过这个函数不会返回通常的短显示名,而是返回包含了相应路径的长显示名。如果想得到的是短文件名的话,最好使用SHGetFileInfo函数。下面是函数的定义:
function ILGetDisplayName(PIDL: PItemIDList;
Name: Pointer): LongBool; stdcall;
Windows NT和PWideChar
回头看一下已经定义的未公开的函数就会发现通常字符串类型的变量,并没有定义为Pchar而是定义为Pointer,这是因为对于未公开的函数来说,在Windows 9x上字符串变量都是PAnsiChar类型的,而在NT上都是PWideChar类型的。没有办法像公开的函数那样可以任选ANSI或UNICODE版本的函数,未公开函数在Windows 9x上只能使用ANSI版本,在Windows NT 上只能使用UNICODE版本的函数。如果想在所有版本的操作系统上都能正常工作,就必须在运行时检查操作系统类型,SysUtils单元中的Win32Platform 全局变量可以用来判断操作系统类型。如果程序是运行在Windows NT上的,在调用前就需要把字符串变量转化为PWideChar 类型,当函数返回时,又需要把返回字符串变回PAnsiChar。这种转化比较麻烦,但这就是使用未公开函数调用的代价。
如果想确定两个PIDL是否相同,标准方法是使用IShellFolder接口的CompareIDs 方法,相对的PIDLs 可以用他们父文件夹的IShellFolder接口,而绝对PIDLs的比较必须使用桌面的IShellFolder接口。同样的,系统也提供了未公开的快捷方法,要想确定两个PIDL是否相等,可以使用ILIsEqual 函数,如果想确定一个PIDL是否是另一个PIDL的子对象,可以使用ILIsParent 函数。如果希望判断子对象是否是父对象的最直接的子对象的话,需要设定函数的ImmediateParent 参数为True,下面的就是函数的定义:
function ILIsEqual(PIDL1: PItemIDList; PIDL2: PItemIDList):
LongBool; stdcall;
function ILIsParent(PIDL1: PItemIDList;
PIDL2: PItemIDList; ImmediateParent: LongBool):
LongBool; stdcall;
这两个函数的索引值分别为21和23。要注意的是通过二进制的比较是无法判断两个PIDL是否相等的,因为相等的PIDL可能会有不同的二进制结构。
有时,我们会想要分解一个PIDL为单独的ID列表,没有公开的函数可以实现这项功能,很显然,微软公司希望程序员自己实现切割PIDL的功能,幸运的是还是有未公开的函数可以简化开发。
如果我们想确定PIDL中所有标识符的尺寸,可以使用ILGetSize 函数。如果想遍历PIDL中每一个项目标识符的话,可以使用ILGetNext 函数。当给定一个PIDL后,函数会返回一个指向列表中下一个项目标识符的指针。如果PIDL为nil或已经指向了列表中的最后一项,函数会返回nil。要想返回列表中最后一项item identifier,可以使用未公开的ILFindLastID函数。
一个更专业的查找函数是ILFindChild ,给定一个父PIDL和一个子PIDL,它将返回一个指向子PIDL独特部分的指针。比如,如果你把目录 'C:\DIR'的PIDL作为父PIDL,而把”C:\DIR\FILE.TXT “的PIDL作为子PIDL的话,它会返回一个指针指向代表FILE.TXT的子PIDL。如果给定的子PIDL不是父PIDL的子对象,函数返回nil。这些函数的索引值分别为152、153、16和24,函数定义如下:
function ILGetSize(PIDL: PItemIDList): UINT; stdcall;k
function ILGetNext(PIDL: PItemIDList):
PItemIDList; stdcall;
function ILFindLastID(PIDL: PItemIDList):
PItemIDList; stdcall;
function ILFindChild(ParentPIDL: PItemIDList;
ChildPIDL: PItemIDList): PItemIDList; stdcall;
复制和合并
有时在进行外壳编程的时候需要制作一个PIDL的拷贝,给定一个已有的PIDL, ILClone 函数将会分配并返回一个新的PIDL的克隆。而ILCloneFirst 函数可以从源PIDL中生成一个只包含第一个item identifier的PIDL。如果想获得最后一个item identifier的拷贝,组合使用ILFindLastID和ILCloneFirst函数调用就可以了。对于PIDL的其他部分,就需要不断调用ILGetNext和ILCloneFirst函数了。这两个函数定义如下,其索引值为18和19:
function ILClone(PIDL: PItemIDList): PItemIDList; stdcall;
function ILCloneFirst(PIDL: PItemIDList):
PItemIDList; stdcall;
如果想合并两个PIDL,则可以使用ILCombine 函数,给定两个PIDL,它会创建一个包含两个源列表的新的PIDL。如果想把一个单独的item identifier同PIDL合并,可能需要使用ILAppendID 函数。它可以把一个TItemID 记录添加到一个已有的PIDL的开头或结尾。然而同ILCombine不同,原来的PIDL在操作后将被销毁。ILAppendID 函数中的PIDL参数甚至可以为nil。这两个函数的索引值分别为25和154,函数定义如下:
function ILCombine(PIDL1: PItemIDList; PIDL2: PItemIDList):
PItemIDList; stdcall;
function ILAppendID(PIDL: PItemIDList; ItemID: PShItemID;
AddToEnd: LongBool): PItemIDList; stdcall;
全局内存克隆
前面已经提到了,为PIDL分配内存需要使用外壳内存分配器,系统中有两个未公开的函数提供了不同的分配和释放内存的方法。它们是ILGlobalClone和ILGlobalFree 函数(索引值为20和156)。函数定义如下:
function ILGlobalClone(PIDL: PItemIDList):
PItemIDList; stdcall;
procedure ILGlobalFree(PIDL: PItemIDList); stdcall;
在Windows NT中,这两个函数使用缺省进程的堆(由getprocessheap得到的)。堆的分配在某些方面比外壳分配器效率更高,而外壳在内部使用全局分配函数可以提高效率。
在Windows 9x 上外壳中的绝大多数内部结构都需要在DLL的所有实例中共享,同样PIDL使用的内存也应该是可共享的。ILGlobalClone 使用一个可共享的堆来分配PIDL的内存,使得可以从任何地方存取PIDL的指针。
删改
如果想删除整个PIDL,只要使用ILFree 函数就可以了,如果想从列表的末尾删除最后一个item identifier,可以使用ILRemoveLastID 函数:
function ILRemoveLastID(PIDL: PItemIDList):LongBool; stdcall;
它的索引值为17,要注意的是它并不真的释放任何内存,它只是重置了列表的最后位置。它是唯一一个删除相关操作的函数,如果我们想从PIDL的开始删除一个item identifier,就只能使用ILGetNext 和ILClone 来生成一个从原始PIDL的第二个ID开始的拷贝了,然后使用ILFree删除源PIDL。从列表的中间删除一个ID显然更加麻烦了,但幸运的是在实际中几乎不存在这种需要。
深入命名空间
现在我们对PIDL已经有了一定程度的了解了,接下来就是研究如何遍历命名空间。桌面是遍历命名空间的根节点,从桌面开始,可以枚举外壳中的所有对象。在开始遍历命名空间前,需要获得桌面对象的IShellFolder接口,下面的代码演示了如何获得桌面接口:
var
Desktop: IShellFolder;
Begin
OleCheck(SHGetDesktopFolder(Desktop));
...
IShellFolder 可以用来枚举外壳中的内容,设定或取得外壳对象的名字,查询它们的属性并通过界面元素进行交互。下面是一个使用IShellFolder 接口的例子:
type
TItemListArray = array of PItemIDList;
...
function GetShellItems(
Folder: IShellFolder): TItemListArray;
Const
SHCONTF_ALL=SHCONTF_FOLDERSorSHCONTF_NONFOLDERSor
SHCONTF_INCLUDEHIDDEN;
Var
EnumList: IEnumIDList;
NewItem: PItemIDList;
Dummy: Cardinal;
I: Integer;
Begin
Result := nil;
I := 0;
if Folder.EnumObjects(
0, SHCONTF_ALL, EnumList) = S_OK then
while EnumList.Next(1, NewItem, Dummy) = S_OK do
begin
Inc(I);
SetLength(Result, I);
Result[I - 1] := NewItem;
end;
end;
GetShellFolders 函数返回一组相对于父文件夹的PIDL列表。通过EnumObjects方法可以获得PIDL枚举接口,不过最终要负责释放全部结果中的项目。
function GetShellObjectName(Folder: IShellFolder;
ItemList: PItemIDList): string;
Var
StrRet: TStrRet;
Begin
Folder.GetDisplayNameOf(ItemList, SHGDN_INFOLDER, StrRet);
case StrRet.uType of
STRRET_WSTR:
Begin
Result := WideCharToString(StrRet.pOleStr);
CoTaskMemFree(StrRet.pOleStr);
end;
STRRET_OFFSET: Result := PChar(Cardinal(ItemList) + StrRet.uOffset);
STRRET_CSTR: Result := StrRet.cStr;
end;
end;
GetShellObjectName 函数则返回一个相对的PIDL的字符串表达。把这些代码集成起来,就可以编写一个过程来输出指定深度的外壳命名空间的层次关系了:
procedure EnumShellNamespace(Strings: TStrings; Depth: Integer;
Folder: IShellFolder = nil);
procedure AddObjectName(Folder: IShellFolder; ItemList: PItemIDList; Level: Integer);
Var
S: string;
Begin
SetLength(S, Level * 2);
fillchar(PChar(S)^, Length(S), ' ');
Strings.Add(S + GetShellObjectName(Folder, ItemList));
end;
procedure EnumItems(Folder: IShellFolder; Level: Integer);
var
Items: TItemListArray;
ItemList: PItemIDList;
Flags: Cardinal;
SubFolder: IShellFolder;
I: Integer;
Begin
Inc(Level);
Items := GetShellItems(Folder);
Try
for I := 0 to Length(Items) - 1 do
begin
ItemList := Items[I];
AddObjectName(Folder, ItemList, Level);
if Level < Depth then
begin
Flags := SFGAO_HASSUBFOLDER;
OleCheck(Folder.GetAttributesOf(1, ItemList, Flags));
if Flags and SFGAO_HASSUBFOLDER = SFGAO_HASSUBFOLDER then
Begin
OleCheck(Folder.BindToObject(
ItemList, nil,IID_IShellFolder, SubFolder));
EnumItems(SubFolder, Level);
end;
end;
end;
finally
for I := 0 to Length(Items) - 1 do
ILFree(Items[I]);
end;
begin
Strings.BeginUpdate;
Try
Strings.Clear;
if Folder = nil then
begin
OleCheck(SHGetDesktopFolder(Folder));
AddObjectName(Folder, nil, 0);
end;
if Depth > 0 then
EnumItems(Folder, 0);
Finally
Strings.EndUpdate;
end;
end;
end.
对于Delphi来说,由于其提供了一个非常友好的对象框架,所以这里对IShellFolder的功能进行了封装,实现了一个TShellNode 类。表2.8对TShellNode类进行了描述:
TShellNode被设计成一个基类,可以从它继承更加有用的类来,一些在表2.8中列出的属性和方法是protected的,需要在