從Windows 95開始,微軟公司為作業系統引入了新的外殼界面,新的外殼從根本上改變了應用程式同作業系統的結合方式,遺憾的是微軟公司對於發布同外殼相關的編程信息方面顯得很吝嗇,可以得到的資料非常少,而且質量也不高。
基本介紹
- 中文名:PIDL
- 平台:Windows 95
- 開發:微軟公司
- 概念:命名空間
簡介,外殼命名,記憶體分配,相互轉換,解析PIDL,其他用途,
簡介
對於Delphi開發者來說,情況就更為嚴重了,因為幾乎所有的Windows API文檔都是針對C/C++程式設計師的,但是Nothing is impossible,在本文中,我們將開始外殼編程的歷險,就讓我們從PIDL開始吧。
外殼命名
對於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。
從表中就可以清楚地知道cb的用途了,它可以被用來作為可靠的路標來遍歷一個TItemIDList。PItemIDList指針指向TItemIDList記錄的第一個位元組。除非PItemIDList 為nil,否則列表中至少會有一個TShItemID 。然後通過cb的值就可以知道列表中下一個TShItemID的起始位置。如果cb為0,就表明列表結束了。
下面的代碼用PItemIDList作為參數,然後遍歷整個TItemIDList,並返回整個列表的尺寸,當需要複製列表時,獲得的信息可以用來確定複製所需緩衝區大小。
function GetPIDLSize(PIDL: PItemIDList): Integer;
var
CurrentID: PShItemID;
begin
// 判斷PIDL是否為nil
if (PIDL <> nil) then
begin
// 對於終止的標誌的cb至少為2
Result := SizeOf(CurrentID.cb);
// 初始化item id 指針並遍歷列表直到碰到cb = 0才終止
// 把碰到的每個cb的值添加到結果中
CurrentID := PShItemID(PIDL);
while (CurrentID.cb <> 0) do begin
Inc(Result, CurrentID.cb);
Inc(PChar(CurrentID), CurrentID.cb);
end
end
else
// 如果PIDL為nil返回0
Result := 0;
end;
外殼中的資料夾可以通過一個IShellFolder COM接口來進行控制,這個接口提供了許多方法,這些方法的參數通常就是相對PIDL,因為接口本身就代表了父資料夾。
而以Sh開頭的Shell API函式通常則使用絕對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)
['{ 00000002-0000-0000-C000-000000000046 }']
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版本的函式。
function ILIsEqual(PIDL1: PItemIDList; PIDL2: PItemIDList):
LongBool; stdcall;
function ILIsParent(PIDL1: PItemIDList;
PIDL2: PItemIDList; ImmediateParent: LongBool):
LongBool; stdcall;
這兩個函式的索引值分別為21和23。要注意的是通過二進制的比較是無法判斷兩個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\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;
複製和合併
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的,需要在繼承類中聲明為public。衍生類不應該重新定義constructors過程,但可以重載Initialize方法。
擴展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來調用選單命令,比如顯示屬性頁,調用我的電腦的屬性命令顯示屬性頁的示意圖如圖2.17所示。
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來獲得相應屬性。