65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (33投票s)

2007年2月27日

CPOL

9分钟阅读

viewsIcon

166146

downloadIcon

4262

本文详细介绍了 Windows Vista 中新提供的 IExplorerBrowser 接口和 Explorer Browser 对象。Explorer Browser 对象允许开发人员在他们的应用程序中托管 Windows Explorer。

Screenshot - ExplorerBrowser1.jpg

引言

Windows Vista 的一项很棒但文档很少的功能是支持托管 Windows Explorer 窗口的 API。上图显示了托管 Explorer 窗口的示例 MFC 应用程序的截图。基于 COM 的 API 非常简单,它允许对 Windows Explorer 窗口进行相当大的控制,方式如下:

  1. 能够控制不同的视图类型:大图标、小图标、平铺、详细信息、缩略图等。
  2. 自动旅行日志:用户浏览历史记录由托管的 Explorer 窗口维护,托管应用程序可以控制前进/后退/向上导航。
  3. 能够处理 Explorer 窗口导航事件并采取适当的操作来控制从托管应用程序内部进行的导航。
  4. 能够显示像 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 安装附带的一个批处理文件。可以从开始菜单启动它,通过快捷方式,如下所示:

Launching the integration tool

运行此工具后,您可以通过启动“工具”->“选项”对话框来验证 VS2005 的 include、header 和可执行文件路径是否已更改。

VS 2005 Tools Options

这里需要注意的重要一点是,应该修改可执行文件目录、include 目录和库文件目录。Windows SDK 附带了新版本的工具,例如 MIDL 编译器,这些工具对于构建 Windows Vista 应用程序是必需的,因此可执行文件路径也应该正确设置。

验证构建环境设置正确后,您就可以开始为 Windows Vista 开发了。要获取所有 Vista API,您需要相应地定义宏 WINVERWIN32_WINNTWIN32_IE

#define WINVER 0x0600
#define _WIN32_WINNT 0x0600
#define _WIN32_IE 0x0700    

现在,您可以在应用程序中使用 Windows Vista API 了。接下来让我们看看如何在 MFC 应用程序中托管 Explorer。

在视图中托管 Explorer 窗口

在此示例应用程序中,我们将把 Explorer 窗口托管在 CView 派生类中。Explorer 可以托管在任何窗口上,没有限制。例如,它甚至可以托管在对话框中。我花了很大的力气才发现视图应该设置 WS_CLIPSIBLINGSWS_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 窗口时包含框架。框架包括主详细信息视图、位置栏、文件夹树和详细信息窗格,如下面的屏幕截图所示。

Framed Explorer Browser Views

默认情况下不显示框架,视图如下所示:

NormalExplorer Browser Views

调用 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 的调用位于 CViewOnDestroy 方法中。

void CExplorerBrowserView::OnDestroy()
{
  CView::OnDestroy();
   
  if(m_dwAdviseCookie)
  {
      m_spExplorerBrowser->Unadvise(m_dwAdviseCookie);
  }

  m_spExplorerBrowser->Destroy();
}

获取选定项目列表

在某些应用程序中,可能需要获取 Explorer 浏览器中的选定项目并对它们执行某些操作。获取选定项目是一个两步过程:

  1. 通过调用 GetCurrenView 方法获取指向 IShellView 接口的接口指针。
    CComPtr<IShellView> spSV;
    if (SUCCEEDED(m_spExplorerBrowser->GetCurrentView(IID_PPV_ARGS(&spSV))))
  2. thus 获得的 IShellView 指针可用于将选定项目作为 OLE DataObject 获取。
    CComPtr<IDataObject> spDataObject;
    if (SUCCEEDED(spSV->GetItemObject(SVGIO_SELECTION, 
                  IID_PPV_ARGS(&spDataObject))))
  3. 最后,可以使用数据对象来获取选定项目,如下所示:
  4. //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日 - 最初发布。
© . All rights reserved.