转:Windows Shell 编程 第五章_2

转自:http://yadang418.blog.163.com/blog/static/2684365620096534024792/

 

探测器允许使用‘打印机’作为通常意义的文件夹名。换言之,它模糊了物理文件夹和虚拟文件夹的差异,精确地讲,‘打印机’是一个内容为有效打印机设备的虚拟文件夹的显示名。在这个例子中我们建立一个示范程序称之为Pidl,其用户界面如下:

      

‘搜索路径’按钮接受编辑框的内容并努力查找具有这个名字的文件夹。编辑框中的串就是一个文件夹的显示名(记住,路径名也是显示名)。如果成功,应用将在列表观察中显示所有在这个文件夹中找到的文件对象。另一方面,‘显示PIDL内容’按钮将在列表观察中枚举特殊文件夹中找到的所有文件对象,特殊文件夹在下拉列表中选择。

通过显示名搜索

        让我们从点击‘搜索路径’按钮所执行的代码开始,当然这两个新按钮都需要APP_DlgProc()过程来处理,所以在这里添加代码如下:

case WM_COMMAND:

switch(wParam)

{

case IDC_SEARCHPATH:

DoSearchPath(hDlg);

return FALSE;

case IDC_PIDLCONTENT:

DoEnumeratePidl(hDlg);

return FALSE;

case IDCANCEL:

EndDialog(hDlg, FALSE);

return FALSE;

}

break;

这里所涉及到的第一个函数是DoSearchPath(),它从‘文件夹名’编辑框中读出你键入的名字,把它作为搜索路径名,如果它确实是一个路径名,所有操作都正常进行,但是如果它是一个文件夹的显示名,将会有什么事情发生呢?我们希望函数能够处理如C:\(C:)等的串,这个实现将能够正确处理所有路径名和与桌面关联的子文件夹的显示名,或‘我的计算机’等虚拟文件夹。

注意,正常地,驱动器的显示名由括弧中驱动器字符加标号给出,例如:Ms-Dos_6(C:)。如果没有标号,则认为有一个前导空格在(C:)

DoSearchPath()一开始就枚举桌面文件夹的内容:

void DoSearchPath(HWND hDlg)

{

LPITEMIDLIST pidl = NULL;

LPSHELLFOLDER pFolder = NULL;

LPSHELLFOLDER pSubFolder = NULL;

// 取得内存分配器

LPMALLOC pMalloc = NULL;                                    

SHGetMalloc(&pMalloc);

// 取得搜索名

TCHAR szName[MAX_PATH] = {0};

GetDlgItemText(hDlg, IDC_FOLDER, szName, MAX_PATH);

// 取得卓面的IShellFolder接口

SHGetDesktopFolder(&pFolder);

// 试图在桌面上找到一个匹配

int iNumOfItems = SHEnumFolderContent(pFolder, NULL, 0, NULL);

int rc = SHEnumFolderContent(

pFolder, SearchText, reinterpret_cast<DWORD>(szName), &pidl);

SHEnumFolderContent()是用户定义的函数,他接受一个文件夹的PIDL和一个回调函数作为输入,然后枚举这个文件夹的所有项,并传递这些项到这个函数作进一步的处理。后面我们将进一步讨论它。为了理解它在这里的用途,你只需要知道,如果没有指定回调函数,它返回所找到的项目数:

int iNumOfItems = SHEnumFolderContent(pFolder, NULL, 0, NULL);

否则,它返回实际处理的项目数。这两个值必然是不同的,因为回调函数可以在本身选中的点上停止枚举。例如SearchText()函数,当它找到了它正在寻找的名字时,SHEnumFolderContent()停止了。

   SHEnumFolderContent()函数开始搜索,检查我们在编辑框中键入的名字是否对应桌面下一个文件夹的显示名。这就是上面代码终止后,rciNumOfItems不等的情况。如果它们相等,我们就在‘我的计算机’节点上开始一个新的搜索:

//如果没找到,在‘我的计算机’上再试

if(rc == iNumOfItems)

{

// 绑定到‘我的计算机’

LPITEMIDLIST pidlMyComp;

SHGetSpecialFolderLocation(NULL, CSIDL_DRIVES, &pidlMyComp);

pFolder->BindToObject(pidlMyComp, NULL, IID_IShellFolder,

reinterpret_cast<LPVOID*>(&pSubFolder));

//释放桌面文件夹指针

pFolder->Release();

pMalloc->Free(pidlMyComp);

pFolder = pSubFolder;

//扫描‘我的计算机’

iNumOfItems = SHEnumFolderContent(pFolder, NULL, 0, NULL);

rc = SHEnumFolderContent(

pFolder, SearchText, reinterpret_cast<DWORD>(szName), &pidl);

在重复调用SHEnumFolderContent()函数工作于‘我的计算机’文件夹上之前,我们需要为之获得IShellFolder接口指针,在这一点上我们有的只是桌面的IShellFolder接口,然而,我们可以通过接口的BindToObject()方法获得想要的接口。这个操作使你能绑定到子文件夹的IShellFolder接口,因此,你可以同样使用这个方法作用于PIDL

HRESULT IShellFolder::BindToObject(

LPCITEMIDLIST pidl, //我们想要的文件夹的PIDL

LPBC pbcReserved, //保留,必须为空NULL

REFIID riid, // 必须是IID_IShellFolder

LPVOID* ppvOut // 接收IShellFolder的指针

);

如果既没在桌面上也没在‘我的计算机’上找到指定的显示名,则我们所取得的是一个快捷方式,而且我们确实不能确定它的位置。尽管如此,也不要认为这是一个系统限制—在文件夹上使用递归搜索来确定名字的位置是很有可能的。这个方法略述如下:

                  枚举桌面文件夹的内容,就象上面所做的。

                  对每一个找到的文件夹(不仅是‘我的计算机’)重复这个搜索过程

然而,完全递归的搜索可能导致试图通过名字查找一个不唯一的文件夹—这是很有可能的。当然可能有两个文件夹具有相同的显示名MyDir,一个在c:\,另一个在d:\,上面的算法将总是停止在头一个文件夹出现的地方。

        一个较好的方法是接受和分析全质量文件夹名,比如:

My Computer\ (c:)\Windows

Control Panel\Add New Hardware

这样做仅仅需要很少的额外代码来分析文件夹名,并且从搜索桌面上的第一个项开始,前一步完成到达文件夹的下一个项,等等。上面所看到的代码可以稍微加推广,和封装在一个循环中。

        回想一下,这确实与在文件系统中搜索没有什么不同,正好就是使用FindFirstFile()FindNextFile()来枚举目录的内容,只是使用了由文件夹对象的COM接口暴露的方法而已。

        在完成代码之前,注意,键入的显示名是一个完整的路径名的情况,如c:\。在输出一个消息框之前处理这种情况是有价值的—正象我们需要转换这个名字为PIDL格式一样,看一下什么情况发生了,如果没有错误,则一个路径名被接受了。

if(rc == iNumOfItems)

{

// 做最后的努力,它是一个路径名

HRESULT hr = SHPathToPidlEx(szName, &pidl, pFolder);

if(FAILED(hr))

{

Msg("\"%s\" not found under Desktop or My Computer.", szName);

pMalloc->Free(pidl);

pFolder->Release();

// 调用辅助函数刷新UI

ClearUI(hDlg);

return;

}

}

}

最后,如果函数在这一点之前没有返回,我们知道,已经有了一个可用于输出文件夹图标的PIDL,也就是说,在编辑框中有一个输入串作为上面源码中的szName被引用。我们就用那个名字标识文件夹对象并获得它的PIDL。现在,要枚举这个文件夹的内容,我们需要获得它的IShellFolder接口和把它传递给SHEnumFolderContent()函数。

   因而,‘搜索路径’按钮处理程序的结尾代码有如下形式:

// 如果到达这里,则:

// pidl 指向我们需要绑定的文件夹以枚举它的内容

// pFolder 指向pidl父文件夹的IShellFolder

// 绑定到我们正在搜索的子文件夹

// pFolder 可以指向桌面的或‘我的计算机’的IShellFolder

pFolder->BindToObject(pidl, NULL, IID_IShellFolder,

reinterpret_cast<LPVOID*>(&pSubFolder));

// 刷新UI (清空列表观察和图像列表等)

ClearUI(hDlg);

// 枚举文件夹内容到列表观察

HWND hwndListView = GetDlgItem(hDlg, IDC_LISTVIEW);

SHEnumFolderContent(pSubFolder, ShowFolderContent,

reinterpret_cast<DWORD>(hwndListView), NULL);

// 清理

pFolder->Release();

pSubFolder->Release();

pMalloc->Free(pidl);

pMalloc->Release();

return;

}

