Contents
Introduction
From the shell's point of view, the contents of your computer -- hard drives, CD-ROMs, mapped network drives, the desktop, and so on -- are arranged in one large tree, with the desktop as the topmost node, called the shell namespace. Explorer provides a means to insert custom objects into the namespace via namespace extensions. In this article, I'll cover the steps involved in making a basic, simple namespace extension. Our extension will create a virtual folder that lists the drives on the computer, similar to the My Computer list pictured below.
The article assumes you know C++, ATL, and COM. Familiarity with shell extensions is also helpful.
I realize this is a really long article, but namespace extensions are extremely complicated and the best documentation I could find was the comments in the RegView sample in MSDN (67K). That sample is functional, but it does nothing to explain the internal sequence of events in namespaces. Dino Esposito's great book Visual C++ Windows Shell Programming sheds a bit more light, and includes a WinView sample (download source, 1 MB) which is based on RegView. I took the information in those two sources, threw in tons of trace messages to see the logic flow, and compiled it all in this article.
The sample project included with this article is a basic extension; it does very little, yet it is fully functional. (Even a "simple" extension required all you see here in this article.) I purposely avoided some topics -- such as subfolders in a namespace, and interacting with other parts of the namespace -- since that would have only made the article longer, and the code more complicated. I may cover those topics in future articles.
Structure of Explorer
The familiar two-pane view of Explorer is actually composed of several parts, all of which are important to a namespace extension. The parts are illustrated below:
In the picture above, the items like Control Panel and Registry View are virtual folders. These do not show part of the file system, but rather are folder-like UIs that expose some sort of functionality provided by namespace extensions. An extension shows its UI in the right pane, called the shell view. An extension can also manipulate Explorer's menu, toolbar, and status bar using a COM interface that Explorer provides. Explorer manages the tree view, where it shows the namespace, and an extension's control over the tree is limited to showing subfolders.
Structure of Namespace Extensions
The internal structure of a namespace extension is, of course, dependent on the compiler and programming language you use. However, there is one important common element, the PIDL. PIDL (rhymes with "fiddle") stands for pointer to an ID list, and is the data structure Explorer uses to organize the items and sub-folders that are shown in the tree view. While the exact format of the data is up to the extension to define, there are a few rules regarding how the data is organized in memory. These rules define a generic format for PIDLs so that Explorer can deal with PIDLs from any extension, without regard for its internal structure.
I know that's rather vague, but for now, suffice it to say that PIDLs are how an extension stores data meaningful to itself. I will cover all the details of PIDLs, and how to construct them, later on in this article.
The other major part of an extension is the COM interfaces it must implement. The required interfaces are:
IShellFolder
: Provides a communication channel between Explorer and the code implementing the virtual folder.
IEnumIDList
: A COM enumerator that lets Explorer or the shell view enumerate the contents of the virtual folder.
IShellView
: Manages a window that appears in the right pane of Explorer.
More complex extensions can also implement interfaces that customize the tree view side of Explorer, however, I will not cover those interfaces in this article, since the extension presented here is purposely being kept simple.
PIDLs
What PIDLs are
Every item in Explorer's namespace, whether it's a file, directory, Control Panel applet, or an object exposed by an extension, can be uniquely specified by its PIDL. An absolute PIDL of an object is analogous to a fully-qualified path to a file; it is the object's own PIDL and the PIDLs of all its parent folders concatenated together. So for example, the absolute PIDL to the System Control Panel applet can be thought of as [Desktop]\[My Computer]\[Control Panel]\[System applet]
.
A relative PIDL is just the object's own PIDL, relative to its parent folder. Such a PIDL is only meaningful to the virtual folder that contains the object, since that folder is the only thing that can understand the data in the PIDL.
The extension in this article deals with relative PIDLs, because no communication happens with other parts of the namespace. (Doing so would require constructing absolute PIDLs.)
Structure of a PIDL
A PIDL is a structure analogous to a singly-linked list, only without pointers. A PIDL consists of a series of ITEMIDLIST
structures, placed back-to-back in a contiguous memory block. An ITEMIDLIST
only has one member, a SHITEMID
structure:
typedef struct _ITEMIDLIST
{
SHITEMID mkid;
} ITEMIDLIST;
The definition of SHITEMID
is:
typedef struct _SHITEMID
{
USHORT cb;
BYTE abID[1];
} SHITEMID;
The cb
member holds the size of the entire struct
, and functions like a "next" pointer in singly-linked lists. The abID
member is where a namespace extension stores its own private data. This member is allowed to be any length; the value of cb
indicates its exact size. So for example, if an extension stored 12 bytes of data, cb
would be 14 (12 + sizeof(USHORT)
). The data stored at abID
can be anything meaningful to the namespace, however, no two objects in a folder can have the same data, just as no two files in a directory can have the same filename.
The end of the PIDL is indicated by a SHITEMID
struct
with cb
set to 0, just as linked lists use a NULL next pointer to indicate the end of the list.
Here is a sample PIDL containing only one block of data, with a variable pPidl
pointing at the start of the list.
Notice how we can move from one SHITEMID
struct
to the next by adding each struct
's cb
value to the pointer.
Now, you may be asking what good is a SHITEMID
or a PIDL if Explorer doesn't know the data format. The answer is, Explorer views PIDLs as opaque data types that it only passes around to namespaces. They are much like handles in this regard. When you have, say an HWND
, you don't care what the internal data structure behind a window is, but you know you can do everything with a window by passing its handle back to the OS. PIDLs are the opposite - Explorer doesn't know the data underlying a PIDL, but it can interact with namespaces by passing PIDLs to them.
Our namespace's PIDL data
As mentioned above, the data used to identify an item in a namespace's folder needs to be unique within that folder. Fortunately, there is already a unique identifier for drives, the drive letter, so all we need to store in the abID
field is the letter. Our PIDL data is defined as a PIDLDATA
struct
:
struct PIDLDATA
{
TCHAR chDriveLtr;
};
Namespace Extension Interfaces
IEnumIDList
IEnumIDList
is an implementation of a COM enumerator that enumerates over a collection of PIDLs. A COM enumerator implements functions that allow sequential access to a collection, much like an iterator
in STL collections. ATL provides classes that implement the enumerator for us, so all we have to do is provide the collection of data and tell ATL how to copy PIDLs.
IEnumIDList
is used in two cases:
- The shell view needs to enumerate the contents of a folder in order to know what to display.
- Explorer needs to enumerate subfolders of a folder in order to populate the tree view.
Since our extension contains no subfolders, we will only run into case 1.
IShellFolder, IPersistFolder
IShellFolder
is the interface that Explorer uses to initialize and communicate with an extension. Explorer calls IShellFolder
methods when it's time for the extension to create its view window. IShellFolder
also has methods to enumerate the contents of an extension's virtual folder, and compare two items in the folder for sorting purposes.
IPersistFolder
has one method, Initialize()
, that is called so an extension can perform any startup initialization tasks.
IShellView, IOleCommandTarget
IShellView
is the interface through which Explorer informs an extension of UI-related events. IShellView
has methods that tell the extension to create and destroy a view window, refresh the display, and so on. IOleCommandTarget
is used by Explorer to send commands to the view, such as a refresh command when the user presses F5.
IShellBrowser
IShellBrowser
is an interface exposed by Explorer, and lets an extension manipulate the Explorer window. IShellBrowser
has methods to change the menu, toolbar, and status bar, as well as send generic messages to the controls in Explorer.
Our Implementation
PIDL manager class
To make dealing with PIDLs easier, our extension uses a helper class called CPidlMgr
that performs operations on PIDLs. I will touch on the important parts here, which are creating a PIDL, returning the data we stored in a PIDL, and returning a textual description of a PIDL. Here are the relevant parts of the class declaration:
class CPidlMgr
{
public:
LPITEMIDLIST Create ( const TCHAR );
TCHAR GetData ( LPCITEMIDLIST );
DWORD GetPidlPath ( LPCITEMIDLIST, LPTSTR );
private:
CComPtr<IMalloc> m_spMalloc;
};
Creating a new PIDL
The Create()
function takes a drive letter and creates a relative PIDL that contains that drive letter as its data. We start by calculating the memory required for the first item in the PIDL.
LPITEMIDLIST CPidlMgr::Create ( const TCHAR chDrive )
{
UINT uSize = sizeof(ITEMIDLIST) + sizeof(PIDLDATA);
Remember that one node in a PIDL is an ITEMIDLIST
struct
, which contains our PIDLDATA
struct
. Next, we use the shell's memory allocator to allocate memory for that first node, as well as a second ITEMIDLIST
which will mark the end of the PIDL.
LPITEMIDLIST pidlNew =
(LPITEMIDLIST) m_spMalloc->Alloc(uSize + sizeof(ITEMIDLIST));
Now, we have to fill in the contents of the PIDL. To set up the first node, we set the members of the SHITEMID
struct
. The cb
member is set to uSize
, the size of the first node.
if ( pidlNew )
{
LPITEMIDLIST pidlTemp = pidlNew;
pidlTemp->mkid.cb = uSize;
Then we store our PIDL data in the abID
member (the variable-length block of memory at the end of the struct
).
PIDLDATA* pData = (PIDLDATA*) pidlTemp->mkid.abID;
pData->chDriveLtr = chDrive;
Next, we advance pidlTemp
to the second node and set its members to zero to mark the end of the PIDL.
pidlTemp = GetNextItem ( pidlTemp );
pidlTemp->mkid.cb = 0;
pidlTemp->mkid.abID[0] = 0;
}
return pidlNew;
}
Getting the drive letter from a PIDL
The GetData()
function reads a PIDL and returns the drive letter stored in the PIDL.
TCHAR CPidlMgr::GetData ( LPCITEMIDLIST pidl )
{
PIDLDATA* pData;
pData = (PIDLDATA*)( pidl->mkid.abID );
return pData->chDriveLtr;
}
Getting a text description for a PIDL
The last method I'll cover here, GetPidlDescription()
, returns a textual description of a PIDL.
void CPidlMgr::GetPidlDescription ( LPCITEMIDLIST pidl, LPTSTR szDesc )
{
TCHAR chDrive = GetData ( pidl );
if ( '\0' != chDrive )
wsprintf ( szDesc, _T("Drive %c:"), chDrive );
else
*szDesc = '\0';
}
GetPidlDescription()
uses GetData()
to read the drive letter from the PIDL, then returns a string such as "Drive A:" which can be shown in the user interface.
IEnumIDList
When our extension receives a request for an enumerator, we create a collection of drive letters representing the drives to be shown in the shell view. We then use ATL's CComEnumOnSTL
class to create the enumerator.
Requirements for using CComEnumOnSTL
CComEnumOnSTL
requires four things from us:
- The interface of the enumerator being implemented, in our case
IEnumIDList
.
- The type of the data to be returned from the enumerator, in our case
LPITEMIDLIST
.
- The type of the collection holding the data.
- A copy policy class.
The collection holding the data must be an STL container such as vector
or list
. Our extension will use a vector<TCHAR>
to hold the drive letters.
ATL calls methods in the copy policy class when it needs to initialize, copy, or destroy elements. The generic form of a copy policy class is:
class CopyPolicy
{
public:
static void init ( DESTTYPE* p );
static HRESULT copy ( DESTTYPE* p1, SRCTYPE* p2 );
static void destroy ( DESTTYPE* p );
};
Here is our copy policy class:
class CCopyTcharToPidl
{
public:
static void init ( LPITEMIDLIST* p )
{
}
static HRESULT copy ( LPITEMIDLIST* pTo, const TCHAR* pFrom )
{
*pTo = m_PidlMgr.Create ( *pFrom );
return (NULL != *pTo) ? S_OK : E_OUTOFMEMORY;
}
static void destroy ( LPITEMIDLIST* p )
{
m_PidlMgr.Delete ( *p );
}
private:
static CPidlMgr m_PidlMgr;
};
This is pretty straightforward; we use CPidlMgr
to do the work of creating and deleting PIDLs. One last thing we need is a typedef
that puts all this together into one class.
typedef
CComEnumOnSTL<IEnumIDList, &IID_IEnumIDList,
LPITEMIDLIST,
CCopyTcharToPidl,
std::vector<TCHAR> >
CEnumIDListImpl;
IShellFolder
When Explorer creates our namespace extension, it first instantiates an IShellFolder
object. IShellFolder
has methods for browsing to a new virtual folder, creating a shell view window, and taking actions on the folder's contents. The important IShellFolder
methods are:
GetClassID()
- Inherited from IPersist
. Returns our object CLSID to Explorer.
Initialize()
- Inherited from IPersistFolder
. Gives us a chance to do one-time initialization.
BindToObject()
- Called when a folder in our part of the namespace is being browsed. Its job is to create a new IShellFolder
object, initialize it with the PIDL of the folder being browsed, and return that new object to the shell.
CompareIDs()
- Responsible for comparing two PIDLs and returning their relative order.
CreateViewObject()
- Called when Explorer wants us to create our shell view. It creates a new IShellView
object and returns it to Explorer.
EnumObjects()
- Creates a new PIDL enumerator that can enumerate the contents of the virtual folder.
GetAttributesOf()
- Returns attributes (such as read-only) for an item or items in the virtual folder.
GetUIObjectOf()
- Returns a COM object implementing a UI element (such as a context menu) associated with an item or items in the virtual folder.
I will cover two of the important methods here, CreateViewObject()
and EnumObjects()
.
Creating a shell view
Explorer calls CreateViewObject()
when it wants our extension to create a window in the shell view pane. The prototype for CreateViewObject()
is:
STDMETHODIMP IShellFolder::CreateViewObject (
HWND hwndOwner,
REFIID riid,
void** ppvOut );
hwndOwner
is the window in Explorer which will be the parent for our view window. riid
and ppvOut
are the IID of the interface Explorer is requesting (IID_IShellView
in our example) and an out parameter where we'll store the requested interface pointer. Our CreateViewObject()
method creates a new CShellViewImpl
COM object (our class that implements IShellView
, which I will cover later).
STDMETHODIMP CShellFolderImpl::CreateViewObject ( HWND hwndOwner,
REFIID riid, void** ppvOut )
{
HRESULT hr;
CComObject<CShellViewImpl>* pShellView;
hr = CComObject<CShellViewImpl>::CreateInstance ( &pShellView );
if ( FAILED(hr) )
return hr;
This uses CComObject
to create a new CShellViewImpl
object. Next, we call a private initialization function in CShellViewImpl
and pass it a pointer to the folder object. The view will use this pointer later in calls to EnumObjects()
and CompareIDs()
.
pShellView->AddRef();
hr = pShellView->_init ( this );
if ( FAILED(hr) )
{
pShellView->Release();
return hr;
}
Finally, we query the CShellViewImpl
object for the interface that Explorer is requesting.
hr = pShellView->QueryInterface ( riid, ppvOut );
pShellView->Release();
return hr;
}
(This method doesn't pass hwndOwner
to the view object, but the view object retrieves the parent window on its own, so this is OK.)
Enumerating objects in our virtual folder
In our simple extension, EnumObjects()
is called by the view object when it needs to know the contents of the folder it is displaying. Notice the clear separation of functionality here: the shell folder knows the contents, but has no UI code; the shell view handles the UI, but doesn't intrinsically know the contents of the folder.
The prototype for EnumObjects()
is:
STDMETHODIMP IShellFolder::EnumObjects (
HWND hwndOwner,
DWORD dwFlags,
LPENUMIDLIST* ppEnumIDList );
hwndOwner
is a window that can be used as the parent window of any dialogs or message boxes that the method might need to display. dwFlags
is used to tell the method what type of objects to return in the enumerator (for example, only subfolders or only non-folders). Our extension has no subfolders, so we have no need to check the flags. ppEnumIDList
is an out parameter in which we store an IEnumIDList
interface to the enumerator object that the method creates.
Our EnumObjects()
method creates a new CEnumIDListImpl
object, and fills in a vector<TCHAR>
with the drive letters on the system. The enumerator object uses the vector
and our copy policy class (as described earlier in the "Requirements for using "#RequirementsforusingCComEnumOnSTL">CComEnumOnSTL
" section) to return PIDLs.
Here's the beginning of our EnumObjects()
. We first fill in the vector
(which is a member, m_vecDriveLtrs
).
STDMETHODIMP CShellFolderImpl::EnumObjects ( HWND hwndOwner,
DWORD dwFlags, LPENUMIDLIST* ppEnumIDList )
{
HRESULT hr;
DWORD dwDrives;
int i;
m_vecDriveLtrs.clear();
for ( i = 0, dwDrives = GetLogicalDrives(); i <= 25; i++ )
if ( dwDrives & (1 << i) )
m_vecDriveLtrs.push_back ( 'A' + i );
Next, we create a CEnumIDListImpl
object.
CComObject<CEnumIDListImpl>* pEnum;
hr = CComObject<CEnumIDListImpl>::CreateInstance ( &pEnum );
if ( FAILED(hr) )
return hr;
pEnum->AddRef();
Next, we initialize the enumerator, passing it the folder's IUnknown
interface and a reference to the vector
. CComEnumOnSTL
calls AddRef
on the IUnknown
to ensure that the folder COM object remains in memory while the enumerator is using it.
hr = pEnum->Init ( GetUnknown(), m_vecDriveLtrs );
Finally, we return an IEnumIDList
interface to the caller.
if ( SUCCEEDED(hr) )
hr = pEnum->QueryInterface ( IID_IEnumIDList, (void**) ppEnumIDList );
pEnum->Release();
return hr;
}
IShellView
Our IShellView
implementation creates a list control in report mode (the most common way for namespace extensions to show data, since it follows what Explorer itself does). The class CShellViewImpl
also derives from ATL's CWindowImpl
class, meaning CShellViewImpl
is a window and has a message map. CShellViewImpl
creates its own window, then creates the list control as a child. That way, CShellViewImpl
's message map receives notification messages from the list control. CShellViewImpl
also derives from IOleCommandTarget
so it can receive commands from Explorer.
The important IShellView
methods are:
GetWindow()
- Inherited from IOleWindow
. Returns our shell view's window handle.
CreateViewWindow()
- Creates a new shell view window.
DestroyViewWindow()
- Destroys the shell view window, and lets us do any cleanup tasks.
GetCurrentInfo()
- Returns our view's current view settings in a FOLDERSETTINGS
struct
. FOLDERSETTINGS
is described below.
Refresh()
- Called when we must refresh the contents of the shell view.
UIActivate()
- Called when our view gains or loses focus. This method is when the view can modify Explorer's UI to add custom commands.
I will cover CreateViewWindow()
and UIActivate()
in detail here, since that's where most of the UI action happens.
CShellViewImpl class listing
Managing the UI requires saving a lot of state information, so I've listed that data here along with the class declaration:
class ATL_NO_VTABLE CShellViewImpl :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CShellViewImpl, &CLSID_ShellViewImpl>,
public IShellView,
public IOleCommandTarget,
public CWindowImpl<CShellViewImpl>
{
public:
DECLARE_NO_REGISTRY()
DECLARE_WND_CLASS(NULL)
BEGIN_COM_MAP(CShellViewImpl)
COM_INTERFACE_ENTRY(IShellView)
COM_INTERFACE_ENTRY(IOleWindow)
COM_INTERFACE_ENTRY(IOleCommandTarget)
END_COM_MAP()
BEGIN_MSG_MAP(CShellViewImpl)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
MESSAGE_HANDLER(WM_SIZE, OnSize)
END_MSG_MAP()
Pretty standard stuff so far. Notice the DECLARE_NO_REGISTRY()
macro - this tells ATL that this COM object does not require registration, and has no corresponding .RGS file. Skipping to the private data, we first have some variables holding various UI states:
private:
CPidlMgr m_PidlMgr;
UINT m_uUIState;
int m_nSortedColumn;
bool m_bForwardSort;
FOLDERSETTINGS m_FolderSettings;
m_uUIState
holds a constant from the following list:
SVUIA_ACTIVATE_FOCUS
- Our view window has the focus.
SVUIA_ACTIVATE_NOFOCUS
- Our view window is visible in Explorer, but some other window (the tree view or address bar) currently has the focus.
SVUIA_DEACTIVATE
- Our view window is about to lose focus and be hidden or destroyed (for example, a different folder was just selected in the tree view).
This member is used when we add or remove our own commands from Explorer's menu. Next are m_nSortedColumn
and m_bForwardSort
, which describe how the list control's contents are currently being sorted. Finally, there's m_FolderSettings
, which Explorer passes to us. It contains various flags regarding the suggested appearance of the view window.
Window and UI object handles are next:
HWND m_hwndParent;
HMENU m_hMenu;
CContainedWindowT<ATLControls::CListViewCtrl> m_wndList;
m_hwndParent
is a window in Explorer that we use as the parent of our own window. m_hMenu
is a handle to a menu that is shared between Explorer and our extension. Finally, m_wndList
is a list control wrapper from atlcontrols.h (included in the source zip file) that we use to manage our list control.
Next are a couple of interface pointers:
CShellFolderImpl* m_psfContainingFolder;
CComPtr<IShellBrowser> m_spShellBrowser;
m_psfContainingFolder
is an interface on the CShellFolderImpl
object that created the view. m_spShellBrowser
is an IShellBrowser
interface pointer that Explorer passes to the view that lets it manipulate the Explorer window (for example, modify the menu).
Finally, some member functions. FillList()
populates the list control. CompareItems()
is a callback used when sorting the list's contents. HandleActivate()
and HandleDeactivate()
are helper functions that modify Explorer's menu so that our custom commands appear in the menu.
void FillList();
static int CALLBACK CompareItems ( LPARAM l1, LPARAM l2, LPARAM lData );
void HandleActivate(UINT uState);
void HandleDeactivate();
};
How the view is created
This is the sequence of events that occur when our shell view gets created:
CShellFolderImpl::CreateViewObject()
creates a CShellViewImpl
and calls _init()
(this is how m_psfContainingFolder
is set).
- Explorer calls
CShellViewImpl::CreateViewWindow()
.
CShellViewImpl::CreateViewWindow()
creates a container window.
CShellViewImpl::OnCreate()
handles WM_CREATE
sent during the previous step and creates the list control as a child of the container window.
CreateViewWindow()
CreateViewWindow()
is responsible for creating a shell view window and returning its handle to Explorer. The prototype is:
STDMETHODIMP IShellView::CreateViewWindow (
LPSHELLVIEW pPrevView,
LPCFOLDERSETTINGS lpfs,
LPSHELLBROWSER psb,
LPRECT prcView,
HWND* phWnd );
pPrevView
is a pointer to a previous shell view that is being replaced, if there is one. Our extension doesn't use this. lpfs
points to a FOLDERSETTINGS
struct
, which I described in the previous section. psb
is an IShellBrowser
interface provided by Explorer. We use this to modify the Explorer UI. prcView
points to a RECT
which holds the coordinates our container window should occupy. Finally, phWnd
is an out parameter where we'll return the container's window handle.
Our CreateViewWindow()
first initializes some member data:
STDMETHODIMP CShellViewImpl::CreateViewWindow (
LPSHELLVIEW pPrevView,
LPCFOLDERSETTINGS lpfs,
LPSHELLBROWSER psb,
LPRECT prcView,
HWND* phWnd )
{
m_spShellBrowser = psb;
m_FolderSettings = *lpfs;
m_spShellBrowser->GetWindow( &m_hwndParent );
Then we create our container window (remember that CShellViewImpl
inherits from CWindowImpl
):
if ( NULL == Create ( m_hwndParent, *prcView ) )
return E_FAIL;
*phWnd = m_hWnd;
return S_OK;
}
OnCreate()
The CWindowImpl::Create()
call above generates a WM_CREATE
message, which CShellViewImpl
's message map routes to CShellViewImpl::OnCreate()
. OnCreate()
creates a list control and attaches m_wndList
to it.
LRESULT CShellViewImpl::OnCreate ( UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled )
{
HWND hwndList;
DWORD dwListStyles = WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER |
LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_SHAREIMAGELISTS;
DWORD dwListExStyles = WS_EX_CLIENTEDGE;
DWORD dwListExtendedStyles = LVS_EX_FULLROWSELECT | LVS_EX_HEADERDRAGDROP;
switch ( m_FolderSettings.ViewMode )
{
case FVM_ICON: dwListStyles |= LVS_ICON; break;
case FVM_SMALLICON: dwListStyles |= LVS_SMALLICON; break;
case FVM_LIST: dwListStyles |= LVS_LIST; break;
case FVM_DETAILS: dwListStyles |= LVS_REPORT; break;
DEFAULT_UNREACHABLE;
}
This sets up the list control's window styles. Next, we create the list control and attach m_wndList
.
hwndList = CreateWindowEx ( dwListExStyles, WC_LISTVIEW, NULL, dwListStyles,
0, 0, 0, 0, m_hWnd, (HMENU) sm_uListID,
_Module.GetModuleInstance(), 0 );
if ( NULL == hwndList )
return -1;
m_wndList.Attach ( hwndList );
FillList();
return 0;
}
Filling in the list control
CShellViewImpl::FillList()
is responsible for populating the list control. It first calls the EnumObjects()
method of its containing shell folder to get an enumerator for the contents of the folder.
void CShellViewImpl::FillList()
{
CComPtr<IEnumIDList> pEnum;
LPITEMIDLIST pidl = NULL;
HRESULT hr;
hr = m_psfContainingFolder->EnumObjects ( m_hWnd, SHCONTF_NONFOLDERS, &pEnum );
if ( FAILED(hr) )
return;
We then begin enumerating the folder's contents, and add a list item for each drive. We make a copy of each PIDL and store it in each list item's data area for later use.
DWORD dwFetched;
while ( pEnum->Next(1, &pidl, &dwFetched) == S_OK )
{
LVITEM lvi = {0};
TCHAR szText[MAX_PATH];
lvi.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM;
lvi.iItem = m_wndList.GetItemCount();
lvi.iImage = 0;
TCHAR chDrive = m_PidlMgr.GetData ( pidl );
lvi.lParam = (LPARAM) m_PidlMgr.Create ( chDrive );
As for the item's text, we use CPidlMgr::GetPidlDescription()
to get a string.
m_PidlMgr.GetPidlDescription ( pidl, szText );
lvi.pszText = szText;
m_wndList.InsertItem ( &lvi );
I've omitted the code to fill in the other columns, since it's just straightforward list control calls. Finally, we sort the list by the first column. CListSortInfo
is a struct
that holds info needed by the CompareItems()
callback. The second member (SIMPNS_SORT_DRIVELETTER
) indicates which column to sort by.
CListSortInfo sort = { m_psfContainingFolder, SIMPNS_SORT_DRIVELETTER, true };
m_wndList.SortItems ( CompareItems, (LPARAM) &sort );
}
Here's what the resulting list looks like:
Handling window activation and deactivation
Explorer calls CShellViewImpl::UIActivate()
to inform us when our window is gaining or losing focus. When these events occur, we can add or remove commands to Explorer's menu and toolbar. In this section, I'll cover how we handle the activation messages; the next section will cover modifying the UI.
UIActivate()
is rather simple, it compares the new state with the last-saved state, and then delegates the call to the HandleActive()
helper.
STDMETHODIMP CShellViewImpl::UIActivate ( UINT uState )
{
if ( m_uUIState == uState )
return S_OK;
HandleActivate ( uState );
return S_OK;
}
HandleActivate()
will be covered in the next section. There are a couple of tricky situations dealing with window focus. Our container window has the WS_TABSTOP
style, meaning the user can TAB to the window. Since the container window itself has no UI, it just sets the focus to the list control:
LRESULT CShellViewImpl::OnSetFocus ( UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled )
{
m_wndList.SetFocus();
return 0;
}
The other tricky case is when the user clicks on the list control directly to give it the focus. Normally, Explorer keeps track of which window has the focus. Since the list is not owned or managed by Explorer, it isn't notified when the list directly receives the focus. As a result, Explorer loses track of the focused window. When we receive a NM_SETFOCUS
message from the list, indicating that it received the focus, we call IShellBrowser::OnViewWindowActivate()
to tell Explorer that our view window now has the focus.
LRESULT CShellViewImpl::OnListSetfocus ( int idCtrl,
LPNMHDR pnmh, BOOL& bHandled )
{
m_spShellBrowser->OnViewWindowActive ( this );
HandleActivate ( SVUIA_ACTIVATE_FOCUS );
return 0;
}
Modifying Explorer's menu
Namespace extensions can change Explorer's menu and toolbar to add their own commands. During development, I was unable to reliably modify the toolbar, so the sample extension only modifies the menu. Our extension uses two helper functions when modifying the menu, HandleActivate()
to do the modifications, and HandleDeactivate()
to remove them. We have two different menus, one if the list control has the focus, and another one if not. The two are pictured here:
This popup menu is inserted right before Explorer's Help menu. The Explore Drive item opens another Explorer window on the selected drive. The System Properties item runs the System Control Panel applet. We also add an item to the Help menu that shows our own About box.
HandleActivate()
takes one parameter, the UI state that Explorer is about to enter. The first thing it does is call HandleDeactivate()
to undo the previous menu modifications and destroy the old menu.
void CShellViewImpl::HandleActivate ( UINT uState )
{
HandleDeactivate();
I will cover HandleDeactivate()
shortly. Next, if our window is being activated, we can start modifying the menu. We first create a new, empty menu.
if ( SVUIA_DEACTIVATE != uState )
{
ATLASSERT(NULL == m_hMenu);
m_hMenu = CreateMenu();
The next step is to call IShellBrowser::InsertMenusSB()
, which lets Explorer put its menu items in the newly-created menu. InsertMenusSB()
takes its logic from OLE containers, which also have shared menus. Our extension creates an OLEMENUGROUPWIDTHS
struct
and passes that, along with the menu handle, to InsertMenusSB()
. That struct
has an array of six LONG
s, representing six "groups" within the menu. The container (in this case, Explorer) uses groups 0, 2, and 4; while the contained object (our extension) uses groups 1, 3, and 5. Explorer fills in indexes 0, 2, and 4 of the array with the number of top-level menu items it put in each group. A normal situation has the array returning as {2, 0, 3, 0, 1, 0} representing two menus in the first group (File, Edit), three in the third group (View, Favorites, Tools), and one in the fifth group (Help). Our extension can use those numbers to calculate where the standard menus are, and where it can insert its own top-level menu items.
Now, luckily for us, Explorer isn't a generic OLE container. Its standard menus are always the same, and there are some predefined constants we can use to access the standard menus and avoid doing error-prone calculations with group widths. They are defined in shlobj.h as FCIDM_*
, for example, FCIDM_MENU_EDIT
for the position of the standard Edit menu. Our extension uses the FCIDM_MENU_HELP
to locate the standard Help menu, and inserts the popup menu pictured above right before Help.
Here is the code that sets up the shared menu, and adds a popup before Help.
if ( NULL != m_hMenu )
{
OLEMENUGROUPWIDTHS omw = { 0, 0, 0, 0, 0, 0 };
m_spShellBrowser->InsertMenusSB ( m_hMenu, &omw );
HMENU hmenuSimpleNS;
hmenuSimpleNS = LoadMenu ( ... );
if ( NULL != hmenuSimpleNS )
{
InsertMenu ( m_hMenu, FCIDM_MENU_HELP,
MF_BYCOMMAND | MF_POPUP,
(UINT_PTR) GetSubMenu ( hmenuSimpleNS, 0 ),
_T("&SimpleNSExt") );
}
Next, we add our About box item. We first get the handle to the Help menu using GetMenuItemInfo()
, then insert a new menu item.
MENUITEMINFO mii = { sizeof(MENUITEMINFO), MIIM_SUBMENU };
if ( GetMenuItemInfo ( m_hMenu, FCIDM_MENU_HELP, FALSE, &mii ))
{
InsertMenu ( mii.hSubMenu, -1, MF_BYPOSITION,
IDC_ABOUT_SIMPLENS, _T("About &SimpleNSExt") );
}
One last thing we do is remove the standard Edit menu if our view window has the focus. The standard Edit menu is empty in this case, so there's no use in leaving it there.
if ( SVUIA_ACTIVATE_FOCUS == uState )
{
DeleteMenu ( m_hMenu, FCIDM_MENU_EDIT, MF_BYCOMMAND );
}
Finally, we call IShellBrowser::SetMenuSB()
to have Explorer use the menu. We then save the new UI state and return.
m_spShellBrowser->SetMenuSB ( m_hMenu, NULL, m_hWnd );
}
}
m_uUIState = uState;
}
HandleDeactivate()
is much simpler. It calls SetMenuSB()
and RemoveMenusSB()
to remove our menu from Explorer's frame, then destroys the menu.
void CShellViewImpl::HandleDeactivate()
{
if ( SVUIA_DEACTIVATE != m_uUIState )
{
if ( NULL != m_hMenu )
{
m_spShellBrowser->SetMenuSB ( NULL, NULL, NULL );
m_spShellBrowser->RemoveMenusSB ( m_hMenu );
DestroyMenu ( m_hMenu );
m_hMenu = NULL;
}
m_uUIState = SVUIA_DEACTIVATE;
}
}
One important thing to check is that your menu item IDs fall within FCIDM_SHVIEWFIRST
and FCIDM_SHVIEWLAST
(defined in shlobj.h as 0 and 0x7FFF respectively), otherwise Explorer will not properly route messages to our extension.
Handling messages
Our view window handles several standard and list control notification messages. They are:
WM_CREATE
: Sent when our view window is first created.
WM_SIZE
: Sent when the view is resized. The handler resizes the list control to match.
WM_SETFOCUS
, NM_SETFOCUS
: Described earlier.
WM_CONTEXTMENU
: Handles a right-click in the list control, and shows a context menu if a list item was clicked.
WM_INITMENUPOPUP
: Sent when a menu is first clicked on, and disables the Explore Drive item if no drive is selected.
WM_MENUSELECT
: Sent when a new menu item is selected, and shows a flyby help string in Explorer's status bar.
WM_COMMAND
: Sent when one of our menu items is selected.
LVN_DELETEITEM
: Sent when a list item is being removed. The handler deletes the PIDL stored with each item.
HDN_ITEMCLICK
: Sent when a list header is clicked, and re-sorts the list by that column.
I will cover some of the more interesting handlers here, the ones for WM_MENUSELECT
, HDN_ITEMCLICK
, and WM_COMMAND
.
WM_MENUSELECT
Our window receives WM_MENUSELECT
when the selected menu item changes. Our handler verifies that the selected item matches one of our menu IDs, and if so, shows a help string in Explorer's status bar.
LRESULT CShellViewImpl::OnMenuSelect(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled)
{
WORD wMenuID = LOWORD(wParam);
WORD wFlags = HIWORD(wParam);
if ( !(wFlags & MF_POPUP) )
{
switch ( wMenuID )
{
case IDC_EXPLORE_DRIVE:
case IDC_SYS_PROPERTIES:
case IDC_ABOUT_SIMPLENS:
{
CComBSTR bsHelpText;
if ( bsHelpText.LoadString ( wMenuID ))
m_spShellBrowser->SetStatusTextSB ( bsHelpText.m_str );
return 0;
}
break;
}
}
return DefWindowProc();
}
We use IShellBrowser::SetStatusTextSB()
to change the status bar text.
HDN_ITEMCLICK
HDN_ITEMCLICK
is sent when the user clicks a column header. We first check the current sorted column. If the same column was clicked, m_bForwardSort
is toggled to reverse the sort direction. Otherwise, the new column is saved as the current sorted column.
LRESULT CShellViewImpl::OnHeaderItemclick ( int idCtrl,
LPNMHDR pnmh, BOOL& bHandled )
{
NMHEADER* pNMH = (NMHEADER*) pnmh;
int nClickedItem = pNMH->iItem;
if ( nClickedItem == m_nSortedColumn )
m_bForwardSort = !m_bForwardSort;
else
m_bForwardSort = true;
m_nSortedColumn = nClickedItem;
Next, we set up a CListSortInfo
data packet which holds a pointer to the view's containing shell folder (which is the object that knows how to sort PIDLs), the column to sort, and the direction. We then call the list control method SortItems
(which boils down to a LVM_SORTITEMS
message).
const ESortedField aFields[] =
{ SIMPNS_SORT_DRIVELETTER, SIMPNS_SORT_VOLUMENAME,
SIMPNS_SORT_FREESPACE, SIMPNS_SORT_TOTALSPACE };
CListSortInfo sort = { m_psfContainingFolder,
aFields[m_nSortedColumn], m_bForwardSort };
m_wndList.SortItems ( CompareItems, (LPARAM) &sort );
return 0;
}
To show how the sorting works, here is CShellViewImpl::CompareItems()
:
int CALLBACK CShellViewImpl::CompareItems ( LPARAM l1,
LPARAM l2, LPARAM lData )
{
CListSortInfo* pSort = (CListSortInfo*) lData;
return (int) pSort->pShellFolder->CompareIDs ( lData,
(LPITEMIDLIST) l1, (LPITEMIDLIST) l2 );
}
This just calls through to CShellFolderImpl::CompareIDs()
. The parameters are the LPARAM
data values for the two items being compared (l1
and l2
), plus the second parameter to SortItems()
(lData
) which is the CListSortInfo
struct
we set up in OnHeaderItemclick()
.
Here is CompareIDs()
. It takes the same three parameters as CompareItems()
, just in a different order. The return value is like strcmp()
(-1, 0, or 1 indicating the order of the PIDLs). We first use CPidlMgr::GetData()
to retrieve the two drive letters from the PIDLs.
STDMETHODIMP CShellFolderImpl::CompareIDs ( LPARAM lParam,
LPCITEMIDLIST pidl1, LPCITEMIDLIST pidl2 )
{
TCHAR chDrive1 = m_PidlMgr.GetData ( pidl1 );
TCHAR chDrive2 = m_PidlMgr.GetData ( pidl2 );
CListSortInfo* pSortInfo = (CListSortInfo*) lParam;
HRESULT hrRet;
Next, we check the field to sort by. I'll show sorting by drive letter here.
switch ( pSortInfo->nSortedField )
{
case SIMPNS_SORT_DRIVELETTER:
{
if ( chDrive1 == chDrive2 )
hrRet = 0;
else if ( chDrive1 < chDrive2 )
hrRet = -1;
else
hrRet = 1;
}
break;
...
}
The other cases are similar; they just get different information (volume name, free space, etc.) and set hrRet
based on that. The last step is to check the sort order and reverse it if necessary.
if ( !pSortInfo->bForwardSort )
hrRet *= -1;
return hrRet;
}
WM_COMMAND
CShellViewImpl
's message map has a COMMAND_ID_HANDLER
entry for each of our menu commands:
BEGIN_MSG_MAP(CShellViewImpl)
...
COMMAND_ID_HANDLER(IDC_SYS_PROPERTIES, OnSystemProperties)
COMMAND_ID_HANDLER(IDC_EXPLORE_DRIVE, OnExploreDrive)
COMMAND_ID_HANDLER(IDC_ABOUT_SIMPLENS, OnAbout)
END_MSG_MAP()
Here is the code for OnExploreDrive()
. We begin by getting the selected item, then retrieving its LPARAM
data which is the corresponding PIDL.
LRESULT CShellViewImpl::OnExploreDrive(WORD wNotifyCode,
WORD wID, HWND hWndCtl, BOOL& bHandled)
{
LPCITEMIDLIST pidlSelected;
int nSelItem;
TCHAR chDrive;
TCHAR szPath[] = _T("?:\\");
nSelItem = m_wndList.GetNextItem ( -1, LVIS_SELECTED );
pidlSelected = (LPCITEMIDLIST) m_wndList.GetItemData ( nSelItem );
chDrive = m_PidlMgr.GetData ( pidlSelected );
We then fill in the drive letter in szPath
, and call ShellExecute()
to explore that drive.
*szPath = chDrive;
ShellExecute ( NULL, _T("explore"), szPath, NULL, NULL, SW_SHOWNORMAL );
return 0;
}
IOleCommandTarget
One additional way Explorer communicates with our extension is the IOleCommandTarget
interface. This has two methods:
QueryStatus()
: Called by Explorer to determine which standard commands our extension supports.
Exec()
: Called when the user executes a command in Explorer that we have to deal with.
There is little documentation regarding the commands, what they are used for, or even what their IDs are. The only meaningful command I could see is Refresh, which is sent when the user presses F5 or clicks Refresh on the View menu. In the following sections, I demonstrate a minimal implementation of the two methods that handle the Refresh command. The actual code in the sample project contains trace messages so you can see what commands are being queried for and sent.
QueryStatus()
The parameters to QueryStatus()
are a command group and one or more commands. If QueryStatus()
returns S_OK
, it means our extension supports the commands, and Explorer can then call Exec()
to have us respond to the commands. There are three groups that I saw being used during my testing: NULL
, CGID_Explorer
, and CGID_ShellDocView
. The Refresh command is in the NULL
group, and has ID OLECMDID_REFRESH
. Our QueryStatus()
just looks through the commands, and if it finds OLECMDID_REFRESH
, it sets flags in the OLECMD
struct
and returns S_OK
. Otherwise, it returns an error code to indicate that we don't support the command.
STDMETHODIMP CShellViewImpl::QueryStatus ( const GUID* pguidCmdGroup,
ULONG cCmds, OLECMD prgCmds[], OLECMDTEXT* pCmdText )
{
if ( NULL == pguidCmdGroup )
{
for ( UINT u = 0; u < cCmds; u++ )
{
switch ( prgCmds[u].cmdID )
{
case OLECMDID_REFRESH:
prgCmds[u].cmdf = OLECMDF_SUPPORTED | OLECMDF_ENABLED;
break;
}
}
return S_OK;
}
return OLECMDERR_E_UNKNOWNGROUP;
}
Exec()
Our Exec()
method again checks for the NULL
command group and Refresh command ID, and if the parameters match those values, calls Refresh()
to repopulate the list control.
STDMETHODIMP CShellViewImpl::Exec ( const GUID* pguidCmdGroup, DWORD nCmdID,
DWORD nCmdExecOpt, VARIANTARG* pvaIn,
VARIANTARG* pvaOut )
{
HRESULT hrRet = OLECMDERR_E_UNKNOWNGROUP;
if ( NULL == pguidCmdGroup )
{
if ( OLECMDID_REFRESH == nCmdID )
{
Refresh();
hrRet = S_OK;
}
}
return hrRet;
}
Registering the Extension
There are two parts to the registration: the normal COM server stuff, and an entry that tells Explorer to use our extension. The default value of the GUID key (the GUID is the one for CShellFolderImpl
, since that's the coclass that the shell instantiates directly) is the text to use for the extension's item. The InfoTip
value holds text to show in the info tip when the mouse hovers over the extension's item. The DefaultIcon
key specifies the location of the icon to use for the item. The Attributes
value holds a combination of SFGAO_*
flags (defined in shlobj.h). At the very least, it must be 671088640 (0x28000000) which is SFGAO_FOLDER|SFGAO_BROWSABLE
. Our extension also includes SFGAO_CANRENAME|SFGAO_CANDELETE
for a grand total of 671088688 (0x28000030). Adding those flags lets the user rename or delete the namespace item using the Explorer context menu or the keyboard. (If you don't include SFGAO_DELETE
, the user must manually edit the registry to remove the extension.)
HKCR
{
NoRemove CLSID
{
ForceRemove {4145E10E-36DB-4F2C-9062-5DE1AF40BB31} = s 'Simple NSExt'
{
InprocServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Apartment'
}
val InfoTip =
s 'A simple sample namespace extension from CodeProject'
DefaultIcon = s '%MODULE%,0'
ShellFolder
{
val Attributes = d '671088688'
}
}
}
}
Here's the namespace extension item with its infotip:
The other part of the RGS file creates a junction point, which is how we tell Explorer to use our extension and where it should appear in the namespace. This is similar to shell extensions, which use a ShellEx key for the same purpose.
HKLM
{
NoRemove Software
{
NoRemove Microsoft
{
NoRemove Windows
{
NoRemove CurrentVersion
{
NoRemove Explorer
{
NoRemove Desktop
{
NoRemove NameSpace
{
ForceRemove {4145E10E-36DB-4F2C-9062-5DE1AF40BB31}
{
val 'Removal Message' =
s 'Your custom "Don''t delete me!" text goes here.'
}
}
}
}
}
}
}
}
}
You can change the Desktop key to change where the namespace extension appears; My Computer is a common one, which makes the extension appear at the same level as your drives and Control Panel. The GUID is again the GUID of CShellFolderImpl
. The Removal Message
string is displayed if the user has delete confirmation enabled and tries to delete the extension's item:
Conclusion
Yes, there is a ton of stuff to do when writing a namespace extension! And this article has only covered the basics. I already have ideas for future articles; part 2 will cover making an extension with subfolders, and handling events in Explorer's tree view.