ShellFolderTree






4.97/5 (21投票s)
2002 年 5 月 15 日
6分钟阅读

203536

3417
模仿和扩展 Shell 的文件夹树控件功能
引言
不久前,我需要一个“Shell 文件夹树”控件——一个类似于资源管理器外壳窗口左侧窗格中树的树形控件。虽然在用户应用程序中使用实际的 Shell 对象在技术上是可行的,但这并不容易。(有关想法,请参阅 Leon Finker 关于托管 Shell 视图的优秀示例。)此外,我还希望我的控件能显示文件,而不仅仅是文件夹对象。
此类控件的几种实现方式已经存在。有些是 MFC,有些是 WTL,有些是 ActiveX,还有些是纯 Win32。我没有找到任何一个拥有我所有期望功能,并且价格在我愿意支付的范围(0美元)内的。因此,作为任何一名合格的 C++ 程序员都会做的事情,我编写了自己的。本文将发布这份工作,并讨论一些更有趣的特性。我无法支持此代码——如果您在应用程序中使用它,您将负全责。但是,如果您在已发布的应用程序中使用它,我希望能收到您的消息!
ShellControls 是一个用 ATL 编写的 ActiveX 控件库。它包含一个名为“ShellFolderTree”的控件(尽管我最初设想了一系列“shell 控件”,但这是我唯一一个完成并具有任何健壮性的控件)。此版本的库使用 VC++ .NET 和 ATL 7.0 编译。将其在 MSVC 6.0 中运行应该很简单。
原始源文件是由应用程序向导生成的标准 ATL 控件。我要求向导生成对双接口和连接点的支持。除此之外,所有附加功能都是手工编码的。您还会注意到我倾向于手工调整(和手工重新格式化)向导代码。我个人偏好将实现与定义分开,因此大部分代码都位于 .cpp 文件中。
在开始之前,我想感谢 Oz Solomonvitch 在项目中使用的部分代码。特别是,我借用了他的 CPIDL 类,他用它来实现他流行的 Visual C++ 6.0 WndTabs 插件的部分功能。
该控件是一个“仅带窗口”的控件,包含一个 Win32 树控件,由 ATL CContainedWindow
类封装。初始化树控件并执行其他依赖于树的初始化任务的函数是 CShellFolderTree::OnCreate
。这里有几点需要注意:
- 树形控件是使用可以通过操作控件属性来设置的 Windows 样式创建的。通过这种方式,您可以在控件实例化后但在容器提示它创建其窗口之前预设控件的外观和感觉。
- 我创建了一个特定于控件实例的互斥体。这将用于同步从不同线程调用的针对控件的操作。
- 我启动了一个后台线程,用于监视树中可见文件夹的文件系统更改。
- 我用我们在
FinalConstruct
中加载的图像列表初始化了树控件的图像列表。该列表包含文件夹和许多 shell 对象的系统定义图像。我还从某个地方借用了GetSystemImageList
的代码——但我很抱歉不记得是谁写的。向那位作者致敬和感谢。这只在 Windows NT 4.0 及更早的系统上是必要的——操作系统的更高版本在 API 中具有此功能。 - 最后,我为控件启用了拖放功能(由属性定义),并使用指定的文件夹对其进行初始化,该文件夹默认是桌面对象。
关于该控件第一个有趣的地方是填充树的代码。为了提高速度,树只填充已展开和可见的文件夹。也就是说,一个可以展开的节点(文件夹)实际上直到用户第一次展开该文件夹时才会填充内容。因此,您可能会注意到,当展开一个包含许多子项的文件夹时,会有一个短暂的停顿。这是有意为之的,并模仿了资源管理器中“真实”shell 树的工作方式。
CShellFolderTree::AddFolderContents
是负责树中任何给定文件夹的初始填充的方法。(CShellFolderTree::RefreshFolderContents
功能类似,但用于文件夹已展开一次后。)它在调用时带有一个 HTREEITEM
,表示要填充的节点/文件夹。树项缓存相对 PIDL (shell Item ID List),它(通常)唯一地标识该节点的 shell 对象。通过树项的父项进行简单的递归可以构建一个绝对 PIDL。
实际工作由 CShellFolderTree::BuildInsertList
完成。InsertList
是 InsertStructs
的 STL 向量,InsertStructs
是一种派生自并扩展系统 TVINSERTSTRUCT
的数据类型。一旦获取了树项表示的 shell 对象的绝对 PIDL,代码就会绑定到关联的 shell 对象并检索其 IShellFolder
接口。IShellFolder
提供了一个枚举方法,该方法检索 IEnumIDList
接口。调用此接口上的 Next
使代码能够访问每个子 PIDL。
HRESULT CShellFolderTree::BuildInsertList(HTREEITEM hFolder, V_InsertStruct* pvecInserts, char* pszFolderPathRet, bool bIncludePaths) { // get item's pidl: CPIDL pidlFolder; if (FALSE == GetTreeItemAbsPIDL(hFolder, pidlFolder)) return FALSE; if (pszFolderPathRet) ::SHGetPathFromIDList(pidlFolder, pszFolderPathRet); // get folder interface: IShellFolderPtr piFolder; HRESULT hr = m_piDesktopFolder->BindToObject(pidlFolder, NULL, (IShellFolder), reinterpret_cast<void**>(&piFolder)); if (FAILED(hr)) m_piDesktopFolder.QueryInterface(&piFolder); // enumerate objects in folder: IEnumIDListPtr piEnum; hr = piFolder->EnumObjects(NULL, SHCONTF_FOLDERS | (m_bShowHidden ? SHCONTF_INCLUDEHIDDEN : 0) | (m_bShowFiles ? SHCONTF_NONFOLDERS : 0) , &piEnum); if (FAILED(hr)) return hr; INT iOverlayIndex(0); DWORD dwStyle(0); LPITEMIDLIST pidlNext; SHFILEINFO sfi; MSG msg; while (S_OK == piEnum->Next(1, &pidlNext, NULL)) { // pump messages while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) ::DispatchMessage(&msg); // wrap the pidl, generate an absolute pidl: CPIDL* ppidlChild = new CPIDL(pidlNext); if (NULL == ppidlChild) return E_OUTOFMEMORY; CPIDL pidlAbsChild; CPIDL::Concat(pidlFolder, *ppidlChild, pidlAbsChild); // get object info: icon, display name, attributes: sfi.iIcon = 0; ::SHGetFileInfo((LPCSTR)(LPCITEMIDLIST)pidlAbsChild, 0, &sfi, sizeof(SHFILEINFO), SHGFI_PIDL | SHGFI_DISPLAYNAME | SHGFI_ATTRIBUTES | (m_bHasIcons ? SHGFI_SYSICONINDEX | SHGFI_SMALLICON : 0)); // fix odd bug where all bits get lit for control panel objects if (0xFFFFFFFF == sfi.dwAttributes) sfi.dwAttributes = 0; // determine if folder is empty or not: if (sfi.dwAttributes & SFGAO_FOLDER && !(sfi.dwAttributes & SFGAO_HASSUBFOLDER)) { char szPath[_MAX_PATH]; if (::SHGetPathFromIDList(pidlAbsChild, szPath)) { lstrcat(szPath, "\\*.*"); BOOL bRet(FALSE); WIN32_FIND_DATA fd; HANDLE hff = ::FindFirstFile(szPath, &fd); while (hff && fd.cFileName[0] == '.' && (bRet = ::FindNextFile(hff, &fd))); if (bRet) sfi.dwAttributes |= SFGAO_HASSUBFOLDER; ::FindClose(hff); } } // ghosted image for hidden files, overlay image for shortcuts, shares: dwStyle = sfi.dwAttributes & SFGAO_GHOSTED ? TVIS_CUT : 0; if (m_bHasIcons && m_bHasOverlayIcons) { dwStyle |= sfi.dwAttributes & SFGAO_LINK ? INDEXTOOVERLAYMASK(2) : 0; dwStyle |= sfi.dwAttributes & SFGAO_SHARE ? INDEXTOOVERLAYMASK(1) : 0; } // initialize new insert item: SFolderTreeInsertStruct ftis(hFolder); ftis.Set(ppidlChild, sfi.szDisplayName, sfi.dwAttributes, sfi.iIcon, sfi.iIcon, dwStyle); // store path too? if (bIncludePaths) ::SHGetPathFromIDList(pidlAbsChild, ftis.m_szPath); // store pvecInserts->push_back(ftis); } return S_OK; }
在可能耗时的枚举过程中,使用了一个旧的 Win32 技巧来保持用户应用程序的响应性。在构成枚举的循环中,我们有一个迷你消息泵,可以实时分派任何排队的消息。
对于每个访问过的项目,我们构建一个绝对 PIDL 并确定对象的一些属性。什么图标代表此项目?它的显示名称是什么?它是一个文件夹吗?它是隐藏的吗?等等。
此数据被收集并缓存到 InsertStruct
中,然后附加到最终将返回的向量中。
完成后,CShellFolderTree::AddFolderContents
遍历向量并插入项目。然后它对树节点进行排序。
为什么不直接添加文件夹内容?为什么要使用 InsertList
?主要原因是为了减少代码量并简化实现。其他方法也更新树(参见 CShellFolderTree::RefreshFolderContents
),它们都使用 CShellFolderTree::BuildInsertList
,尽管它们有不同的插入项目逻辑。此外,当我编写代码时,我正在尝试不同的排序方案;一度在插入前对向量进行排序。
该控件的另一个有趣方面是它能够在后台线程中监视可见文件夹,并在文件系统发生更改时实时更新树。
静态 CShellFolderTree::MonitorThreadProc
在后台高效运行,使用 Win32 文件系统更改通知和 ::MsgWaitForMultipleObjects
。它还处理到达线程消息队列的任何开始或结束监视文件夹的请求。
DWORD CShellFolderTree::MonitorThreadProc(LPVOID pvThis) { // get our context: CShellFolderTree* pThis = reinterpret_cast<CShellFolderTree*>(pvThis); // maps track the change-handles/tree items we're monitoring: M_HandleToTreeitem mapHdlToTi; M_TreeitemToHandle mapTiToHdl; // cycle for (;;) { // generate an array of handles to monitor: typedef std::vector<HANDLE> V_Handle; V_Handle vecHandles; for (M_HandleToTreeitem::iterator it = mapHdlToTi.begin() ; it != mapHdlToTi.end() ; it++) vecHandles.push_back(it->first); // wait efficiently for something to happen: DWORD dwWaitRet = ::MsgWaitForMultipleObjects(vecHandles.size(), &vecHandles[0], FALSE, INFINITE, QS_ALLINPUT); // what triggered? if (dwWaitRet >= WAIT_OBJECT_0 && dwWaitRet < WAIT_OBJECT_0 + vecHandles.size()) { // change notification: // refresh folder: M_HandleToTreeitem::iterator it = mapHdlToTi.find(vecHandles[dwWaitRet]); if (it != mapHdlToTi.end()) pThis->RefreshFolderContents(it->second); // get next change notification for this folder: ::FindNextChangeNotification(vecHandles[dwWaitRet]); } else if (dwWaitRet == WAIT_OBJECT_0 + vecHandles.size()) { // message in thread message queue: MSG msg; while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { switch (msg.message) { case WM_QUIT: case WM_USER_MONITOR_RESET: { // close any remaining change-notification handles: for (M_HandleToTreeitem::iterator it = mapHdlToTi.begin() ; it != mapHdlToTi.end() ; it++) ::FindCloseChangeNotification(it->first); mapHdlToTi.clear(); mapTiToHdl.clear(); if (WM_QUIT == msg.message) ::ExitThread(0); } break; case WM_USER_MONITOR_FOLDER: if (0x1 == pThis->m_bAutoUpdate) { // add or remove folder monitor: CTreeLock lock(pThis); pThis->MonitorFolder(mapHdlToTi, mapTiToHdl, reinterpret_cast<HTREEITEM>(msg.wParam), msg.lParam ? true : false); } break; default: break; } } } } return 0; }
当树视图项展开时 (CShellFolderTree::OnItemExpanded
),节点会刷新,并向文件系统监视线程发送一条消息,请求监视相应的文件夹。如果文件系统触发该文件夹的更改通知,监视线程会自动循环并刷新树中表示该文件夹的节点。
当树视图项折叠时,会向监视线程发送一条消息,告知它停止监视相应的文件夹。
该控件还支持各种其他功能,这些功能的实现大多是直接的。最复杂的是模拟 Explorer shell 行为的功能:双击项目会调用对象的默认操作,右键单击会显示对象的上下文菜单,与 shell 兼容的拖放等。
现在,有人想看 .NET 的 MC++ 版本吗?
尽情享用!