什么是PIDL?(相关信息摘自百度百科)
新的命名空间的树根就是桌面文件夹,这从资源管理器左边的树视图中就能看到。桌面下包括我的电脑文件夹,其中包括了旧的DOS命名空间-磁盘驱动器。桌面和我的电脑明显不是文件系统的一部分,同样的特殊的文件夹,比如控制面板、打印机、回收站和网络邻居等等都不是原来意义上的文件系统了。
但不管外壳的概念如何变化,它必须是可唯一标识的,每个外壳中的文件夹和对象必须有一个唯一的“名字”,“名字”有两种类型:相对和绝对的“名字”。
项目标识符列表(IDL,Item Identifier List),在Delphi中使用TItemIDList来标识它。因为IDL主要是通过指针来进行操作的,因此通常主要使用的是它的指针形式PIDL,在Delphi中定义为PItemIDList。PIDL就是在外壳命名空间确定唯一一个元素的通用方法。
同DOS-样式的字符串类型的路径不同的是,PIDL是二进制类型的数据,同时TShItemID 和 TItemIDList 是变长的数据类型
如同有相对和绝对路径一样,同样也有相对和绝对的PIDL,一个绝对的PIDL是从命名空间的根节点桌面开始算起的,而相对PIDL通常是从其直接父对象算起的。
而以Sh开头的Shell API函数通常则使用绝对PIDL作为参数,因为它们不是类,无法代表类,因此只能使用绝对PIDL,我们在应用中一定要搞清楚两者的区别。
PIDL经常是在一个模块中被分配,而在另一个模块中被释放,比如外壳API经常会在函数内部分配并返回一个PIDL,这时我们的程序就要负责在使用后进行释放。这意味着内存的分配和释放必须是语言无关的
为了解决不同的开发语言的内存管理函数是完全不兼容的问题,
操作系统提供了外壳任务分配器(shell task allocator)来统一外壳内存管理。
外壳任务分配器是通过IMalloc COM接口实现的。IMalloc实现了一个非常完整的内存分配引擎,它定义在ActiveX单元中,获得一个IMalloc接口实例最简单的办法是使用SHGetMalloc API函数
如果不需要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。
https://vimsky.com/examples/detail/cpp-ex---IShellFolder-GetUIObjectOf-method.html
实际应用起来太复杂,不过不要紧,有三个未公开的函数可以帮助我们简化这一功能的实现:
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;
如何从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的拷贝,给定一个已有的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; stdcal
删改
如果想删除整个PIDL,只要使用ILFree 函数就可以了,如果想从列表的末尾删除最后一个item identifier,可以使用ILRemoveLastID 函数:
function ILRemoveLastID(PIDL: PItemIDList):LongBool; stdcall;
它的索引值为17,要注意的是它并不真的释放任何内存,它只是重置了列表的最后位置。它是唯一一个删除相关操作的函数,如果我们想从PIDL的开始删除一个item identifier,就只能使用ILGetNext 和ILClone 来生成一个从原始PIDL的第二个ID开始的拷贝了,然后使用ILFree删除源PIDL。从列表的中间删除一个ID显然更加麻烦了,但幸运的是在实际中几乎不存在这种需要。
如何遍历命名空间?
1、命名空间的根节点是什么?遍历的前提是什么?
桌面是遍历命名空间的根节点,从桌面开始,可以枚举外壳中的所有对象。在开始遍历命名空间前,需要获得桌面对象的IShellFolder接口。
OleCheck(SHGetDesktopFolder(Desktop));
2、IShellFolder能做哪些事?
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 类。
扩展TShellNode 的类可以添加系统图像列表索引属性、查找能力等等,这完全取决于你的想像力。还有一点是除了桌面外,微软公司还定义了一组CoClasses对象,它们都暴露了IShellFolder 接口,我们也可以从它们出发来遍历命名空间,表2.9列出了这些CoClass的定义和描述。
举例来说,可以使用下面代码来创建一个简单的打印机选择组合列表框:
EnumShellNamespace(ComboBox.Items, 1,
CreateCOMObject(CLSID_Printers) as IShellFolder);
在例子程序中,我们从TShellNode类又衍生了一个TShellTreeNode 类,添加了图像索引和Strings属性。ImageIndex 属性对应于系统图像列表中的节点的图像索引,Strings 属性则保存着节点的绝对PIDL列表中每一项的显示名称。程序允许我们在绝对和相对PIDL察看模式间切换。图2.16就是程序中显示的外壳对象树的示意图。
例子程序的主要目的是演示如何进行PIDL的操作,在GetItemListStrings过程中,演示了如何使用ILClone、ILFindChild、ILFree、ILGetCount、ILIsRoot和ILRemoveLastID等例程。
显示属性页
IShellFolder接口不仅提供对外壳内部数据结构的存取,也可以调用界面元素进行交互。例如,使用IShellFolder.GetUIObjectOf 方法,可以请求上下文相关菜单。在下面代码中演示了如何操作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(Desktop.BindToObject(
ParentList, nil, IID_IShellFolder,Folder));
RelativeList := ILFindChild(ParentList, ItemList);
OleCheck(Folder.GetUIObjectOf(Handle, 1, RelativeList,
IID_IContextMenu, nil, ContextMenu));
FillChar(CommandInfo, SizeOf(TCMInvokeCommandInfo), #0);
with CommandInfo do
begin
cbSize := SizeOf(TCMInvokeCommandInfo);
hwnd := Handle;
lpVerb := 'Properties';
nShow := SW_SHOW;
end;
OleCheck(ContextMenu.InvokeCommand(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;
其他用途
IShellFolder并不是使用PIDL的唯一接口,
其他像文件快捷方式、外壳扩展等都利用PIDL来扩展或嵌入外壳。
Windows还提供了一组公开的使用PIDL的函数,比如
调用SHGetSpecialFolderLocation 函数就可以由PIDL获得特色文件夹的相应文件路径。
而用SHGetDataFromIDList 函数可以查询文件系统或网络资源中的PIDL来获得相应属性。
———以上内容从百度百科“PIDL”整理。