使用新的 Vista 托管 API 在应用程序中托管 Windows Explorer






4.92/5 (33投票s)
本文详细介绍了 Windows Vista 中新提供的 IExplorerBrowser 接口和 Explorer Browser 对象。Explorer Browser 对象允许开发人员在他们的应用程序中托管 Windows Explorer。
引言
Windows Vista 的一项很棒但文档很少的功能是支持托管 Windows Explorer 窗口的 API。上图显示了托管 Explorer 窗口的示例 MFC 应用程序的截图。基于 COM 的 API 非常简单,它允许对 Windows Explorer 窗口进行相当大的控制,方式如下:
- 能够控制不同的视图类型:大图标、小图标、平铺、详细信息、缩略图等。
- 自动旅行日志:用户浏览历史记录由托管的 Explorer 窗口维护,托管应用程序可以控制前进/后退/向上导航。
- 能够处理 Explorer 窗口导航事件并采取适当的操作来控制从托管应用程序内部进行的导航。
- 能够显示像 Explorer 中那样的导航框架。
此功能在安装程序 IDE、CD 刻录机或任何其他显示 Shell 或文件系统视图并允许用户将项目从 Shell 视图拖放到应用程序窗口的应用程序中非常有用。此功能的另一个用途可能是 Visual Studio 插件,它可以在 IDE 中显示文件系统视图。在您的自定义应用程序中可能还有其他用途,唯一(较大的)缺点是此功能仅在 Windows Vista 中可用。
准备构建环境
首先,您需要安装 Microsoft Windows Vista SDK。安装可以作为 Web 安装或 DVD 映像提供。Web 安装是一个小的 exe 文件,您运行它来启动安装,然后该 exe 会根据需要从 Internet 下载文件。Web 安装通常需要 2-3 小时才能完成。但是,如果您想在多台计算机上安装 SDK,或者稍后在同一台计算机上重新安装,您应该下载安装的 DVD 映像。您可以从这个 链接 下载 SDK。
下载并安装 SDK 后,您需要将 SDK 与 VS2005 集成。SDK 包含新的 C++ 头文件和库文件,其中包含 Windows Vista 中可用新 API 的定义。因此,您需要将 VS2005 头文件和库文件的路径设置为指向 VS2005 的安装目录。最简单的方法是运行 Windows SDK 安装附带的一个批处理文件。可以从开始菜单启动它,通过快捷方式,如下所示:
运行此工具后,您可以通过启动“工具”->“选项”对话框来验证 VS2005 的 include、header 和可执行文件路径是否已更改。
这里需要注意的重要一点是,应该修改可执行文件目录、include 目录和库文件目录。Windows SDK 附带了新版本的工具,例如 MIDL 编译器,这些工具对于构建 Windows Vista 应用程序是必需的,因此可执行文件路径也应该正确设置。
验证构建环境设置正确后,您就可以开始为 Windows Vista 开发了。要获取所有 Vista API,您需要相应地定义宏 WINVER
、WIN32_WINNT
和 WIN32_IE
。
#define WINVER 0x0600 #define _WIN32_WINNT 0x0600 #define _WIN32_IE 0x0700
现在,您可以在应用程序中使用 Windows Vista API 了。接下来让我们看看如何在 MFC 应用程序中托管 Explorer。
在视图中托管 Explorer 窗口
在此示例应用程序中,我们将把 Explorer 窗口托管在 CView
派生类中。Explorer 可以托管在任何窗口上,没有限制。例如,它甚至可以托管在对话框中。我花了很大的力气才发现视图应该设置 WS_CLIPSIBLINGS
和 WS_CLIPCHILDREN
Windows 样式,否则托管的 Explorer 会有一些绘图问题。这可以在 PreCreateWindow
函数中完成。
BOOL CExplorerBrowserView::PreCreateWindow(CREATESTRUCT& cs) { if (!CView::PreCreateWindow(cs)) return FALSE; cs.style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; return TRUE; }
实际的托管 API 是一个 COM API,我们需要使用的接口是 IExplorerBrowser
接口。此接口的指针应该是 CView
派生类的成员,并且其生命周期应与 CView
派生类相同。在示例应用程序中,IExplorerBrowser
接口的智能指针是 CExplorerBrowserView
类的一个成员。
class CExplorerBrowserView : public CView { private: CComPtr<IExplorerBrowser> m_spExplorerBrowser;
接下来,我们需要创建实际的 Explorer 窗口作为视图窗口的子窗口。这可以在创建窗口时调用的 OnCreate
方法中完成。此外,我们还可以处理托管浏览器时可能发生的任何错误。首先,我们需要实例化具有 CLSID CLSID_ExplorerBrowser
的 ExplorerBrowser COM 类。成功实例化对象后,调用 Initialize
方法会将 Explorer 窗口创建为视图窗口的子窗口。以下是创建 Explorer 窗口的代码:
int CExplorerBrowserView::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CView::OnCreate(lpCreateStruct) == -1) return -1; HRESULT hr = SHCoCreateInstance(NULL, &CLSID_ExplorerBrowser, NULL, IID_PPV_ARGS(&m_spExplorerBrowser)); if (SUCCEEDED(hr)) { if (theApp.ShowFrames()) m_spExplorerBrowser->SetOptions(EBO_SHOWFRAMES); FOLDERSETTINGS fs; fs.fFlags = 0; fs.ViewMode = FVM_DETAILS; hr = m_spExplorerBrowser->Initialize(m_hWnd, CRect(0, 0, 0, 0), &fs); } return SUCCEEDED(hr) ? 0 : -1; }
请注意,在调用 Initialize
方法之前调用了 SetOptions
方法。SetOptions
允许您指定有关显示外观的更多选项。例如,您可以在显示 Explorer 窗口时包含框架。框架包括主详细信息视图、位置栏、文件夹树和详细信息窗格,如下面的屏幕截图所示。
默认情况下不显示框架,视图如下所示:
调用 SetOptions(EBO_SHOWFRAMES)
会显示框架。一旦调用了 Initialize
方法,就无法打开或关闭框架。如果您打算动态显示框架,则必须销毁 Explorer 浏览器并再次调用 Initialize
方法。
视图销毁后的清理工作
当视图被销毁时,Explorer 浏览器对象应该被清理。请注意,仅仅释放 COM 接口指针是不够的。您必须在 OnDestroy
方法中调用 IExplorerBrowser::Destroy
,如下所示:
void CExplorerBrowserView::OnDestroy()
{
CView::OnDestroy();
m_spExplorerBrowser->Destroy();
}
调整 Explorer 浏览器窗口的大小
如果视图完全由 Explorer 浏览器窗口组成,则需要调整其大小以适应视图。Explorer Browser 对象提供 SetRect
方法,可用于调整窗口大小。调整大小的代码放在视图对象的 OnSize
方法中,如下所示:
void CExplorerBrowserView::OnSize(UINT nType, int cx, int cy) { CView::OnSize(nType, cx, cy); m_spExplorerBrowser->SetRect(NULL, CRect(0, 0, cx, cy)); }
到目前为止,我们已经学会了如何创建 Explorer 浏览器窗口、调整其大小以及销毁它。但它仍然没有做什么有趣的事情。现在在下一节中,我们将看看如何使用 Explorer 浏览器进行导航。
使用 Explorer 浏览器进行导航
托管的 Explorer 可以导航到任何 Shell 文件夹,包括文件系统文件夹和非文件系统文件夹(如网络、打印机和控制面板)。由于 Windows Shell 项目不仅限于文件系统,因此 Shell 中的项目不是通过路径标识的,而是通过项 ID 列表 (PIDL) 标识的。(项 ID 列表结构在 MSDN 提供的 Shell 文档中有更详细的描述。)IExplorerBrowser
提供了一个名为 BrowseToIDList
的函数,该函数接受 Shell 文件夹的项 ID 列表,浏览器将导航到该文件夹。要导航到文件系统路径,我们必须将文件系统路径转换为项 ID 列表。以下代码显示了如何将 Explorer 浏览器导航到由文件路径指定的一个文件夹。
bool CExplorerBrowserView::NavigateTo(LPCTSTR szPath) { HRESULT hr = S_OK; LPITEMIDLIST pidlBrowse; if (FAILED(hr = SHParseDisplayName(szPath, NULL, &pidlBrowse, 0, NULL))) { ATLTRACE("SHParseDisplayName Failed! hr = %d\n", hr); return false; } if (FAILED(hr = m_spExplorerBrowser->BrowseToIDList(pidlBrowse, 0))) ATLTRACE("BrowseToIDList Failed! hr = %d\n", hr); ILFree(pidlBrowse); return SUCCEEDED(hr); }
NavigateTo
函数接受一个文件夹的路径。它首先使用 SHParseDisplayName
函数将文件夹转换为 PIDL
,最后调用 BrowseToIDList
函数使 Explorer 浏览器导航到该文件夹。最后,它通过调用 ILFree
来释放分配的 PIDL
。
Explorer 浏览器还维护一个访问过的所有文件夹的旅行日志。可以以编程方式向前或向后导航。这可以通过调用 BrowseToIDList
函数来完成,如下所示:
void CExplorerBrowserView::OnViewBack() { m_spExplorerBrowser->BrowseToIDList(NULL, SBSP_NAVIGATEBACK); } void CExplorerBrowserView::OnViewForward() { m_spExplorerBrowser->BrowseToIDList(NULL, SBSP_NAVIGATEFORWARD); }
监听导航事件
除了以编程方式导航之外,Explorer 浏览器还提供了监听导航事件的机制,这些事件在用户通过 Explorer 用户界面(例如,通过双击文件夹)进行导航时发生。这可以通过向 Explorer 浏览器对象提供实现 IExplorerBrowserEvents
的对象来完成。示例中的 CExplorerBrowserEvents
类实现了 IExplorerBrowserEvents
并将其委托给主视图类。这是 CExplorerBrowserEvents
的代码:
class CExplorerBrowserEvents : public CComObjectRootEx<CComSingleThreadModel>, public IExplorerBrowserEvents { private: CExplorerBrowserView* m_pView; public: BEGIN_COM_MAP(CExplorerBrowserEvents) COM_INTERFACE_ENTRY(IExplorerBrowserEvents) END_COM_MAP() public: void SetView(CExplorerBrowserView* pView) { m_pView = pView; } public: STDMETHOD(OnNavigationPending)(PCIDLIST_ABSOLUTE pidlFolder) { return m_pView ? m_pView->OnNavigationPending(pidlFolder) : E_FAIL; } STDMETHOD(OnViewCreated)(IShellView *psv) { if (m_pView) m_pView->OnViewCreated(psv); return S_OK; } STDMETHOD(OnNavigationComplete)(PCIDLIST_ABSOLUTE pidlFolder) { if (m_pView) m_pView->OnNavigationComplete(pidlFolder); return S_OK; } STDMETHOD(OnNavigationFailed)(PCIDLIST_ABSOLUTE pidlFolder) { if (m_pView) m_pView->OnNavigationFailed(pidlFolder); return S_OK; } };
Explorer 浏览器触发的所有四个事件都在 CExplorerBrowserEvents
类中处理,并被委托回托管的 CExplorerBrowserView
实例。视图本身将事件处理方法声明为虚拟的,以便派生类可以覆盖它们。
class CExplorerBrowserView : public CView { protected: virtual HRESULT OnNavigationPending(PCIDLIST_ABSOLUTE pidlFolder); virtual void OnNavigationComplete(PCIDLIST_ABSOLUTE pidlFolder); virtual void OnViewCreated(IShellView *psv); virtual void OnNavigationFailed(PCIDLIST_ABSOLUTE pidlFolder); ... }
让我们更详细地了解一下这些事件方法。其中三个方法以文件夹的 PIDL
作为参数。
- 当导航由用户或以编程方式启动时,将调用
OnNavigationPending
方法。通过从方法返回失败的HRESULT
可以取消实际的导航过程。 - 导航完成后调用
OnNavigationComplete
方法;此方法是更新 UI 以使其与导航的文件夹同步的正确位置。 OnViewCreated
方法在OnNavigationPending
方法之后立即被调用。Explorer 在每次导航到文件夹时都会销毁并重新创建列表视图窗口。此方法接受指向IShellView
接口的指针,该接口提供了额外的查询和控制视图的方法。- 最后,如果文件夹导航因某种原因失败,将调用
OnNavigationFailed
方法。
实现 IExplorerBrowserEvents
后,我们需要创建实现类的实例(在本例中为 CExplorerBrowserEvents
),并将其提供给 Explorer 浏览器对象。这是通过调用 IExplorerBrowser::Advise
方法来完成的。应在将浏览器导航到特定文件夹之前调用此方法。对于 CExploreBrowserView
,此函数在 Initialize
调用之后立即在 OnCreate
函数中调用。
int CExplorerBrowserView::OnCreate(LPCREATESTRUCT lpCreateStruct) { .... CComObject<CExplorerBrowserEvents>* pExplorerEvents; if (SUCCEEDED(CComObject<CExplorerBrowserEvents>:: CreateInstance(&pExplorerEvents))) { pExplorerEvents->AddRef(); pExplorerEvents->SetView(this); m_spExplorerBrowser->Advise(pExplorerEvents, &m_dwAdviseCookie); pExplorerEvents->Release(); } }
调用 Advise
方法可确保 CExplorerBrowserEvents
类接收每个事件的通知。匹配的 Unadvise
方法调用会阻止 Explorer 浏览器向该对象通知事件。Advise
方法返回一个 DWORD
cookie,可以将其提供给 Unadvise
方法。Unadvise
的调用位于 CView
的 OnDestroy
方法中。
void CExplorerBrowserView::OnDestroy() { CView::OnDestroy(); if(m_dwAdviseCookie) { m_spExplorerBrowser->Unadvise(m_dwAdviseCookie); } m_spExplorerBrowser->Destroy(); }
获取选定项目列表
在某些应用程序中,可能需要获取 Explorer 浏览器中的选定项目并对它们执行某些操作。获取选定项目是一个两步过程:
- 通过调用
GetCurrenView
方法获取指向IShellView
接口的接口指针。CComPtr<IShellView> spSV; if (SUCCEEDED(m_spExplorerBrowser->GetCurrentView(IID_PPV_ARGS(&spSV))))
- thus 获得的
IShellView
指针可用于将选定项目作为 OLE DataObject 获取。CComPtr<IDataObject> spDataObject; if (SUCCEEDED(spSV->GetItemObject(SVGIO_SELECTION, IID_PPV_ARGS(&spDataObject))))
- 最后,可以使用数据对象来获取选定项目,如下所示:
//Code adapted from https://codeproject.org.cn/shell/shellextguide1.asp FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; STGMEDIUM stg; stg.tymed = TYMED_HGLOBAL; if (SUCCEEDED(spDataObject->GetData(&fmt, &stg))) { HDROP hDrop = (HDROP) GlobalLock ( stg.hGlobal ); UINT uNumFiles = DragQueryFile ( hDrop, 0xFFFFFFFF, NULL, 0 ); HRESULT hr = S_OK; for(UINT i = 0; i < uNumFiles; i++) { TCHAR szPath[_MAX_PATH]; szPath[0] = 0; DragQueryFile(hDrop, i, szPath, MAX_PATH); if (szPath[0] != 0) arrSelection.Add(szPath); } GlobalUnlock ( stg.hGlobal ); ReleaseStgMedium ( &stg );
上面的代码将选定项目的路径添加到 CStringArray
对象。
CExploreBrowserView
类提供了一个名为 GetSelectedItems
的方法,该方法接受一个 CStringArray
引用;返回时,字符串数组会填充每个选定项目的路径。
结论与未来工作
Windows Vista 在 Shell 中有很多此类不错的增强功能,但不幸的是,它们的文档并不完善。我希望本文和示例能帮助一些人在自己的应用程序中使用 Explorer 浏览器。我之所以选择 MFC 作为本文的素材,是因为使用 MFC 开发示例比较容易,但我正在致力于将应用程序移植到 .NET 和 C#。请继续关注未来的更新。
历史
- 2006年2月27日 - 最初发布。