转换路径名到PIDLs()

        查看上面的代码,你可能已经注意到,我使用了SHPathToPidlEx()函数来转换路径名到PIDL。在这一章的开始,我们开发了有同样目的的SHPathToPidl()辅助函数—它使用IShellFolder接口的ParseDisplayName()方法。SHPathToPidl()函数的代码浓缩到这一点上,它取得相对于桌面的PIDL—即,层次的根是

SHGetDesktopFolder(&pFolder);

pFolder->ParseDisplayName(NULL, NULL, wszPath, &n, ppidl, NULL);

不幸的是这个PIDL是相对于提供IShellFolder接口文件夹的,是桌面。新情况是我们需要相对于操作文件夹之父文件夹的PIDL。理由是,当我们使用BindToObject()方法来获得一个子文件夹的IShellFolder时,要求传递一个PIDL,它是与我们调用BindToObject()位于相同位置文件夹的PIDL

   此后,我们还需要在获得IShellFolder接口指针和ParseDisplayName()函数之间添加几步,这几步是要保证用于ParseDisplayName()调用的IShellFolder接口确实是我们想要的文件夹的接口。

代码如下:

HRESULT SHPathToPidlEx(

LPCTSTR szPath, LPITEMIDLIST* ppidl, LPSHELLFOLDER pFolder)

{

OLECHAR wszPath[MAX_PATH] = {0};

ULONG nCharsParsed = 0;

LPSHELLFOLDER pShellFolder = NULL;

BOOL bFreeOnExit = FALSE;

MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, szPath, -1, wszPath, MAX_PATH);

// 默认使用桌面的IShellFolder

if(pFolder == NULL)

{

SHGetDesktopFolder(&pShellFolder);

bFreeOnExit = TRUE;

}

else

pShellFolder = pFolder;

HRESULT hr = pShellFolder->ParseDisplayName(

NULL, NULL, wszPath, &nCharsParsed, ppidl, NULL);

if(bFreeOnExit)

pShellFolder->Release();

return hr;

}

这个函数比SHPathToPidl()更一般,而且它也要求传递PIDL相对的文件夹。如果传递一个NULL,而不是一个IShellFolder指针,则桌面的IShellFolder接口被使用,然后被释放。在这个例子中调用转换函数的代码是:

HRESULT hr = SHPathToPidlEx(szName, &pidl, pFolder);

试着传递一个NULL而不是确定的路径名给pFolder,进行搜索,将会看到:无论如何,你将总是枚举‘桌面’文件夹的内容。

清除用户界面

        作为SHEnumFolderContent()细节的一部分,在这个代码中我们所看到的最简单的辅助函数是ClearUI()

void ClearUI(HWND hDlg)

{

HWND hwndListView = GetDlgItem(hDlg, IDC_LISTVIEW);

ListView_DeleteAllItems(hwndListView);

ImageList_RemoveAll(g_himl);

SetDlgItemText(hDlg, IDC_FOUND, __TEXT("0 item(s) found."));

}

