http://blog.sina.com.cn/s/blog_56dee71a0100fucz.html
MSDN 2005 -> Win32 和 COM 开发 -> User Interface -> Windows User Experience -> Windows Shell -> Windows Shell -> Shell Programmer's Guide -> Shell Basics -> Navigating the Namespace
其实,在翻译这篇文档前,我就根据前面几篇文档的内容,写了个简单的名字空间浏览程序了。它比这篇MSDN文档的示例程序功能完整些,可以说是个简化版的资源管理器程序。其界面如下:

它可以在名字空间中浏览,双击某文件夹项时,将打开文件夹,列出其内容;而双击叶子节点时,将执行与之关联的默认动作,就跟在资源管理器中双击节点类似。比如说,双击某控制面板项将执行相关的控制面板应用;双击某文件时,将启动关联程序对其进行编辑。
从程序角度总结如下:
1 获取文件夹的IShellFolder接口指针
列出文件夹内容是通过IEnumIDList接口进行的,而这个接口要通过IShellFolder接口获得。获取某文件夹IShellFolder接口指针的代码如下:
int CShellNamespace::Goto(LPCTSTR pPath) { if (NULL == m_pFolder) SHGetDesktopFolder(&m_pFolder); if (NULL == m_pFolder) return -1; if (NULL != pPath) { LPTSTR pTmpPath; LPOLESTR pOlePath; ITEMIDLIST* pidl; HRESULT hRet; IShellFolder* pNewFolder; USES_CONVERSION; pTmpPath = StrDup(pPath); if (NULL == pTmpPath) return -1; pOlePath = T2OLE(pTmpPath); hRet = m_pFolder->ParseDisplayName(NULL,NULL,pOlePath,NULL,&pidl,NULL); LocalFree((HLOCAL)pTmpPath); if (FAILED(hRet)) return -1; hRet = m_pFolder->BindToObject(pidl,NULL,IID_IShellFolder,(void**)&pNewFolder); if (FAILED(hRet)) return -1; FreeMemory(m_pIdl); m_pIdl = pidl; m_pFolder->Release(); m_pFolder = pNewFolder; } return RefreshList(); } |
首先是获取桌面的IShellFolder接口指针,然后通过ParseDisplayName获取指定路径的PIDL,再通过BindToObject获取PIDL对应的文件夹的IShellFolder接口指针。要注意的是:
(1) 调用ParseDisplayName时,第三个参数表示文件夹显示名,对于文件系统文件夹,也就是通常所说的路径。这个参数是LPOLESTR类型的,所以要对LPCTSTR类型的路径名进行转换。方法是使用COM编程中的转换宏。这个是从我买的那本烂书《COM+编程指南》中学来的。
(2) 把pNewFolder赋值给m_pFolder之前,先调用m_pFolder->Release()。这是COM编程中,接口指针的引用计数机制要求的。这个还是得益于那本烂书。
(3) 当然,用SHParseDisplayName和SHBindToObject替换IShellFolder的两个同名方法也是可以的。感觉SHxxx方法等价于桌面对象的IShellFolder::xxx方法。
2 列出文件夹内容
int CShellNamespace::RefreshList() { IShellFolder* pDesktop = NULL; IEnumIDList* pEnum = NULL; ITEMIDLIST* pidl = NULL; ITEMIDLIST* pfullidl = NULL; STRRET objName = {0}; LPTSTR pName = NULL; SHFILEINFO sfiFileInfo; CImageList* pImageList; int nIdx,nCount; SHCONTF flags; HRESULT hRet; DWORD dwRet; ASSERT(m_pFolder); ASSERT(m_pListCtrl); SHGetDesktopFolder(&pDesktop); if (NULL == pDesktop) return -1; // 开始枚举 flags = SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN; hRet = m_pFolder->EnumObjects(NULL,flags,&pEnum); if (FAILED(hRet)) return -1; // 复位列表控件 m_pListCtrl->SetRedraw(FALSE); m_pListCtrl->DeleteAllItems(); ResetImageList(); if (m_pFolder != pDesktop) { m_pListCtrl->InsertItem(0,_T("向上"),0); m_pListCtrl->SetItemData(0,0); } pDesktop->Release(); hRet = pEnum->Next(1,&pidl,NULL); while (NOERROR == hRet) { // 取显示名 pfullidl = NULL; ZeroMemory(&objName,sizeof(objName)); objName.uType = STRRET_WSTR; hRet = m_pFolder->GetDisplayNameOf(pidl,SHGDN_NORMAL,&objName); if (S_OK != hRet) goto next_item; hRet = StrRetToStr(&objName,pidl,&pName); if (S_OK != hRet) goto next_item; // 取关联图标 pfullidl = CatIDList(pfullidl,m_pIdl); pfullidl = CatIDList(pfullidl,pidl); if (NULL == pfullidl) pfullidl = pidl; nIdx = 0; pImageList = m_pListCtrl->GetImageList(LVSIL_NORMAL); dwRet = SHGetFileInfo((LPCTSTR)pfullidl,FILE_ATTRIBUTE_NORMAL, &sfiFileInfo,sizeof(sfiFileInfo), SHGFI_ICON | SHGFI_PIDL); if (dwRet > 0) nIdx = pImageList->Add(sfiFileInfo.hIcon); nCount = m_pListCtrl->GetItemCount(); m_pListCtrl->InsertItem(nCount,pName,nIdx); m_pListCtrl->SetItemData(nCount,(DWORD_PTR)pidl); next_item: if (pfullidl != pidl) FreeMemory(pfullidl); if (pName) CoTaskMemFree(pName); if (objName.pOleStr) FreeMemory(objName.pOleStr); hRet = pEnum->Next(1,&pidl,NULL); } pEnum->Release(); m_pListCtrl->SetRedraw(TRUE); UpdateUI(); return 0; } |
要点如下:
(1)调用IShellFolder::EnumObjects方法获取IEnumIDList接口指针,通过它的Next方法枚举文件夹内容。
(2)IEnumIDList::Next方法会返回文件夹内容项的单层PIDL,把它传给IShellFolder::GetDisplayName方法就可以获取内容项的显示名。显示名是以STRRET结构体的形式返回的,需要用StrRetToStr把它转换成字符串形式。
(3)SHGetFileInfo可以获取文件的各种属性,包括图标相关属性。它要求使用全限定PIDL,所以另外编写了CatIDList方法来串接两个PIDL。把当前全限定PIDL,即m_pIdl与内容项的单层PIDL,即pidl串接起来,就得到内容项的全限定PIDL了。这就好像把文件名串接到文件所在文件夹的全路径名之后,就得到文件的全路径名一样。CatIDList方法的代码如下:
// 连接两个ITEMIDLIST ITEMIDLIST* CShellNamespace::CatIDList(ITEMIDLIST* pDstList,const ITEMIDLIST* pSrcList) { const BYTE* pData; unsigned short nDstTotalSize = 0; unsigned short nSrcTotalSize = 0; unsigned short nItemSize,nLastItemSize; LPMALLOC pMalloc; if (S_OK != CoGetMalloc(1,&pMalloc)) return NULL; // Src List Size if (NULL != pSrcList) { pData = (BYTE*)pSrcList; while (0 != (nItemSize = *((unsigned short*)pData))) { nSrcTotalSize += nItemSize; pData += nItemSize; } nSrcTotalSize += 2; } // Dst List Size nLastItemSize = 0; if (NULL != pDstList) { pData = (BYTE*)pDstList; while (0 != (nItemSize = *((unsigned short*)pData))) { nDstTotalSize += nItemSize; pData += nItemSize; nLastItemSize = nItemSize; } } // Copy // To parent folder if (0 == nSrcTotalSize) { nDstTotalSize -= nLastItemSize; pDstList = (ITEMIDLIST*)pMalloc->Realloc(pDstList,nDstTotalSize + 2); if (NULL == pDstList) return NULL; *((BYTE*)pDstList + nDstTotalSize + 0) = 0; *((BYTE*)pDstList + nDstTotalSize + 1) = 0; } // To child folder else { pDstList = (ITEMIDLIST*)pMalloc->Realloc(pDstList,nDstTotalSize + nSrcTotalSize); if (NULL == pDstList) return NULL; CopyMemory((BYTE*)pDstList + nDstTotalSize,pSrcList,nSrcTotalSize); } pMalloc->Release(); return pDstList; } |
只要理解了《Shell名字空间》一文《标识Shell对象》节的内容,就不难理解了。
3 双击内容项的处理
双击“向上”图标时,需要返回到上级目录,列出其内容;如果双击的是文件夹对象,则打开文件夹,列出其内容;否则,对于文件对象,则执行其默认动作,比如说,对于.txt文件,可能是打开记事本程序对其进行编辑;而对于控制面板小应用,则是运行它。当然,这里的“文件夹”是指Shell文件夹,它既可以是文件系统文件夹,也可以是像“我的电脑”、“网络连接”这样的虚拟文件夹。对于文件,即可以是普通的文件系统中的文件,也可以是Shell名字空间的叶子节点,比如说,“鼠标”这个控制面板小应用。
双击内容项由下面的代码进行处理:
int CShellNamespace::GotoSubFolder(DWORD dwData) { const ITEMIDLIST* pidl = (ITEMIDLIST*)dwData; IShellFolder* pNewFolder; HRESULT hRet; ULONG nAttr; if (NULL == pidl) { hRet = SHBindToParent(m_pIdl,IID_IShellFolder,(void**)&pNewFolder,NULL); } else { nAttr = SFGAO_FOLDER; hRet = m_pFolder->GetAttributesOf(1,&pidl,&nAttr); // 浏览内容 if (nAttr & SFGAO_FOLDER) { hRet = m_pFolder->BindToObject(pidl,NULL,IID_IShellFolder,(void**)&pNewFolder); } // 打开对象 else { OpenItem(pidl); return 0; } } if (S_OK != hRet) return -1; m_pIdl = CatIDList(m_pIdl,pidl); if (NULL == m_pIdl) return -1; m_pFolder->Release(); m_pFolder = pNewFolder; return RefreshList(); } |
要点如下:
(1)传入的参数是列表项的附加数据,这在上文的RefreshList方法中被设置为内容项的单层PIDL。对于“向上”图标,设置附加数据为NULL。
(2)用IShellFolder::GetAttributesOf可以获取Shell对象的各种属性。这里获取文件夹内容项的SFGAO_FOLDER属性,即判断内容项是不是子文件夹。如果是子文件夹,就打开它,列出其内容;否则调用OpenItem对其执行默认动作。
(3) OpenItem方法的代码如下:
void CShellNamespace::OpenItem(const ITEMIDLIST* pidl) { ITEMIDLIST* pFullIdl = NULL; SHELLEXECUTEINFO info = {0}; pFullIdl = CatIDList(pFullIdl,m_pIdl); pFullIdl = CatIDList(pFullIdl,pidl); if (NULL == pFullIdl) return; info.cbSize = sizeof(info); info.fMask = SEE_MASK_IDLIST; info.lpIDList = pFullIdl; info.nShow = SW_SHOWNORMAL; ShellExecuteEx(&info); FreeMemory(pFullIdl); } |
参考《启动应用程序》一文的内容,很容易理解的。
4 一个小问题
程序写好后,发现对于控制面板中的“网络连接”、“管理工具”这两个虚拟文件夹,无法打开浏览其内容。因为对于“我的电脑”、“网上邻居”这两个虚拟文件夹,工作是正常的,所以应该不是虚拟文件夹的问题。调试发现,运行到GotoSubFolder方法的下面一行时:
hRet = m_pFolder->BindToObject(pidl,NULL,IID_IShellFolder,(void**)&pNewFolder);
在Watch窗口输入hRet,其值显示为“0x800401f0 尚未调用CoInitialize。”;而输入@err,hr,值显示为“0x0000007e 找不到指定的模块。”。原来没有显式调用CoInitialize,程序也好像能够正确工作,我就以为对于MFC应用程序,初始化的时候,CoInitialize是已经被CWinApp调用了的,没想到会出这样的错误。在InitInstance方法中加入对CoInitialize的调用,然后调试程序就OK了。对于使用COM的程序,应该对每个线程分别调用CoInitialize或者CoInitializeEx来初始化COM系统。
以下是MSDN文档原文翻译:
现在已经了解浏览名字空间中任何地方所需的所有元素了。开始浏览的最简单方法是调用SHGetDesktopFolder获取桌面的IShellFolder接口。然后,可以按照下列步骤在名字空间中向下浏览:
-
枚举文件夹的内容
-
确定哪些对象是子文件夹,并且选择其中一个
-
绑定到选定的子文件夹,获取其IShellFolder接口
重复上述步骤直到达到浏览目标。
浏览名字空间的简单例子
下面的代码是一个简单的控制台应用程序。它展示了前一节讨论的过程。为使结构清晰,省略了错误检查。程序进行下列任务:
<…… 省略示例代码 ……>