PIDL的秘密
从Windows 开始微软公司为操作系统引入了新的外壳界面新的外壳从根本上改变了应用程序同操作系统的结合方式遗憾的是微软公司对于发布同外壳相关的编程信息方面显得很吝啬可以得到的资料非常少而且质量也不高对于Delphi开发者来说情况就更为严重了因为几乎所有的Windows API 文档都是针对C/C++程序员的但是Nothing is impossible在本文中我们将开始外壳编程的历险就让我们从PIDL开始吧
外壳命名空间
新外壳系统中的一个核心概念就是命名空间(namespace)对于DOS来说命名空间可以理解为就是整个文件系统它有着树一样的继承关系它的树根被称为根目录
对于Windows x和NT来说命名空间仍然是树状继承关系的但它不再一一对应于文件系统了文件系统变成了一个大的命名空间的一部分新的命名空间发展了原有的文件夹和文件概念新的文件夹仍然类似于旧的DOS目录包含其他的命名空间元素比如文件夹和外壳对象而新的外壳对象同旧的DOS文件不同之处在于所有的系统目录都是文件夹但并不是所有的文件夹都是目录所有的文件都是外壳对象但不是所有的外壳对象都是文件
新的命名空间的树根就是桌面文件夹这从资源管理器左边的树视图中就能看到桌面下包括我的电脑文件夹其中包括了旧的DOS命名空间磁盘驱动器桌面和我的电脑明显不是文件系统的一部分同样的特殊的文件夹比如控制面板打印机回收站和网络邻居等等都不是原来意义上的文件系统了
但不管外壳的概念如何变化它必须是可唯一标识的每个外壳中的文件夹和对象必须有一个唯一的名字名字有两种类型相对和绝对的名字相对名字是指相对一个给定的父对象它是唯一的比如我叫张三我哥哥叫张大和张二那么对于我的父亲来说我的名字就可以唯一地确定我的身份了但如何从全国所有名叫张三的同胞中找出我来呢这就需要绝对的名字了这时就应该用中国北京某胡同的张大胡子的儿子张三来唯一地确定我了对于外壳对象来说相对于根节点的路径就可以用来唯一确定它的绝对名字
对于老的DOS文件系统每个文件都有一个唯一确定的路径名这个路径名就相当于它的绝对名字它的格式通常就是C:\windows\system\…\文件名而单独的样式的文件名字则是相对名
对于新的Windows x系统这种DOS方式的路径名已经不够用了它无法描述控制面板这类外壳对象的名字为此微软公司给出了两个新的数据结构每个元素的相对名字用一个TShItemID记录来标识当需要时我们可以合并这些记录从概念上类似于用\连接DOS路径名而一连串的这些记录就是项目标识符列表(IDLItem Identifier List)在Delphi中使用TItemIDList来标识它因为IDL主要是通过指针来进行操作的因此通常主要使用的是它的指针形式PIDL在Delphi中定义为PItemIDListPIDL就是在外壳命名空间确定唯一一个元素的通用方法所有这些Delphi数据结构都定义在ShlObj单元中
同DOS样式的字符串类型的路径不同的是PIDL是二进制类型的数据同时TShItemID 和 TItemIDList 是变长的数据类型其中TShItemID的定义如下
TShItemID = packed record
cb: Word; // 记录的大小
abID: array[] of Byte; // 外壳对象 ID数据
end;
第一个记录成员是cbcb 中应该存放整个TShItemID记录的尺寸而abID 被定义为只有一个元素的字节数组但这并不意味着数组中只有一个元素它可以扩展为cb个元素另外TItemIDList 定义如下
TItemIDList = packed record
mkid: TShItemID;
end;
它只是有一个TShItemID类型的数据成员构成需要注意的是这种定义方法意味着记录并不仅是一个TShItemID成员而是一个TShItemID结构的列表一个挨着一个最后要使用一个cb为的TshItemID标识列表的结束下表中给出了一个TItemIDList的示意图它由个TShItemID 记录组成注意cb 总是比abID的字节大除了列表结束的标志记录的cb这是因为cb 应该包含cb成员本身的字节大小而它正好是
cbabIDcbabIDcbabIDcbabID
bytes
bytes
bytes
bytes
从表中就可以清楚地知道cb的用途了它可以被用来作为可靠的路标来遍历一个TItemIDListPItemIDList指针指向TItemIDList记录的第一个字节除非PItemIDList 为nil否则列表中至少会有一个TShItemID 然后通过cb的值就可以知道列表中下一个TShItemID的起始位置如果cb为就表明列表结束了
下面的代码用PItemIDList作为参数然后遍历整个TItemIDList并返回整个列表的尺寸当需要复制列表时获得的信息可以用来确定复制所需缓沖区大小
function GetPIDLSize(PIDL: PItemIDList): Integer;
var
CurrentID: PShItemID;
begin
// 判断PIDL是否为nil
if (PIDL <> nil) then
begin
// 对于终止的标志的cb至少为
Result := SizeOf(CurrentIDcb);
// 初始化item id 指针并遍历列表直到碰到cb = 才终止
// 把碰到的每个cb的值添加到结果中
CurrentID := PShItemID(PIDL);
while (CurrentIDcb <> ) do begin
Inc(Result CurrentIDcb);
Inc(PChar(CurrentID) CurrentIDcb);
end
end
else
// 如果PIDL为nil返回
Result := ;
end;
如同有相对和绝对路径一样同样也有相对和绝对的PIDL一个绝对的PIDL是从命名空间的根节点桌面开始算起的而相对PIDL通常是从其直接父对象算起的
外壳中的文件夹可以通过一个IShellFolder COM接口来进行控制这个接口提供了许多方法这些方法的参数通常就是相对PIDL因为接口本身就代表了父文件夹而以Sh开头的Shell API函数通常则使用绝对PIDL作为参数因为它们不是类无法代表类因此只能使用绝对PIDL我们在应用中一定要搞清楚两者的区别
PIDL的内存分配
在实际应用中PIDL经常是在一个模块中被分配而在另一个模块中被释放比如外壳API经常会在函数内部分配并返回一个PIDL这时我们的程序就要负责在使用后进行释放这意味着内存的分配和释放必须是语言无关的也就是说可以用C++写PIDL分配模块而用Delphi写释放模块
但实际上不同的开发语言的内存管理函数是完全不兼容的如果使用Delphi的FreeMem 过程来释放一些C语言的Malloc函数分配的内存的话产生的糟糕后果就是会破坏整个堆为了解决这一问题操作系统提供了外壳任务分配器(shell task allocator)来统一外壳内存管理外壳任务分配器是通过IMalloc COM接口实现的IMalloc实现了一个非常完整的内存分配引擎它定义在ActiveX单元中获得一个IMalloc接口实例最简单的办法是使用SHGetMalloc API函数这个函数定义在ShlObj 单元中这些声明定义如下
IMalloc = interface(IUnknown)
[{ C }]
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);
// 分配个字节的缓沖区
Buffer := AllocatorAlloc();
// 扩展缓沖区为 字节
Buffer := AllocatorRealloc(Buffer);
//释放缓沖区
AllocatorFree(Buffer);
end;
如果不需要IMalloc接口提供的全部功能而只是想分配或释放内存的话有两个未经公开的函数SHAlloc 和SHFree封装了对IMalloc接口的调用来分配和释放内存它们在SHELLDLL中的索引分别为和当要想释放一个PIDL时可以使用ILFree 这个未公开的函数它的索引值为三个函数的定义如下
function SHAlloc(BufferSize: ULONG): Pointer; stdcall;
procedure SHFree(Buffer: Pointer); stdcall;
procedure ILFree(Buffer: PItemIDList); stdcall;
路径和PIDL之间的相互转换
如何将文件系统的路径转化为外壳形式的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函数则实现了整个过程它们的索引分别是和
其中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+个字符的缓沖区以避免越界读写
显示名称
如果想要获得一个PIDL对应的显示名称文档中介绍的方法是使用IShellFolder接口的GetDisplayNameOf方法来完成另外使用SHGetFileInfo API函数也能获得显示名不过有一个未公开的API调用ILGetDisplayName函数使用起来是最方便的它实际上就是调用桌面的IShellFolder接口的GetDisplayNameOf 方法同时调用的标志值为SHGDN_FORPARSINGILGetDisplayName 函数的索引值为不过这个函数不会返回通常的短显示名而是返回包含了相应路径的长显示名如果想得到的是短文件名的话最好使用SHGetFileInfo函数下面是函数的定义
function ILGetDisplayName(PIDL: PItemIDList;Name: Pointer): LongBool; stdcall;
Windows NT和PWideChar
回头看一下已经定义的未公开的函数就会发现通常字符串类型的变量并没有定义为Pchar而是定义为Pointer这是因为对于未公开的函数来说在Windows x上字符串变量都是PAnsiChar类型的而在NT上都是PWideChar类型的没有办法像公开的函数那样可以任选ANSI或UNICODE版本的函数未公开函数在Windows x上只能使用ANSI版本在Windows NT 上只能使用UNICODE版本的函数如果想在所有版本的操作系统上都能正常工作就必须在运行时检查操作系统类型SysUtils单元中的WinPlatform 全局变量可以用来判断操作系统类型如果程序是运行在Windows NT上的在调用前就需要把字符串变量转化为PWideChar 类型当函数返回时又需要把返回字符串变回PAnsiChar这种转化比较麻烦但这就是使用未公开函数调用的代价
如果想确定两个PIDL是否相同标准方法是使用IShellFolder接口的CompareIDs 方法相对的PIDLs 可以用他们父文件夹的IShellFolder接口而绝对PIDLs的比较必须使用桌面的IShellFolder接口同样的系统也提供了未公开的快捷方法要想确定两个PIDL是否相等可以使用ILIsEqual 函数如果想确定一个PIDL是否是另一个PIDL的子对象可以使用ILIsParent 函数如果希望判断子对象是否是父对象的最直接的子对象的话需要设定函数的ImmediateParent 参数为True下面的就是函数的定义
function ILIsEqual(PIDL: PItemIDList; PIDL: PItemIDList):LongBool; stdcall;
function ILIsParent(PIDL: PItemIDList;PIDL: PItemIDList; ImmediateParent: LongBool):LongBool; stdcall;
这两个函数的索引值分别为和要注意的是通过二进制的比较是无法判断两个PIDL是否相等的因为相等的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\FILETXT 的PIDL作为子PIDL的话它会返回一个指针指向代表FILETXT的子PIDL如果给定的子PIDL不是父PIDL的子对象函数返回nil这些函数的索引值分别为和函数定义如下
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函数了这两个函数定义如下其索引值为和
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这两个函数的索引值分别为和函数定义如下
function ILCombine(PIDL: PItemIDList; PIDL: PItemIDList):PItemIDList; stdcall;
function ILAppendID(PIDL: PItemIDList; ItemID: PShItemID;
AddToEnd: LongBool): PItemIDList; stdcall;
全局内存克隆
前面已经提到了为PIDL分配内存需要使用外壳内存分配器系统中有两个未公开的函数提供了不同的分配和释放内存的方法它们是ILGlobalClone和ILGlobalFree 函数(索引值为和)函数定义如下
function ILGlobalClone(PIDL: PItemIDList):PItemIDList; stdcall;
procedure ILGlobalFree(PIDL: PItemIDList); stdcall;
在Windows NT中这两个函数使用缺省进程的堆(由GetProcessHeap得到的)堆的分配在某些方面比外壳分配器效率更高而外壳在内部使用全局分配函数可以提高效率
在Windows x 上外壳中的绝大多数内部结构都需要在DLL的所有实例中共享同样PIDL使用的内存也应该是可共享的ILGlobalClone 使用一个可共享的堆来分配PIDL的内存使得可以从任何地方存取PIDL的指针
删改
如果想删除整个PIDL只要使用ILFree 函数就可以了如果想从列表的末尾删除最后一个item identifier可以使用ILRemoveLastID 函数
function ILRemoveLastID(PIDL: PItemIDList):LongBool; stdcall;
它的索引值为要注意的是它并不真的释放任何内存它只是重置了列表的最后位置它是唯一一个删除相关操作的函数如果我们想从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 := ;
if FolderEnumObjects( SHCONTF_ALL EnumList) = S_OK then
while EnumListNext( NewItem Dummy) = S_OK do
begin
Inc(I);
SetLength(Result I);
Result[I ] := NewItem;
end;
end;
GetShellFolders 函数返回一组相对于父文件夹的PIDL列表通过EnumObjects方法可以获得PIDL枚举接口不过最终要负责释放全部结果中的项目
function GetShellObjectName(Folder: IShellFolder;
ItemList: PItemIDList): string;
Var
StrRet: TStrRet;
Begin
FolderGetDisplayNameOf(ItemList SHGDN_INFOLDER StrRet);
case StrRetuType of STRRET_WSTR:
Begin
Result := WideCharToString(StrRetpOleStr);
CoTaskMemFree(StrRetpOleStr);
end;
STRRET_OFFSET: Result := PChar(Cardinal(ItemList) + StrRetuOffset);
STRRET_CSTR: Result := StrRetcStr;
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 * );
FillChar(PChar(S)^ Length(S) );
StringsAdd(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 := to Length(Items) do
begin
ItemList := Items[I];
AddObjectName(Folder ItemList Level);
if Level < Depth then
begin
Flags := SFGAO_HASSUBFOLDER;
OleCheck(FolderGetAttributesOf( ItemList Flags));
if Flags and SFGAO_HASSUBFOLDER = SFGAO_HASSUBFOLDER then
Begin
OleCheck(FolderBindToObject(
ItemList nilIID_IShellFolder SubFolder));
EnumItems(SubFolder Level);
end;
end;
end;
finally
for I := to Length(Items) do
ILFree(Items[I]);
end;
begin
StringsBeginUpdate;
Try
StringsClear;
if Folder = nil then
begin
OleCheck(SHGetDesktopFolder(Folder));
AddObjectName(Folder nil );
end;
if Depth > then
EnumItems(Folder );
Finally
StringsEndUpdate;
end;
end;
end
对于Delphi来说由于其提供了一个非常友好的对象框架所以这里对IShellFolder的功能进行了封装实现了一个TShellNode 类对TShellNode类进行了描述
AbsoluteList 标识节点的绝对PIDL指针
CountItems属性中的节点数
HasChildren表示是否有子节点
Item子TShellNode对象
Name节点的显示名称
Path节点对应的系统文件路径
Parent对象的父节点对桌面来说这个属性为nil
RelativeList相对PIDL指针
ShellFolder对应于节点的IShellFolder接口
Create创建节点
CreateFromList根据PIDL创建节点
CreateFromFolder由特殊路径创建节点
Destroy析构函数
Assign复制节点信息
Clear释放节点的子对象
Initialize初始化方法
TShellNode被设计成一个基类可以从它继承更加有用的类来一些在表中列出的属性和方法是protected的需要在继承类中声明为public衍生类不应该重新定义constructors过程但可以重载Initialize方法
扩展TShellNode 的类可以添加系统图像列表索引属性查找能力等等这完全取决于你的想像力还有一点是除了桌面外微软公司还定义了一组CoClasses对象它们都暴露了IShellFolder 接口我们也可以从它们出发来遍历命名空间下面列出这些CoClass的定义和描述
CLSID_NetworkPlaces网络邻居
CLSID_NetworkDomain网络域
CLSID_NetworkServer网络服务器
CLSID_NetworkShare网络共享
CLSID_MyComputer我的电脑
CLSID_Internet我的网络
CLSID_ShellFSFolder文件系统的桌面目录
CLSID_RecycleBin回收站
CLSID_ControlPanel控制面板
CLSID_Printers打印机
CLSID_MyDocuments我的文档
举例来说可以使用下面代码来创建一个简单的打印机选择组合列表框
EnumShellNamespace(ComboBoxItems
CreateCOMObject(CLSID_Printers) as IShellFolder);
在例子程序中我们从TShellNode类又衍生了一个TShellTreeNode 类添加了图像索引和Strings属性ImageIndex 属性对应于系统图像列表中的节点的图像索引Strings 属性则保存着节点的绝对PIDL列表中每一项的显示名称程序允许我们在绝对和相对PIDL察看模式间切换下图就是程序中显示的外壳对象树的示意图
例子程序的主要目的是演示如何进行PIDL的操作在GetItemListStrings过程中演示了如何使用ILCloneILFindChildILFreeILGetCountILIsRoot和ILRemoveLastID等例程
显示属性页
IShellFolder接口不仅提供对外壳内部数据结构的存取也可以调用界面元素进行交互例如使用IShellFolderGetUIObjectOf 方法可以请求上下文相关菜单在下面代码中演示了如何操作PIDL来获得IContextMenu 接口并通过IContextMenu来调用菜单命令比如显示属性页调用我的电脑的属性命令显示属性页的示意图下图所示
procedure ShowProperties(Handle: HWND; ItemList: PItemIDList); overload;
var
Desktop: IShellFolder;
Folder: IShellFolder;
ParentList: PItemIDList;
RelativeList: PItemIDList;
ContextMenu: IContextMenu;
CommandInfo: TCMInvokeCommandInfo;
Begin
ParentList := ILClone(ItemList);
if ParentList <> nil then
try
ILRemoveLastID(ParentList);
OleCheck(SHGetDesktopFolder(Desktop));
OleCheck(DesktopBindToObject(ParentList nil IID_IShellFolderFolder));
RelativeList := ILFindChild(ParentList ItemList);
OleCheck(FolderGetUIObjectOf(Handle RelativeList
IID_IContextMenu nil ContextMenu));
FillChar(CommandInfo SizeOf(TCMInvokeCommandInfo) #);
with CommandInfo do
begin
cbSize := SizeOf(TCMInvokeCommandInfo);
hwnd := Handle;
lpVerb := Properties;
nShow := SW_SHOW;
end;
OleCheck(ContextMenuInvokeCommand(CommandInfo));
Finally
ILFree(ParentList);
end;
end;
procedure ShowProperties(Handle: HWND;
const DisplayName: string); overload;
var
ItemList: PItemIDList;
Begin
ItemList := ILCreateFromPath(PChar(DisplayName));
Try
ShowProperties(Handle ItemList)
Finally
ILFree(ItemList);
end;
end;
PIDL的其他用途
IShellFolder并不是使用PIDL的唯一接口其他像文件快捷方式外壳扩展等都利用PIDL来扩展或嵌入外壳Windows还提供了一组公开的使用PIDL的函数比如调用SHGetSpecialFolderLocation 函数就可以由PIDL获得特色文件夹的相应文件路径而用SHGetDataFromIDList 函数可以查询文件系统或网络资源中的PIDL来获得相应属性
结论
PIDL是理解外壳的一把关键的钥匙几乎所有外壳功能的实现都离不开PIDL因此掌握PIDL的相关编程知识就显得至关重要了进一步研究下去我们还可以使用命名空间来查询网络设备存取虚拟文件夹中的对象从现在开始研究PIDL明天你将获得更加灵活的同外壳结合更加紧密的应用程序