这正是重置应用的对话框,从观察列表中删除所有项,以及清空由SHEnumFolderContent()函数建立的图像列表,最后一项任务由HIMAGELIST类型的全称变量g_himl完成,它在WinMain()中被初始化为0

构建枚举器函数

        关于枚举给定文件夹内容这方面仍然有大量的问题需要考虑,比如,它是一个物理目录还是一个如‘打印机’的虚拟文件夹等。我们所看到的源码给出一个重要的函数SHEnumFolderContent(),这是一个负责查询文件夹和逐个枚举其内容的函数。

   有些文件夹其内容是文件的集合,还有些文件夹其可见内容可以是单个文件的记录或某种硬件设备。一般而言,仅仅是文件夹才确切地知道它的内容是什么。对探测器或程序而言,没有任何保险的方法不需要查询文件夹就能枚举它所包含的项。这并不奇怪,因为这种通讯是基于COM接口的。

        在程序中,SHEnumFolderContent()询问一个文件夹的内容,并传输它找到的每一个项的名字到另外的函数做进一步的处理。你已经看到了这两个函数:SearchText()ShowFolderContent()。然而要理解它们的适当作用,首先要研究项目枚举是怎样发生的。

读文件夹的内容

        关联于‘搜索路径’(和显示PIDLs内容)按钮的代码,其目的在于读出文件夹的内容。为了允许枚举其中的项,文件夹实现了IEnumIDList接口,这个接口暴露了四个函数:Next(),Skip(),Reset()Clone(),使之可以在给定的集合中前后移动。它们的原型为:

HRESULT IEnumIDList::Next(ULONG celt,

LPITEMIDLIST* rgelt,

ULONG* pceltFetched);

头一个变量是请求的项目数,第二个是一个PIDLs数组指针,第三个则是返回的实际拷贝的项目数。IEnumIDList接口本身负责分配保存PIDL数据的内存。

        要想了解指定文件夹的内容,需要设计一段代码,要记住是通过获取指向IEnumIDList接口的指针和IShellFolder接口所暴露的方法EnumObjects()实际完成这个操作的。这个方法的原型如下:

HRESULT IShellFolder::EnumObjects(HWND hwndOwner, //一个窗口Handle

DWORD grfFlags, //一个标志集

LPENUMIDLIST* ppenumIDList //接收IEnumIDList的指针

);

这个方法的第二个参数允许你规定枚举项目的类型。它取如下定义的枚举类型组合值:

typedef enum tagSHCONTF

{

SHCONTF_FOLDERS = 32,

SHCONTF_NONFOLDERS = 64,

SHCONTF_INCLUDEHIDDEN = 128,

} SHCONTF;

就如助记名所述的一样:你可以决定枚举文件夹、非文件夹对象,甚至是隐藏对象。

        以后在写一个命名空间扩展得时候,详细讨论这些接口是非常必要的。现在,我们建议你花点时间看一下VC++的帮助文件,以澄清这些方法名和原型。

LPENUMIDLIST pEnumIDList = NULL;

LPITEMIDLIST pItem = NULL;

ULONG ulFetched = 0;

pFolder->EnumObjects(

NULL, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, &pEnumIDList);

while(pEnumIDList->Next(1, &pItem, &ulFetched) == NOERROR)

{

...

}

上面这段代码描述了触发枚举文件夹项函数的过程。每次循环条件成立,pItem指向一个PIDL的单项。获得这个数据后有两件事情需要处理:它的显示名和可能的图标。

取得项的显示名

        即使有了PIDL,取得项目的显示名也不是一件容易的事。尽管有IShellFolder::GetDisplayNameOf()函数,仍有一些工作要做,问题在于这个方法不提供正常的ANSIUnicode格式。相反,它返回的是一个STRRET结构的指针。STRRET结构定义如下:

typedef struct _STRRET

{

UINT uType;

union{

LPWSTR pOleStr;

LPSTR pStr; // Unused

UINT uOffset;

char cStr[MAX_PATH];

} DUMMYUNIONNAME;

} STRRET, *LPSTRRET;

正像你所看到的,这个结构用一个标志表示格式,这个标志说明了其后字符串的类型。串可以是Unicode(pOleStr)ANSI(cStr),或甚至是一个串的地址偏移(uOffset)。也就是说不管初始串是什么格式的,你都需要自己写展开例程处理返回串。这里给出一个样例:

void StrretToString(LPITEMIDLIST pidl, LPSTRRET pStr, LPSTR pszBuf)

{

lstrcpy(pszBuf, "");

switch(pStr->uType)

{

case STRRET_WSTR: // Unicode

WideCharToMultiByte(

CP_ACP, 0, pStr->pOleStr, -1, pszBuf, MAX_PATH, NULL, NULL);

break;

case STRRET_OFFSET: //地址偏移

lstrcpy(pszBuf, reinterpret_cast<LPSTR>(pidl) + pStr->uOffset);

break;

case STRRET_CSTR: // ANSI

lstrcpy(pszBuf, pStr->cStr);

break;

}

}

StrretToString()函数接收一个PIDL和一个STRRET结构,经由第三个变量返回一个LPSTR。附带地上面代码还显示了uType的合法值。回到我们主要讨论的话题,GetDisplayNameOf()函数的原型是:

HRESULT IShellFolder::GetDisplayNameOf(LPCITEMIDLIST pidl,

DWORD uFlags,

LPSTRRET lpName);

这里uFlags标志来自SHGNO枚举类型:

typedef enum tagSHGDN

{

SHGDN_NORMAL = 0,

SHGDN_INFOLDER = 1,

SHGDN_INCLUDE_NONFILESYS = 0x2000,

SHGDN_FORADDRESSBAR = 0x4000,

SHGDN_FORPARSING = 0x8000,

} SHGNO;

资料中对这些标志的描述是足够清晰的。因此,你可以构建你所期望的具有最终行为的函数。然而,无论我怎样设置标志,所有样例程序都是以同样的方式工作。坦白地说,我也不知道哪儿除了问题。所以我建议总是使用0作为这个参数的值。

