Windows95/98对 Dos/Win3.x作了许多重大改进,在文件系统方面,它除了采用长文件名替代 Dos中的 8.3文件名以外,引入外壳名字空间( Shell Name Space)来代 Dos文件系统是其又一大突破.本文将简要地介绍如何在 Windows 95/98或 Windows NT4.0以上版本。
-
- 简介
在Dos/Win3.x中,每个逻辑分区构成一棵目录树,文件系统由这一统一的根,而且每个目录或文件必须一一对应于文件系统中客观存在的项。但 Windows引入了“ 外壳名字空间” ( Shell Name Space)的概念之后,这一切就都变了。
外壳名字空间是 Windows下的标准文件系统,它大大扩展了 Dos文件系统,形成了以“ 桌面” ( Desktop)为根的单一的文件系统树,原有的C盘、D盘等目录树变成了“ 我的电脑” 这一外壳名字空间子树的下一级子树,而像“ 控制面板” 、“ 回收站” 、“ 网上邻居” 等应用程序及“ 打印机” 等设备也被虚拟成了外壳名字空间中的节点。另外,与DOS中物理存储只能和文件系统项一一对应这一点不同的是,一个实际目录在外壳名字空间中可以表现为不同的项。例如“ 我的文档” 与“ C;/My Documents” 其实都指向“ C;/My Documents” 目录,但它们在外壳名字空间中是不同的项。如果我们运行 Windows 自带的“ Windows资源管理器” 看一下的话,那么在它的左部树型视图中我们就可以清楚的看到整个外壳名字空间替代DOS文件系统, Windows在文件系统的组织与管理上终于有了质的飞跃。
为了区别于DOS中“ 目录” 的概念, Windows引入了“ 文件夹” ( Folder)的概念。“ 文件夹” 一般是指外壳名字空间树中的非叶节点,既可以是DOS下的目录,也可是“ 控制面板” 、“ 回收站” 这类虚拟的目录。但外壳名字空间中有些项本身并不是文件夹(即不具有文件夹属性),但却含有子文件夹,比如“ 网上邻居” 等。以下为讲座方便,我们也认为它们是文件夹。
在下面的讲座过程中我们将用“ 文件系统” 一词来指代DOS文件系统,而用“ 外壳名字空间” 一词来指代 Windows中的外壳名字空间:另外用“ 文件” 一词来指代外壳名字空间这棵树中的叶节点(虽然它们不都是物理存储上的文件)。
在 Windows中, Win3.x的文件操作函数,如 FindFirstFile、 FindNextFile、 SetCurrentDirectory等,虽然仍可使用,但用它们只能浏览文件系统,却无法浏览与操纵整个外壳名字空间。要浏览 Windows中的外壳名字空间,就必须使用一套全新的、基于COM(组件对象模型)基础上的方法。
-
- 新的“ 路径” PIDL
在讨论基于 COM的方法之前,我们先来介绍一下外壳名字空间中“ 路径” 的表示问题。 DOS的字符串路径只能表示文件系统,而无法表示整个外壳名字空间,所以外壳名字空间提供了一种“ 路径” 的替代物椩乇晔斗斜恚虺莆?/FONT>PIDL)。
PIDL是一个元素类型为 ITEMIDLIST结构的数组,数组中元素的个数是未知的,但紧接着数组末尾的必是一个双字节的零。每个数组元素代表了外壳名字空间树中的一层(即一个文件夹或文件),数组中的前一元素代表的是后一元素的父文件夹。由此可见, PIDL实际上就是指向一块由若干个顺序排列的 ITEMIDLIST结构组成、并在最后有一个双字节零的空间的指针。所以 PIDL的类型就被 Windows定义为 ITEMIDLIST结构的指针。
PIDL亦有“ 绝对路径” 与“ 相对路径” 的概念。表示“ 相对路径” 的 PIDL(本文简称为“ 相对 PIDL” )只有一个 ITEMIDLIST结构的元素,用于标识相对于父文件夹的“ 路径” ;表示“ 绝对路径” 的 PIDL(简称为“ 绝对 PIDL” )有若干个 ITEMIDLIST结构的元素,第一个元素表示外壳名字空间根文件夹(“ 桌面” )下的某一子文件夹 A,第二个元素则表示文件夹 A下的某一子文件夹 B,其余依此类推。这样绝对 PIDL就通过保存一条从“ 桌面” 下的直接子文件夹或文件的绝对 PIDL与相对 PIDL是相同的,而其他的文件夹或文件的相对 PIDL就只是其绝对 PIDL的最后一部分了。
但现在就出现了一个问题:即“ 桌面” 的表示问题。外壳名字空间中其他各项都可以用从“ 桌面” 开始的绝对 PIDL加以表示,但“ 桌面” 的 PIDL数组显然一个元素都没有。这样就只剩下 PIDL数组最后的那个双字节的零了。所以,“ 桌面” 的 PIDL就是一个 16位的零。注意:“ 桌面” 内容是一个双字节的零。另外,虽然“ 桌面” 表示的是“ C:/ Windows /Desktop” 文件夹(这里假定 Windows的系统目录为“ C:/ Windows” )的内容,但“ 桌面” 与“ C:/ Windows /Desktop” 文件夹的 PIDL是完全不同的。这一点同样适用于“ 我的文档” 与“ C:/ My Documents” 等文件夹。
DOS中的路径是一个字符串,但 PIDL是一种二进制结构,所以我们不能直接从 PIDL中获知它所代表的到底是哪个文件夹或文件,而必须调用相应的函数把它转换成代表路径的字符串。如果某绝对 PIDL是文件系统的一部分,则调用 SHGetPathFromIDList函数即可;但如不是,就无法获得路径字符串了,因为 DOS中根本就不存在这种路径。但很可惜的是, Windows并没有提供一个函数来让我们方便地把文件系统的路径字符串转换成 PIDL。不过我们可用一个我们自己实现的函数 ParsePidlFromPath()来达目的(具体函数的实现见下文)。
PIDL的创建与释放一般并不使用 C++的 new和 delete操作或 C语言的 malloc和 free函数 ,而必须使用专门的方法进行 .首先调用 SHGEetMallocI函数得到 Malloc接口 (COM接口的一种 ,关于 COM接口下面将详述 )的指针 ,再调用该接口的 Alloc方法为 PIDL分配空间 ,或调用该接口的 Free方法释放某个 PIDL占用的空间。最后调用该接口的 Release方法释放该接口。
除了下面将要介绍的 IShellFolder、 IEnumIDList等 COM接口可以操作 PIDL外,还有很多以 SH开头的 Windows API函数也可操作 PIDL,不过一般这些函数都要求使用绝对 PIDL作参数。例如 SHGetFileInfo函数可得到某一 PIDL所指对象的各种信息,包括名字、图标、属性等; SHFileOperation函数可对外壳名字空间中的项进行拷贝、移动、改名、删除等操作; SHBrowseForFolder可以显示一个让用户选择外壳名字空间中某一文件夹的浏览对话框 .
-
- 基于
- 新的“ 路径” PIDL
- 简介
讨论清楚了 PIDL的概念之后 ,我们回过头来讨论基于 COM之上的浏览外壳名字空间的方法。如果说 PIDL是外壳的名字空间中的“ 路径” 的话,那么下面所说的两个 COM接口 IshellFolder与 IEnumIDList就起着与 Win 3.x中的 FindFirstFile、 FindNextFile等函数类似的功能。
在 Windows中,每个文件夹都由操作系统实现了一个派生自 Iunknown接口( COM接口的最基本类)的接口 IshellFolder。通过调用某个文件夹的该接口,即可实现对该文件夹的浏览,得到该文件夹中子项(子文件夹或文件)的各种相关信息。
我们可以调用 SHGetDesktopFolder函数来获得外壳名字空间的根文件夹(即“ 桌面” )的 IshellFolder接口。对于某个文件夹 A,以它的子文件夹 B的相对 PIDL为参数,调用它的 IshellFolder接口的 BindToObject方法即可得到子文件夹 B的 IshellFolder接口。如要枚举某个文件夹下的子项,则只需调用它的 IshellFolder接口的 EnumObjects方法即可获得一个 IEnumIDList接口。通过调用该 IEnumIDList接口的 Next方法我们即可枚举出该文件夹的所有子项(包括文件夹和文件等对象),获得它们的相对 PIDL。使用父文件夹的 IshellFolder接口和这些相对 PIDL,我们即可获得这些子项的各种相关信息,包括显示名称、图标、属性等,甚至还可以获得它的右键菜单。例如,调用该接口的 GetDisplayNameOf方法可获得该文件夹下子项的显示名称;调用 ParseDisplayName方法可把某个子项的用 Unicode内码表示的字符串路径翻译成对应的 PIDL。这样通过 PIDL和这两个接口,我们就可以遍历和操纵整个外壳名字空间了。
除了 IshellFolder和 IEnumIDList接口以外, Windows 外壳名字空间还提供了很多其他 COM接口,例如 IshellBrowser、 IshellLink、 IshellIcom、 IshellView等。通过这些接口,应用于程序就可以更好的与外壳名字空间交互。由于本文篇幅有限,这些接口就不详细介绍了,有兴趣的读者可参阅相关资料。
值得注意的是, COM中的接口虽然在使用上与 C++中的类是非常相似(事实上 COM接口在 C ++中就是以类的形式声明的),但维护其正确的引用计数机制是非常重要的。每增加一个对该接口的引用,就要调用一次它的 AddRef( )方法;而在使用完后必须调用它的 Release( )方法释放该接口。关于 COM及 COM接口的细节请参见相关资料,这里不再赘述。
可惜的是,虽然我们可依照上文给出的方法实现外壳名字空间的逐层展开,但外壳名字空间却并没有提供一种让我们自由跳转到某一文件夹的方法,也没有提供返回到上一级文件夹的方法,因为我们无法方便地获得父文件夹的 IshellFolder接口。如果要返回,就必须由应用程序自己想方法获得父文件夹的 IshellFolder接口。一种可行的方法是在展开外壳名字空间时保存每个文件夹的 IShellFolder接口指针和它的绝对 PIDL,这样就可以相对容易地实现自由跳转了。
但无论如何,外壳名字空间提供的浏览和操作的方法比起 DOS/ Windows 3.x的函数来还是有着巨大的飞跃的。只要我们理解清楚了这种方法的优点与不足,我们就可以扬长避短,开发出各种各样的使用外壳名字空间的程序来。
-
- 相关接口、函数和数据结构
对于本文所涉及的一些比较复杂的接口、函数和数据结构,以下仅列举出作者在 Visual C++6.0查到的声明与定义 ,并配上相应的注释 .一些较简单的则从略 ,未列出的请参见相关资料。
-
- 数据结构
typedf IshellFolder*LPSHELLFOLDER;
//IshellFolder接口指针的声明
typedef struct _ITEMIDLIST{//ITEMIDLIST结构的定义
SHITEMID mkid;
}ITEMIDLIST, * LPITEMIDLIST;
typedef struct _SHITEMID{//ITEMIDLIST结构中元素的定义
USHORT cb;//本结构的长度 (以字节计 )
BYTE abID[1];//可变长的元素标识符
} SHITEMID, *LPSHITEMID;
typedef struct _SHFILEINFO{//SFFILEINFO结构的定义
HICON hicon;//文件图标的句柄
Int ilcon;//图标在系统图像列表中的序号
DWORD dwAttributes;//文件的属性
Char szDisplayName [MAX_PATH];//显示名称或路径
Char szTypeName[80];//表示文件类型的字符串
} SHFILEINFO;
2.相关接口
2.1 IshellFolder接口的方法
-
- BindToObject
格式 :HRESULT BindToObject( LPCITEMIDLLIST pidl, LPBC pbcreserved, REFIID riid, LPVOID *ppvOut);
作用:得到本文件夹中某一子文件夹的 IShellFolder接口。
参数: Riid应为 IID_IshellFolder, pbcReserved应为 NNUL,pidl为表示该子文件夹的“ 相对路径” 的 PIDL,从 ppvOut中返回要求的 IshellFolder接口的指针。
- EnumObjects
- BindToObject
格式: HRSULT EnumObjects( HWND hwndOwner, DWORD grfFlags, LPENUMIDLIST*ppenumIDList);
作用:枚举本文件夹的成员。
参数: hwndOwmer为父窗口句柄, grfFlags决定枚举世闻名的内容,可为 SHCONTF_FOLDERS、 SHCONTF_NONFOLDERS、 SHCONTF_INCLUDEHIDDEN的组合 ,从 ppenumIDList返回 IEnumIDList接口的指针。
( 3) GetDisplayNameOf
格式: HRESULT GetDisplayNameOf (LPCITEMIDLIST pidl, DWORD uFlags, LPSTRRERT lpName);
作用:得到本文件夹中某一对象的显示名称。
参数: pidl为表示该子文件夹的“ 相对路径” 的 PIDL, uFlags为 SHGDN_NORMAL、 SHGDN_INFOLDER、 SHGFI_SYSICONINDEX、 SHGFI_EXETYPE、 SHGFI_ATTRIBUTES、 SHGFI_PIDL、 SHGFI_DISPLAYNAME、 SHGFI_LARGEICON等。
返回值:如 uFlags包含 SHGFI-EXETYPE标志,则返回值为该可执行文件夹类型;如 uFlags包含 SHGFI_SYSICONINDEX标志 ,则返回值为系统图像列表的句柄。否则 ,如本函数调用成功则返回非零值,失败则返回零。
-
- 应用举例
-
- 几个非常有用的函数的实现
1. 1ParsePidlFromPath
描述:将文件系统路径翻译成对应的 PIDL。 LPITEMIDLIST ParsePidlFromPath(LPCSTR path)
{
//存放以 Unicode内码表示的路径字符串的缓冲区
OLECHAR szOleChar[MAX_PATH];
//“ 桌面“ 的 IshellFolder接口指针
LPSHELLFOLDER IpsfDeskTop;
//返回的 PIDL
LPITEMIDLIST Ipifq;
ULONG ulEaten, ulAttribs;
HRESULT hres;
//得到“ 桌面” 的 IshellFolderr 接口指针
SHGetDesktopFolder(&lpsfDeskTop);
//将 Ansi字符集的路径字符串转换成 Unicode字符串,
存入 szOleChar
MultiByteToWideChar(CP_ACP,MB_divCOMPOSED,
Path,-1,szOleChar,sizeof(szOleChar));
//将 szOleChar,中的路径径字符串翻译成相应的 PIDL,存入 lpifq
hres=lpsfDeskTop->Release( );
//如果翻译失败,则返回 NULL
if(FAILED(hres))return NULL;
return lpifq;
1.2 GetItemIcon
描述:返回 lpi这个绝对 PIDL所指项的图标在系统图像列表中的序号, uFlags为要求的图标类型。
Int Getltemlcon(LPITEMIDLIST lpi, UINT uFlags)
{
//存放文件信息的结构
SHFILEINFO sfi;
//给 uFlags增加一些公共标志( lpi为 PIDL、要求返回系统图像列表、要求小图标)
uFlags|=SHGFI-PIDL |SHGFI_SYSICONINDEX |SHGFI_SMALLICON;
获得图标
SHGetFileinfo( (LPCSTR) lpi, 0, & sfi , sizeof(SHFILEINFO),uFlags);
//返回图标在系统图像列表中的序号
return sfi,ilcon;
}
1.3 GetName
描述: lpio lpsf所指的 IshellFolder接口代表的文件夹下的相对 PIDL,本函数获得 lpi所指项的显示名称, dwFlags表明欲得到的显示名称类型, lpFriendlyName为存放显示名称的缓冲区。
BOOL GetName(LPSHELLFOLDER lpsf,LPITEMIDLIST lpi,DWORD dwFlags,LPSTR lpFriendlyName)
{
STRRET str;
//得到显示名称
if(NOERROR!=lpsf->GetDisplayNameOf()lpi,dwFlags,&str))
return FALSE;
//根据返回值进行转换
switch(str uType)
{
//如为 Unicode字符串,则转成 Ansi字符集的字符串 case STRRET_WSTR:
WideCharToMultiByte(CP_ACP,0,str.pOleStr,-1,ipFriendlyName,sizeof(lpFriendlyName),NULL,NULL);
Break;
//如为偏移量,则去除偏移量
case STRRET_OFFSET:
lstrcpy(lpFriendlyName,(LPSTR)lpi+str.uOffset);
break;
如为 Ansi字符串,则直接拷贝
case STRRET_CSTR:
Lstrcpy(lpFriendlyName,(LPSTR)str.cStr);
Break;
//非法情况
default:
return FALSE;
}
return TRUE;
-
- 一个实例
以下我们将用 Visual C++6.0制作一个例子来演示外壳名了空间的浏览。具体为使用 Ctreer View,展开外壳名字空间中的“ 桌面” 文件夹,枚举出该文件夹下的所有子文件夹。
在这个项目中, CtreeView 的图像列表我们使用 Windows 的系统图像列表,而不是自己创建一个。
首先,用 AppWizard新建一个项目,类型为 MFC AppWizard(exe),项目名为 Test;在第一步中选择 Single document;在第六步中将 CtestView的基类改为 CtreeView。其它均使用默认设置。
其次,在 CtestView中加一个私有成员变量 m_ImageList,类型为 CimageList,用于保存系统列表。( Windows中所有的图标都保存在系统图像列表中,我们可以在程序中得到这个图像列表)。
第三步,将上文提到的 GetName 和 GetItemIcon 这两个函数的实现拷贝到 CtestView.cpp的较开头的位置。
第四步,在 CtestView的 OnInitialUpdate( )函数中加入以下代码:
//系统图像列表的句柄
HIMAGELIST himlSmall;
//存放文件信息的结构
SHFILEINFO sfi;
//存放树型控件中的节点的信息
TV_INEM tvi;
//向树型控件中插入节点时使用的结构
TV_INSERTSTRUCT tvis;
//欲插入节点的前一节点的句柄
HTREEITEM hParent=TVI_FIRST;
//欲节点的父节点的句柄
HTREEITEM hParent=TV_ROOT;
//某一文件夹的 IshellFolder接口指针
LPSHELLFOLDER lpsf=0;
//IenumiDList接口的指针
LPENUMIDLIST lpe=0;
//lpi为一 PIDL
LPITEMIDLIST lpi=0;
//IMalloc接口的指针
LPMALLOC lpMalloc=0;
//枚举的个数
ULONG ulFetched;
//存放显示名称的缓冲区
char szBuff[MAX_PATH];
//获得系统图像列表,并把它赋给 CtestView的 CtreeCtrl控件
himlsmall=(HIMAGELIST)SHGetFileinfo(“ C://” ,0,&
sfi, sizeof(SHFIEINFO), SHGFI_SYSICONINDEX|SHGFI_SMALLICON);
m_lmageList.Attach(himlsmall);
GetTreeCtrl().SetlmageList(&m_imageList,TVSIL_NORMAL);
//获得 Imalloc接口的指针
SHGetMalloc(&lpMalloc);
//获得“ 桌面” 文件夹的 IshellFolder接口指针
SHGetDesktopFloder(&lpsf);
//创建一个“ 桌面” 的绝对 PIDL
lpi=(LPITEMIDLIST)lpMalloc->Alloc(sizeof(USHORT0));
*((USHORT*)lpi)=0
// 设置要插入的树节点信息
tvi,mask=TVIF_TEXT|TVIF_IMAGE|TVIF_SELECTEDIMAGE|
TVIF_CHILDREN;
tvi.cchTextMax=MAX_PATH;
//设置显示名称
tvi.pszText=_T(“ 桌面” );
//获得标准图标和展开时的图标
tvi.ilmage=Tetltemlcon(lpi,NULL);
tvi.iSelectedlmage=Getltemlcon(lpi,SHGFI_OPENICON);
//设置插入位置
tvis.item=tvi;
tvis.hlnsertAfter=TVI_FIRST;
tvis.hParent=TVI_ROOT;
//插入根节点
hpParent=GetTreeCtrl().Instrtltem(& tvis);
//释放 lpi所占的空间
lpMalloc->Free(lpi);
//获得“ 桌面” 文件夹的 IenumiDList接口指针 lpe
lpsf->EnumObjects(m_hWnd, SHCONTF_FOLDERS |
SHCONTF_NONFOLDERS,& lpe);
//枚举“ 桌面” 下的各个子文件夹
while(S_OK= =lpe-> Next(1,&lpi,&ulFetched))
{
//获得 lpi表示的子文件夹的显示名称
GetName(lpsf,lpi,SHGDN_NORMAL,szBuff);
tvi.pszText=szBuff;
// 获得该项的图标
//由于是“ 桌面” 下的直接子项,所以它的相对 PIDL与绝对 PIDL是一致的
tvi.ilmage=Getltemlcon(lpi,NULL);
tvi.iSelectedimage=Getltemlcon(lpi,SHGFI_OPENICON);
//设置插入位置
tvis.item=tvi;
tvis.hinsertAfter=hPrev;
tvis.hParent=hParent;
//插入节点
hPrev=GetTreeCtrl(). insertltem(& tvis);
//释放 lpi所占的空间
ipMalloc->Free(lpi);
}
//释放 Imalloc和 IsshellFolder接口
lpMalloc->Release();
lpsf->Release();
//对生成的节点进行排序
GetTreeCtrl( ).SortChildren(hParent);
//将 CtestView中的“ 桌面” 节点展开
GetTreeCtrl( ).Selectltem(hParent);
GetTreeCtrl( ).Expand(hParent,TVE_EXPAND);
最后,响应 CtestView的 WM_DESTROY消息,加入以下代码:
//由于使用了系统图像列表,退出时必须释放对它的所有权
//否则,退出后 Windows将一个图标没有
m_imageList.Detach( );
这个演示程序的效果如下图所示:
-
- 后记
由于篇幅的关系,本文所举的例子只能非常简单的演示一下外壳名字空间的浏览,很多较复杂的编程 方法都没有表现出来。 最后,希望本文能够起到抛砖引玉的作用,让更多的开发者认识与使用外壳名字空间,开发出更好的程序来。
参考文献
-
- MicrosoftCorporation. Microsoft Windows95程序员指南,清华大学出版社, 1996
- StefamoMaruzzi.Windows95开发者必读,电子工业出版社, 1997