Windows外壳名字空间的浏览

 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可以显示一个让用户选择外壳名字空间中某一文件夹的浏览对话框 .

        • 基于
        COM 的方法

  讨论清楚了 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

格式: 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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值