STRRET sName;

CHAR szBuf[MAX_PATH] = {0};

pFolder->GetDisplayNameOf(pItem, 0, &sName);

StrretToString(pItem, &sName, szBuf);

这个代码段以可阅读格式显示项目名。再次提醒注意,对于文件型文件夹以及Fonts,Favorites,Printers,Control Panel等特殊文件夹这是正确的。也就是说,我们能够列出所有‘控制面板’中的小程序。

        本身,我们没有用到STRRET结构,这要感谢StrretToString()函数提供的帮助,它当然也包含在Shell库函数中。

读取项目的图标

   已开始接触Shell编程,以为非常艰巨,然而,当你从初始的头三四个月中挺过来之后,你就开始有机会接触高级项目问题的答案了。要说明这一点,你认为怎样才能取得一个项的图标?回答是,你必须询问提供它的文件夹。IShellFolder::GetUIObjectOf()方法返回所有你可能需要处理用户界面和文件对象的接口。

HRESULT IShellFolder::GetUIObjectOf(

HWND hwndOwner, // 窗口Handle

UINT cidl, // 下一个参数中的元素数

LPCITEMIDLIST* apidl, //指向一个PIDLs数组的指针

REFIID riid, // 要求的接口ID

UINT* prgfInOut, //保留(必须为NULL)

LPVOID* ppvOut // 接收接口的指针

);

对这个声明我们感兴趣的是可以请求一定数量的不同接口指针,它们都能影响UI显示。例如,请求IContextMenu,以获得元素的关联菜单HMENUHandle。在我们的任务中,请求的是IExtractIcon接口,以便查找图标(在第十六章中将看到更多关于GetUIObjectOf()的解释)

pFolder->GetUIObjectOf(NULL, 1, const_cast<LPCITEMIDLIST*>(&pItem),

IID_IExtractIcon, NULL, reinterpret_cast<LPVOID*>(&pExtractIcon));

IExtractIcon接口有两个新方法:GetIconLocation()Extract()。头一个使你能知道图标的索引和位置,第二个则返回一个HICON型的Handle。在客户端调用GetIconLocation()函数时,它返回包含图标的文件名,以及在这个文件的资源中的一个从零开始的图标索引。

HRESULT IExtractIcon::GetIconLocation(UINT uFlags,

LPSTR szIconFile,

INT cchMax,

LPINT piIndex,

UINT* pwFlags);

Extract()函数依次从指定文件中抽取给定的图标,和返回它的HICON。这个方法几乎与API函数ExtractIconEx()是一样的。

HRESULT IExtractIcon::Extract(LPCSTR pszFile,

UINT nIconIndex,

HICON* phiconLarge,

HICON* phiconSmall,

UINT nIconSize);

对这些函数而言资料显得有点冗长,例如,即使你不关心pwFlags的内容,你也需要知道它不能为NULL。类似地,即使你仅仅需要大图标,你仍然必须为小图标传递一个有效的非零HICON。下面测试一个例子,说明怎样调用它:

pExtractIcon->GetIconLocation(0, szIconFile, MAX_PATH, &iIconIndex, &u);

pExtractIcon->Extract(szIconFile, iIconIndex, &hIcon, &hIconSm, MAKELONG(32, 16));

pExtractIcon->Release();

在开发这个实例代码时,我获得了另一个有趣的结果,坦白地讲,这超出了我的想象。在有些情况下Extract()函数返回的HandleNULL,即使在图标的位置和索引都是正确的情况下,也是如此。奇怪,使用相同的参数调用ExtractIconEx()函数工作完好。当然,工作环境是直接的:

if(hIcon == NULL)

ExtractIconEx(szIconFile, iIconIndex, &hIcon, NULL, 1);

在这一点上,最后要做的工作就是需要建立一个新的Shell函数,它接收IShellFolder指针,在它的项上循环,为每个项唤醒回调函数。就像很多其他称为‘枚举’的函数一样,我们的SHEnumFolderContent()函数将提供一个用户定义的缓冲(dwData)发送程序级的变量到回调函数。进一步,如果回调函数返回FALSE,则函数将停止工作。这里是它的原型:

int SHEnumFolderContent(LPSHELLFOLDER pFolder,

FOLDERCONTENTPROC pfn, DWORD dwData, LPITEMIDLIST* ppidl);

其中FOLDERCONTENTPROC是一个用户定义的函数指针,其声明是:

typedef BOOL (CALLBACK *FOLDERCONTENTPROC)(LPCSTR, HICON, DWORD);

第一个变量是元素显示名,而后是一个图标Handle。然后是用户定义的缓冲。像已经提到的那样,函数返回FALSE来中断枚举,TRUE则继续。

        SHEnumFolderContent()的最后一个参数是PIDL。这并不严格地必要,只是有时需要(如在我们的例子中),理解这个最后传递的参数PIDL是有帮助的。如果这个变量是NULL,则它被忽略。下面是SHEnumFolderContent()函数的源代码:

int SHEnumFolderContent(LPSHELLFOLDER pFolder,

FOLDERCONTENTPROC pfn, DWORD dwData, LPITEMIDLIST* ppidl)

