Introduction
A neat but barely documented feature of Windows Vista is the API support to host a Windows Explorer window. The image above shows the screenshot of the sample MFC application hosting the Explorer window. The COM based API is quite simple, and it allows a considerable control over the Windows Explorer window, in the following ways:
- Ability to control the different view types: Large Icon, Small Icon, Tiles, Details, Thumbnails etc.
- Automatic TravelLog: The user's browser history is maintained by the hosted Explorer window, and the hosting application can control the forward/back/up navigation.
- Ability to handle the Explorer window navigation events and take appropriate actions to control the navigation from within the hosted application.
- Ability to display navigational frames like that in the Explorer.
This feature is extremely useful in applications such as Installer IDEs, CD Burners, or any other application which shows a view of the Shell or file system and allows users to drag and drop items from the Shell view to the application window. Another use of this feature may be in a Visual Studio add-in that can display a file system view within the IDE. There might be more uses in your custom application with the only (big) drawback being that this feature is only available in Windows Vista.
Preparing the Build Environment
To start, you need to install the Microsoft Windows Vista SDK. The installation is available as a web install or a DVD image. The web install is a small exe which you run to start the installation, and the exe downloads the files from internet on an as needed basis. The web install normally takes around 2-3 hours to complete. However, if you want to install the SDK on multiple machines, or reinstall on the same machine at a later time, you should download the DVD image of the installation. You can download the SDK from this link.
Once you have downloaded and installed the SDK, you need to integrate the SDK with VS2005. The SDK ships with new C++ header and library files that contain the definition of the new APIs available in Windows Vista. Thus, you need to set the path of your VS2005 header and library files to point to the installation directory of VS2005. The easiest way to do that is to launch a bat file that comes with the Windows SDK installation. This can be launched from the Start menu, through a shortcut as shown below:
Once you have run this tool, you can verify that the path of the include, header, and executable file of VS2005 has changed, by launching the Tools -> Options dialog:
An important thing to note here is that the executable files directory, include directory, and the library files directory should be modified. The Windows SDK ships with newer versions of tools such as the MIDL compiler, which are needed to build applications for Windows Vista, and so that path of executable files should also be set correctly.
Once you have verified that the build environment is set up properly, you can start developing for Windows Vista. To get all the Vista APIs, you need to define the macros WINVER
, WIN32_WINNT
, and WIN32_IE
appropriately:
#define WINVER 0x0600
#define _WIN32_WINNT 0x0600
#define _WIN32_IE 0x0700
Now, you can start using the Windows Vista APIs in your application. Let's move next to see how the Explorer can be hosted in an MFC application.
Hosting the Explorer Window in the View
In this sample application, we will host the Explorer window in a CView
derived class. There is no restriction on what window the Explorer can be hosted on. For example, it can even be hosted in a dialog. I found it the hard way that the view should have the WS_CLIPSIBLINGS
and WS_CLIPCHILDREN
Windows styles set, otherwise the hosted Explorer has some painting issues. This can be done in the PreCreateWindow
function.
BOOL CExplorerBrowserView::PreCreateWindow(CREATESTRUCT& cs)
{
if (!CView::PreCreateWindow(cs))
return FALSE;
cs.style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
return TRUE;
}
The actual hosting API is a COM API, and the interface which we need to use is the IExplorerBrowser
interface. A pointer to this interface should be a member of the CView
derived class, and it should have the same lifetime as the CView
derived class. In the sample application, a smart pointer to the IExplorerBrowser
interface is made a member of the CExplorerBrowserView
class.
class CExplorerBrowserView : public CView
{
private:
CComPtr<IExplorerBrowser> m_spExplorerBrowser;
Next, we need to create the actual Explorer window as a child of the view window. This can be done in the OnCreate
method which gets called when the window is created. Also, we can handle any error that may occur when hosting the browser. First, we need to instantiate the ExplorerBrowser COM class which has the CLSID of CLSID_ExplorerBrowser
. Once the object is instantiated successfully, a call to the Initialize
method creates the Explorer window as a child of the view window. The following is the code to create the Explorer window:
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;
}
Notice the call to the SetOptions
method before calling the Initialize
method. The SetOptions
allows you to specify more options on how the display should look like. For example, you can include frames when displaying the Explorer window. The frames consist of the main details view, the places bar, the folders tree, and the details pane, as shown in the screenshot below:
The default is to not show the frames, and the view looks like the following:
The call to SetOptions(EBO_SHOWFRAMES)
displays the frames. Once the Initialize
method is called, it is not possible to turn on or turn off the frames. If you intend to show frames dynamically, you have to destroy the Explorer browser and call the Initialize
method again.
Cleaning Up After the View is Destroyed
When the view is destroyed, the Explorer browser object should be cleaned up. Note that merely releasing the COM interface pointer is not sufficient. You have to call IExplorerBrowser::Destroy
in the OnDestroy
method as shown below:
void CExplorerBrowserView::OnDestroy()
{
CView::OnDestroy();
m_spExplorerBrowser->Destroy();
}
Sizing the Explorer Browser Window
If the view consists of entirely the Explorer browser window, it needs to be sized to fit the view. The Explorer Browser object provides the SetRect
method that can be used to resize the window. The code to resize is placed in the OnSize
method of the view object, and is shown below:
void CExplorerBrowserView::OnSize(UINT nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
m_spExplorerBrowser->SetRect(NULL, CRect(0, 0, cx, cy));
}
So far, we have learnt how to create the Explorer browser window, size it, and destroy it. But it still does not do anything interesting. Now in the next section, we will see how to navigate using the Explorer browser.
Navigating Using the Explorer Browser
The hosted Explorer can be navigated to any Shell folder which includes file system folders and non-file system folders like Network, Printers, and Control Panel. Because the Windows Shell items are not limited to just the file system, items in the Shell are not identified by the path, rather they are identified by item ID lists (PIDL). (The Item ID List structure is described in more detail in the Shell documentation available on MSDN.) The IExplorerBrowser
provides a function named BrowseToIDList
which accepts the item ID list of a Shell folder to which the browser navigates. To navigate to a file system path, we have to convert the file system path to an item ID list. The following code shows how the Explorer browser can be navigated to a folder specified by a file path.
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);
}
The NavigateTo
function accepts the path of a folder. It first converts the folder to PIDL
using the SHParseDisplayName
function, and finally it calls the BrowseToIDList
function to make the Explorer browser navigate to that folder. Finally, it frees the allocated PIDL
by calling ILFree
.
The Explorer browser also maintains a travel log of all the folders visited. It is possible to programmatically navigate back or forward. This can be done using the BrowseToIDList
function as shown below:
void CExplorerBrowserView::OnViewBack()
{
m_spExplorerBrowser->BrowseToIDList(NULL, SBSP_NAVIGATEBACK);
}
void CExplorerBrowserView::OnViewForward()
{
m_spExplorerBrowser->BrowseToIDList(NULL, SBSP_NAVIGATEFORWARD);
}
Listening to Navigation Events
In addition to programmatic navigation, the Explorer browser also provides means to listen to navigation events which occur when the user navigates using the Explorer user interface (e.g., by double clicking on a folder). This can be done by supplying the Explorer browser object with an object implementing the IExplorerBrowserEvents
. The CExplorerBrowserEvents
class in the sample implements the IExplorerBrowserEvents
and delegates to the main view class. Here is the code for 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;
}
};
All four events fired by the Explorer browser and handled in the CExplorerBrowserEvents
class are delegated back to the hosting CExplorerBrowserView
instance. The view itself declares the event handling methods as virtual so that they can be overridden by the derived class.
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);
...
}
Let's take a look at these event methods in a little more detail. Three of these methods take the PIDL
of the folder as parameter.
- The
OnNavigationPending
method is called when the navigation is initiated by either the user or programmatically. The actual navigation process can be canceled by returning a failure HRESULT
from the method. - The
OnNavigationComplete
method is called when the navigation has completed; this method is the right place to update the UI so that it is in sync with the navigated folder. - The
OnViewCreated
method is called just after the navigation pending method. The Explorer destroys and recreates the list view window each time it navigates to a folder. This method takes a pointer to the IShellView
interface which provides additional methods to query and control the view. - Finally, the
OnNavigationFailed
method is called if the folder navigation fails for some reason.
Once we have implemented the IExplorerBrowserEvents
, we need to create an instance of the implementing class, CExplorerBrowserEvents
in this case, and supply it to the Explorer browser object. This is done by calling the IExplorerBrowser::Advise
method. This method should be called before navigating the browser to a particular folder. In the case of CExploreBrowserView
, this function is called in the OnCreate
function just after the call to Initialize
.
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();
}
}
The call to the Advise
method ensures that the CExplorerBrowserEvents
class receives a notification for each of the events. A matching call to the Unadvise
methods stops the Explorer browser from notifying events to the object. The Advise
method returns a DWORD
cookie which can be supplied to the Unadvise
method. The call to Unadvise
is in the OnDestroy
method of CView
:
void CExplorerBrowserView::OnDestroy()
{
CView::OnDestroy();
if(m_dwAdviseCookie)
{
m_spExplorerBrowser->Unadvise(m_dwAdviseCookie);
}
m_spExplorerBrowser->Destroy();
}
Obtaining the List of Selected Items
In some applications, it might be necessary to obtain the selected items in the Explorer browser and take some action on them. To obtain the selected items is a two step process:
- Obtain an interface pointer to the
IShellView
interface by calling the GetCurrenView
method.
CComPtr<IShellView> spSV;
if (SUCCEEDED(m_spExplorerBrowser->GetCurrentView(IID_PPV_ARGS(&spSV))))
- The
IShellView
pointer thus obtained can be used to get the selected items as an OLE DataObject.
CComPtr<IDataObject> spDataObject;
if (SUCCEEDED(spSV->GetItemObject(SVGIO_SELECTION,
IID_PPV_ARGS(&spDataObject))))
- Finally, the data object can be used to get the selected items as shown below:
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 );
The above code adds the path of the selected items to a CStringArray
object.
The CExploreBrowserView
class provides a method called GetSelectedItems
which accepts a CStringArray
reference; on return, the string array is filled with the path of each of the selected items.
Conclusion and Future Work
Windows Vista has lot of such nice goodies in the Shell, but unfortunately, they are not very well documented. I hope that this article and the sample will help someone to use the Explorer browser in their own applications. I selected MFC for this article because it was easy to develop the sample using MFC, but I am working on porting the application to .NET and C#. So stay tuned for future updates.
History
- 2/27/2006 - Initially posted.