{

int iNumOfItems = 0;

//枚举内容

LPENUMIDLIST pEnumIDList = NULL;

pFolder->EnumObjects(

NULL, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, &pEnumIDList);

ULONG ulFetched = 0;

LPITEMIDLIST pItem = NULL;

while(NOERROR == pEnumIDList->Next(1, &pItem, &ulFetched))

{

STRRET sName;

TCHAR szBuf[MAX_PATH] = {0};

pFolder->GetDisplayNameOf(pItem, 0, &sName);

StrretToString(pItem, &sName, szBuf);

// 唤醒回调函数

if(pfn)

{

// 获取图标

UINT u = 0;

int iIconIndex = 0;

HICON hIcon = NULL;

HICON hIconSm = NULL;

TCHAR szIconFile[MAX_PATH] = {0};

LPEXTRACTICON pExtractIcon = NULL;

pFolder->GetUIObjectOf(NULL, 1, const_cast<LPCITEMIDLIST*>(&pItem),

IID_IExtractIcon, NULL,

reinterpret_cast<LPVOID*>(&pExtractIcon));

pExtractIcon->GetIconLocation(0, szIconFile, MAX_PATH,

&iIconIndex, &u);

pExtractIcon->Extract(szIconFile, iIconIndex, &hIcon,

&hIconSm, MAKELONG(32, 16));

pExtractIcon->Release();

if(hIcon == NULL)

ExtractIconEx(szIconFile, iIconIndex, &hIcon, NULL, 1);

if(!pfn(szBuf, hIcon, dwData))

{

// 返回当前的PIDL

if(ppidl != NULL)

*ppidl = pItem;

break;

}

}

++iNumOfItems;

}

return iNumOfItems;

}

回调函数

   典型地,回调函数用于实现某些项的采集任务。如此,SHEnumFolderContent()调用这样的函数采集各个文件夹项。SearchText()函数仅仅报告是否传递过来的两个串是相等的。

BOOL CALLBACK SearchText(LPCSTR pszItem, HICON hIcon, DWORD dwData)

{

return static_cast<BOOL>(lstrcmpi(pszItem, reinterpret_cast<LPCSTR>(dwData)));

}

ShowFolderContent()函数用于构建一个传递过来图标的图像列表,插入图标到提供的列表观察中:

BOOL CALLBACK ShowFolderContent(LPCSTR pszItem, HICON hIcon, DWORD dwData)

{

//建立图像列表

int iIconWidth = GetSystemMetrics(SM_CXICON);

int iIconHeight = GetSystemMetrics(SM_CYICON);

if(g_himl == NULL)

g_himl = ImageList_Create(iIconWidth, iIconHeight, ILC_MASK, 1, 0);

int iIconPos = ImageList_AddIcon(g_himl, hIcon);

HWND hwndListView = reinterpret_cast<HWND>(dwData);

ListView_SetImageList(hwndListView, g_himl, LVSIL_NORMAL);

LV_ITEM lvi;

ZeroMemory(&lvi, sizeof(LV_ITEM));

lvi.mask = LVIF_TEXT | LVIF_IMAGE;

lvi.pszText = const_cast<LPSTR>(pszItem);

lvi.cchTextMax = lstrlen(pszItem);

lvi.iImage = iIconPos;

ListView_InsertItem(hwndListView, &lvi);

//更新计数

TCHAR s[MAX_PATH] = {0};

wsprintf(s, "%d item(s) found.", ListView_GetItemCount(hwndListView));

SetDlgItemText(GetParent(hwndListView), IDC_FOUND, s);

return TRUE;

}

示例程序

一定要保证,主程序代码中包含#includeshlobj.hresource.h,下面图中显示使用这一阶段开发的实例程序可以做的操作。键入‘打印机’,你能充填这个列表观察,就像标准的文件夹窗口一样:

                  

再有,指定一个路径名,可以看到文件和文件夹,正象在探测器中所看到的那样:

                     

        回想一下,如果想要得到任何驱动器根目录的内容时,你必须包括一个最后的反斜杠‘\’,例如C:\工作正常,而C:则产生这个结果:

                          

通过PIDL搜索

        适当地使用我们所有的辅助函数,写一个‘显示PIDLs内容’按钮的处理器不是太困难的事。与这个按钮相关的combo框由名字和一些特殊文件夹的ID初始化,这个过程执行与SHBrowse例子中相同的代码。此处由函数在点击‘显示PIDLs内容’按钮时执行,并充填列表观察:

void DoEnumeratePidl(HWND hDlg)

{

LPITEMIDLIST pidl = NULL;

// 取得特殊文件夹和它的PIDL

HWND hwndCbo = GetDlgItem(hDlg, IDC_SPECIAL);

int i = ComboBox_GetCurSel(hwndCbo);

int nFolder = ComboBox_GetItemData(hwndCbo, i);

SHGetSpecialFolderLocation(NULL, nFolder, &pidl);

//取得IShellFolder接口

LPSHELLFOLDER pFolder = NULL;

SHGetDesktopFolder(&pFolder);

// 绑定到子文件夹

LPSHELLFOLDER pSubFolder = NULL;

pFolder->BindToObject(pidl, NULL, IID_IShellFolder,

reinterpret_cast<LPVOID*>(&pSubFolder));

pFolder->Release();

pFolder = pSubFolder;

//清除程序的UI

ClearUI(hDlg);

//枚举内容

HWND hwndListView = GetDlgItem(hDlg, IDC_LISTVIEW);

SHEnumFolderContent(pFolder, ShowFolderContent,

reinterpret_cast<DWORD>(hwndListView), NULL);

// 清理

LPMALLOC pMalloc = NULL;

SHGetMalloc(&pMalloc);

pMalloc->Free(pidl);

pMalloc->Release();

pFolder->Release();

}

这个函数从获得特殊文件夹的ID开始,这个ID来自combo框的选择,而后调用SHGetSpecialFolderLocation()函数获得文件夹的PIDL,再从这个PIDL获得IShellFolder接口,然后传递给SHEnumFolderContent()函数。图中显示了这个应用怎样枚举‘控制面板’的小程序:

                    

特殊文件夹

        我们首先在第二章中看到了特殊文件夹和它们的基本概念,其中有三种基本类型。几乎所有的特殊文件夹都有对应的目录,但是这些与普通的文件型文件夹和客户文件夹是完全不同的。第三种类型是由没有目录的文件夹(虚拟文件夹)组成。

虚拟文件夹感觉上象一个文件夹,但是它们的位置和内容没有文件和目录概念的映射。‘控制面板’,‘打印机’,‘网上邻居’,‘我的计算机’都是虚拟文件夹的例子。例如‘控制面板’可以包含所有安装了的小程序。

        除了外观,没有称为‘控制面板’的物理目录可以包含与之相关的任何东西,比如‘添加新硬件’或‘Modems’。这个文件夹所列出的所有图标都来自系统目录下的.cpl文件。他们由命名空间扩展聚集和表示为虚拟文件夹。

系统对特殊文件夹的支持

        Windows API定义了一定数量的特殊文件夹,并且有一堆函数作用其上。这些例程通过如ID一样的数字标识每一个特殊文件夹,但是对PIDLsCLSIDs则没有什么可做的。

        ID定义在shlobj.h中,并且都有相当奇怪的符号名:都以CSIDL_开始。下表列出了一些可用的特殊文件夹:

 

文件夹ID

虚拟的

描述

CSIDL_DESKTOP

Yes

桌面

CSIDL_DRIVES

Yes

我的计算机

CSIDL_BITBUCKET

Yes

回收站

CSIDL_CONTROLS

Yes

控制面板

CSIDL_NETWORK

Yes

网上邻居

CSIDL_INTERNET

Yes

Shell上的IE探测器节点(版本4.71以上)

CSIDL_PRINTERS

Yes

打印机

CSIDL_DESKTOPDIRECTORY

 

含有全部桌面快捷方式的目录

CSIDL_FAVORITES

 

Favorite文件夹的快捷方式

CSIDL_FONTS

 

安装的字体

CSIDL_NETHOOD

 

网络域的引用

CSIDL_PRINTHOOD

 

打印机的引用

CSIDL_PERSONAL

 

私有文件的快捷方式

CSIDL_PROGRAMS

 

‘程序’菜单的快捷方式

CSIDL_RECENT

 

最近使用文档的快捷方式

CSIDL_SENDTO

 

‘发送到’菜单项的快捷方式

CSIDL_STARTMENU

 

‘开始’菜单中用户定义的项

CSIDL_STARTUP

 

启动时运行的程序的快捷方式

CSIDL_COOKIES

 

Cookies

CSIDL_TEMPLATES

 

文档模版的快捷方式

CSIDL_HISTORY

 

访问过的Web页面的快捷方式

CSIDL_INTERNET_CACHE

 

IE的临时Internet文件

CSIDL_APPDATA

 

一个应用专有数据的文件夹

CSIDL_ALTSTARTUP

 

非本地‘启动’组

资料中还提到了另外一些标号为CSIDL_COMMON_XXX的文件夹,它们是:

CSIDL_COMMON_STARTUP           CSIDL_COMMON_STARTMENU

CSIDL_COMMON_PROGRAMS          CSIDL_COMMON_FAVORITES

CSIDL_COMMON_DESKTOPDIRECTORY  CSIDL_COMMON_ALTSTARTUP

除了它们指向一个任何用户都可看到的物理文件夹以外,这些文件夹与不包含COMMON的文件夹相同。虽然这一点在资料中没有显式提到,这些文件夹似乎感觉仅在WindowsNT下出现。

获取文件夹路径

        非虚拟文件夹在机器的某个地方有一个路径,你可以通过调用SHGetSpecialFolderPath() API函数获得一个特殊文件夹的路径。特殊文件夹与它的路径之间的连接存储在注册表中,注册键入下:

HKEY_CURRENT_USER

\Software

\Microsoft

\Windows

\CurrentVersion

\Explorer

\Shell Folders

HKEY_LOCAL_MACHINE下相同的键存储了所有可用的COMMON文件夹,但是,在Windows95Windoes98下并不是所有COMMON文件夹都有路径。事实上,仅仅CSIDL_COMMON_DESKTOPDIRECTORYCSIDL_COMMON_STARTUP有路径存储。假设C:\Windows是系统目录,所列出的路径在C:\Windows\All Users folder下。然而,在Windows95/Windows98SHGetSpecialFolderPath()函数不能为其返回任何值。反之,在WindowsNT下使用同样的函数,则返回正确的路径。

函数

        我们可以通过观察曾经使用过的函数SHGetSpecialFolderLocation()来走进函数SHGetSpecialFolderPath()函数。这个函数恢复指定的特殊文件夹的PIDL,它有下面的原型:

HRESULT SHGetSpecialFolderLocation(HWND hwndOwner,

int nFolder,

LPITEMIDLIST* ppidl);

hwndOwner是任何弹出显示窗口的父窗口,nFolder是特殊文件夹的标识符,可以是上表列出的常量之一,而ppidl则是指向包含这个文件夹的PIDL缓冲的指针。SHGetSpecialFolderPath()函数,试图恢复给定文件夹的路径,与上函数非常相似:

HRESULT SHGetSpecialFolderPath(HWND hwndOwner,

LPTSTR lpszPath,

int nFolder,

BOOL fCreate);

lpszPath将包含路径名,而fCreate是一个逻辑值,表示如果文件夹不存在,是否需要建立。当然,在这种情况下,你不能指定一个虚拟文件夹的ID。注意,与SHGetSpecialFolderLocation()函数不同,SHGetSpecialFolderPath()仅仅支持Shell 4.71以上版本。

文件夹设置

        IE4.0和活动桌面极大地增加了系统文件夹的设置量。‘文件夹选项’对话框有完善的复选框来确定文件夹的外观,使其具有我们希望的形式:

                       

这个对话框是Windows系统上人人都使用的对话框,它使你能设置查看系统文件和隐藏文件。有些设置(不是全部)可以通过编程读出,自然这些也仅能在Shell 4.71以上版本上有效。

        VC++的资料中可以找到每一个设置的详细说明,在这里所能找到的仅仅是一个例子程序。

SHGetSettings()函数

        实际上,使用SHGetSettings()函数是相当简单的,它仅需要两个变量:

void SHGetSettings(LPSHELLFLAGSTATE lpsfs, DWORD dwMask);

SHELLFLAGSTATE是一个非常简洁的结构:

typedef struct

{

BOOL fShowAllObjects : 1;

BOOL fShowExtensions : 1;

BOOL fNoConfirmRecycle : 1;

BOOL fShowSysFiles : 1;

BOOL fShowCompColor : 1;

BOOL fDoubleClickInWebView : 1;

BOOL fDesktopHTML : 1;

BOOL fWin95Classic : 1;

BOOL fDontPrettyPath : 1;

BOOL fShowAttribCol : 1;

BOOL fMapNetDrvBtn : 1;

BOOL fShowInfoTip : 1;

BOOL fHideIcons : 1;

UINT fRestFlags : 3;

} SHELLFLAGSTATE, *LPSHELLFLAGSTATE;

dwMask参数是二进制屏蔽位—对于结构中每一个感兴趣的字段和需要函数恢复的字段,都必须设置适当的屏蔽位。可能的值是:

 

字段

屏蔽位

在文件夹选择对话框中的设置

fShowAllObjects

SSF_SHOWALLOBJECTS

显示所有文件

fShowExtensions

SSF_SHOWEXTENSIONS

隐藏已知文件类型的文件扩展

fNoConfirmRecycle

SSF_NOCONFIRMRECYCLE

None

fShowSysFiles

SSF_SHOWSYSFILES

不显示隐藏文件

fShowCompColor

SSF_SHOWCOMPCOLOR

None

fDoubleClickInWebView

SSF_DOUBLECLICKINWEBVIEW

在‘一般|客户设置’对话框上‘双击打开项’选择

fWin95Classic

SSF_WIN95CLASSIC

在‘一般’页上‘典型风格’选择

fDontPrettyPath

SSF_DONTPRETTYPATH

全部允许大写名字

fMapNetDrvBtn

SSF_MAPNETDRVBUTTON

在工具条中显示‘网络驱动器映射’按钮

fShowAttribCol

SSF_SHOWATTRIBCOL

在‘细节观察’中显示文件属性

fShowInfoTip

SSF_SHOWINFOTIP

对文件夹和桌面项显示弹出的描述

fDesktopHTML

SSF_DESKTOPHTML

在活动桌面关联菜单上设置‘Web页面观察’

fHideIcons

SSF_HIDEICONS

当桌面作为Web页面时,隐藏图标

很多资料都说明fHideIcons是没用的,事实上它仅仅说明当桌面设置成Web模式时,是否桌面上的图标应该被显示。现在让我们查看一些应用,以便探讨可以从这些标志中获得的信息。

观察文件扩展

        头一个用途是程序员是否想要在用户的应用界面中显示文件扩展。如果应用程序需要在任何情况下显示文件名,你就应该依据这个标志的状态使用户能够选择确定是否显示扩展名。

使桌面更具活力

        fHideIcons标志使你在观察模式设置为‘Web页面’时知道桌面上的图标是否可以看到。而fDesktopHTML标志从另一方面告诉你是否桌面使用了一个HTML页作为它的背景。如果桌面是在Web模式中,并且图标不可见,则你就不能在桌面上建立新的快捷方式。

        如果我们仅仅能设置这些位而不能获得它们的状态的话,组合使用fDesktopHTMLfHideIcons是非常有用的。考虑下面的情况:有很多方法清除桌面以禁止公共计算机用户而不是你浏览和运行应用程序。而使用fDesktopHTMLfHideIcons的组合给出了一种新方法。首先,它允许你设置标志显示HTML页面作为桌面的背景,其次隐藏所有桌面上的图标。使用这种方法,你可以把Windows桌面(和机器)变成一个专门运行单一HTML应用的服务器。诚然,任务条还在,但是,你可以通过取得其HWND,然后使用SW_HIDE标志调用ShowWindow()函数来隐藏它:

//任务条是一个窗口类'Shell_TrayWnd'

HWND hwnd = FindWindow("Shell_TrayWnd", NULL);

if(IsWindow(hwnd))

ShowWindow(hwnd, SW_HIDE);

点击列表观察

        Shell 4.71以上版的众多文件夹的设置中,有可能设置文件夹的行为使其在被选中时变成下划线形式,和只要一次点击便可打开它们。你可以通过‘文件夹选项’对话框的‘一般’页设置这些选择。有趣的是,这些风格对于通用控件库版本4.70的列表观察也有效。所以你可以根据fDoubleClickInWebView标志来修改列表观察的活动形式和鼠标跟踪能力。如此,需要考虑下列关系:

LVS_EX_ONECLICKACTIVATE(4.70)

LVS_EX_TWOCLICKACTIVATE(4.70)

LVS_EX_UNDERLINECOLD(4.71)

LVS_EX_UNDERLINEHOT(4.71)

这个列表中的版本号指的是通用控件库的版本号,不是Shell的版本号。版本4.70comctl32.dllIE4.0一同发布(无论活动桌面是否安装)4.71IE4.01

        设置扩展风格的列表观察,需要使用ListView_SetExtendedListViewStyle()函数,它是一个围绕LVM_SETEXTENDEDLISTVIEWSTYLE消息建立的宏。上面列表中的头两种风格的意义是直接的,其它的涉及到热点项(一个术语,用于描述鼠标正在经过的项)LVS_EX_UNDERLINECOLD引起非热点项下划线,而LVS_EX_UNDERLINEHOT则仅仅热点项下划线。

删除操作的确认

        fNoConfirmRecycle标志指示在删除文件之前是否显示确认对话框。可以想象,这仅适用于删除到‘回收站’和Shell操作的场合。然而,即使你没有使用Shell函数,如SHFileOperation(),来删除文件,如果用户希望有这样的询问时,是否不能很好地给出确认提示呢?阅读一下fNoConfirmRecycle可以向这个方向前进一大步,进而使之成为可能。

示例程序

   程序界面,作为这一章最后一个例子,如下图所示,可能你已经猜到了什么是我们要用于建立的程序框架。

                                  

               

这个例子的代码是十分容易的:只需要为‘取得设置’按钮加一个处理器即可,它将引起当前Shell的选项设置被读出。下面的源码产生了在图中所看到的结果。总之,一定要记住在源文件顶部包含#include shlobj.h resource.h

void OnSettings(HWND hDlg)

{

SetDlgItemText(hDlg, IDC_SETTINGS, "");

SHELLFLAGSTATE sfs;

SHGetSettings(&sfs, SSF_DESKTOPHTML | SSF_SHOWALLOBJECTS |

SSF_MAPNETDRVBUTTON | SSF_SHOWATTRIBCOL | SSF_SHOWEXTENSIONS);

TCHAR szBuf[MAX_PATH] = {0};

if(sfs.fDesktopHTML)

lstrcat(szBuf, __TEXT("Active Desktop - View as Web page is active\r\n"));

if(sfs.fMapNetDrvBtn)

lstrcat(szBuf, __TEXT("Network buttons on the toolbar\r\n"));

if(sfs.fShowAllObjects)

lstrcat(szBuf, __TEXT("Shows all files\r\n"));

if(sfs.fShowAttribCol)

lstrcat(szBuf, __TEXT("Shows attributes in Detail view\r\n"));

if(sfs.fShowExtensions)

lstrcat(szBuf, __TEXT("Shows extensions for known file types\r\n"));   

SetDlgItemText(hDlg, IDC_SETTINGS, szBuf);

}

参数设置

        阅读了这些参数的设置,对处理各种情况确实有不少帮助,但是更有兴趣的应该是编程地设置这些特征。不幸地是,到目前为止只有SHSetSettings()例程出现,现在,我们要说明的是有大量的可以达到这个目标的方法可用而不需要微软的帮助。

参数存储在哪里

        正如你已经大致猜到的那样,所有可以使用SHGetSettings()读到的参数都存储在注册表的某个地方。也即,可以由一个相对安全的方法来编程地设置这些参数。

   在向下进行之前,我们要强调一个重点。在官方资料缺乏的情况下,微软在未来的操作系统版本中是可以自由改变注册表键的用途的,这就间接地影响到你的代码。在编写代码时,我们使用的技术在版本4.71下是好用的。

        打开注册表,查看下面的键:

HKEY_CURRENT_USER

\Software

\Microsoft

\Windows

\CurrentVersion

\Explorer

\Advanced

          

似乎我们已经找到了要找的东西。修改这些注册表项是足够简单的,对吗?很不幸,不是这样的—你很快就能注意到,这个值列表缺少了条目数。尤其是‘Web观察’设置。

        回想一下测试反向工程注册表设置的黄金定律:总是比较HKEY_CURRENT_USERHKEY_LOCAL_MACHINE下相同键的内容。这里就是我们要找的:

           

           

正如所见,有一个完整的层次结构复制了与‘文件夹选项’对话框相同的树结构。主节点是一个“group”类型的节点,有自己的Bitmap,和显示名。结构的叶含有一个属性集,其中突出的有两个值:RegPathHKeyRoot

           

       

这就是说,在子树中每一个条目都指向注册表中的另一个键,实际值就存储在那儿,其路径是HKeyRoot\RegPath\ValueName。叶特征确定了显示文字,选项类型(复选框或收音按钮),选择值(选中或没选中),默认值,甚至文件名和有帮助的主题ID等。

        给出了这些之后,处理客户的SHSetSettings()函数就只是简单地读写注册表数据操作了。

 

附加客户选项到标准对话框

        因为在‘文件夹选项’对话框的层次结构和注册表子树布局之间有良好的对应,我们能立即猜测到添加一个新键到注册表应该在标准对话框中产生一个新的客户选项。为了证明这一点,仅需要做一件事情:加一个新键到注册表子树。

        我在Folder定义了一个新键,称为MySetting。然后定义所有在其它叶上看到的值:

       

保存这个改变到注册表之后,期待地打开‘文件夹选项’对话框,没有任何新东西出现。事实上,对此有一个显而易见的原因:对话框仅在能够读出所存储的值时才添加新项。正如早先提到过的那样,这个值存储在注册表的另一个位置—是由HKeyRoot,RegPathValueName指向的。剩下的就是在下面的键上建立一个称为MySetting的新值:

HKEY_CURRENT_USER

\Software

\Microsoft

\Windows

\CurrentVersion

\Explorer

\Advanced

还应该设置所期望选项的默认值。在保存改变和响应‘文件夹选项’对话框后,新的设置就出现了,如下图所示:

                                               

                     

什么时候客户选项是有用的

        在 ‘文件夹选项’对话框中添加新的客户选项不是为了向人炫耀—也不是一项允许用户客户化你的程序的便利方法。我们并不建议你使用这个对话框来设置一个应用的所有设置。但是,考虑使用选项来解决用户界面和文件夹的问题是有价值的。能更好地探索这些特征的模块应该在命名空间扩展应用中。

使用注册表路径的选择完全在于你自己,但是,应该认识到这一点,把你自己的设置存储到远离标准设置的地方,好的选择是使用应用特殊的注册键。

小结

        文件夹是一个广泛的题目,这一章一直在努力给出详细解释。你已经看到怎样浏览特殊文件安夹和怎样与它们一道工作。枚举它们的内容和设置它们的参数。特别,在这一章中还揭示了:

        怎样更好地使用SHBrowseForFolder()

   怎样枚举任何文件夹的内容

   处理特殊系统文件夹的函数

   那些文件夹设置是可读的,怎样编程设置它们

为此,我们构建了潜在有用的函数来扩展 API 所提供的工具。例如 SHEnumFolderContent() SHPathToPidlEx() 辅助例程。此外,我们还揭示了 Shell 怎样存储文件夹的设置,和给出了添加新选项到标准的‘文件夹选项’对话框的方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值