Click here to Skip to main content
15,901,373 members
Articles / Desktop Programming / ATL

The Complete Idiot's Guide to Writing Namespace Extensions - Part I

Rate me:
Please Sign up or sign in to vote.
4.96/5 (56 votes)
9 Dec 200127 min read 1.6M   9.3K   256   207
A detailed tutorial on writing your own Explorer namespace extensions.



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:

 [Parts of Explorer - 32K]

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.


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;

The definition of SHITEMID is:

typedef struct _SHITEMID
    USHORT cb;       // Size of the ID (including cb itself)
    BYTE   abID[1];  // The item ID (variable length)

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.

 [Simple PIDL - 2K]

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:

    TCHAR chDriveLtr;

Namespace Extension Interfaces


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:

  1. The shell view needs to enumerate the contents of a folder in order to know what to display.
  2. 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 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  
   // Create a relative PIDL that stores a drive letter.
   LPITEMIDLIST Create ( const TCHAR );
   // Get the drive letter from a PIDL.
   // Create a text description of a PIDL.
   // The shell's memory allocator.
   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) 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.

       // GetNextItem() is a CPidlMgr helper function.
       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 )
    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 );
        *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.


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:

  1. The interface of the enumerator being implemented, in our case IEnumIDList.
  2. The type of the data to be returned from the enumerator, in our case LPITEMIDLIST.
  3. The type of the collection holding the data.
  4. 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:

// SRCTYPE is the type of the objects in the collection.
// DESTTYPE is the type being returned from the enumerator.
class CopyPolicy
    // initialize an object before copying into it
    static void init ( DESTTYPE* p );
    // copy an element
    static HRESULT copy ( DESTTYPE* p1, SRCTYPE* p2 );
    // destroy an element
    static void destroy ( DESTTYPE* p );

Here is our copy policy class:

class CCopyTcharToPidl  
    static void init ( LPITEMIDLIST* p ) 
        // No init needed.
    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 ); 
    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.

  // name and IID of enumerator interface
  CComEnumOnSTL<IEnumIDList, &IID_IEnumIDList,
  // type of object to return
  // copy policy class
  // type of collection holding the data
  std::vector<TCHAR> >


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 )
CComObject<CShellViewImpl>* pShellView;
    // Create a new CShellViewImpl COM object.
    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().

// AddRef() the object while we're using it.

// Object initialization - pass the object its containing folder (this).
hr = pShellView->_init ( this );

if ( FAILED(hr) )
    return hr;

Finally, we query the CShellViewImpl object for the interface that Explorer is requesting.

    // Return the requested interface back to the shell.
    hr = pShellView->QueryInterface ( riid, ppvOut );
    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 <A href="#RequirementsforusingCComEnumOnSTL">CComEnumOnSTL</A>" 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 )
DWORD   dwDrives;
int     i;
    // Enumerate all drives on the system
    // and put the letters of the drives into a vector.
    for ( i = 0, dwDrives = GetLogicalDrives(); i <= 25; i++ )
         if ( dwDrives & (1 << i) )
             m_vecDriveLtrs.push_back ( 'A' + i );

Next, we create a CEnumIDListImpl object.

    // Create an enumerator with CComEnumOnSTL<> and our copy policy class.
CComObject<CEnumIDListImpl>* pEnum;
    hr = CComObject<CEnumIDListImpl>::CreateInstance ( &pEnum );
    if ( FAILED(hr) )
        return hr;
    // AddRef() the object while we're using it.

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.

    // Return an IEnumIDList interface to the caller.
    if ( SUCCEEDED(hr) )
      hr = pEnum->QueryInterface ( IID_IEnumIDList, (void**) ppEnumIDList );
    return hr;


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>
    // ...

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:

    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:

  1. CShellFolderImpl::CreateViewObject() creates a CShellViewImpl and calls _init() (this is how m_psfContainingFolder is set).
  2. Explorer calls CShellViewImpl::CreateViewWindow().
  3. CShellViewImpl::CreateViewWindow() creates a container window.
  4. CShellViewImpl::OnCreate() handles WM_CREATE sent during the previous step and creates the list control as a child of the container window.

CreateViewWindow() is responsible for creating a shell view window and returning its handle to Explorer. The prototype is:

STDMETHODIMP IShellView::CreateViewWindow (
    LPSHELLVIEW pPrevView, 
    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,
    LPRECT prcView,
    HWND* phWnd )
    // Init member variables.
    m_spShellBrowser = psb;
    m_FolderSettings = *lpfs;
    // Get the parent window from Explorer.
    m_spShellBrowser->GetWindow( &m_hwndParent );

Then we create our container window (remember that CShellViewImpl inherits from CWindowImpl):

    // Create a container window, which will be the parent of the list control.
    if ( NULL == Create ( m_hwndParent, *prcView ) )
       return E_FAIL;
    // Return our window handle to the browser.
    *phWnd = m_hWnd;
    return S_OK;

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;
    // Set the list view's display style (large/small/list/report) based on
    // the FOLDERSETTINGS we were given in CreateViewWindow().
    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;

This sets up the list control's window styles. Next, we create the list control and attach m_wndList.

    // Create the list control.  Note that m_hWnd (inherited from CWindowImpl)
    // has already been set to the container window's handle.
    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 );
    // omitted - set up columns & image lists.
    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;
    // Get an enumerator object for the folder's contents.  Since this simple
    // extension doesn't deal with subfolders, we request only non-folder
    // objects.
    hr = m_psfContainingFolder->EnumObjects ( m_hWnd, SHCONTF_NONFOLDERS, &pEnum );
    if ( FAILED(hr) )

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;
        // Store a PIDL for the drive letter,
        // using the lParam member for each item
        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.

// Column 1: Drive letter
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.

    // Sort the items by drive letter initially.
CListSortInfo sort = { m_psfContainingFolder, SIMPNS_SORT_DRIVELETTER, true };
    m_wndList.SortItems ( CompareItems, (LPARAM) &sort );

Here's what the resulting list looks like:

 [Extension drive list - 31K]

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 )
    // Nothing to do if the state hasn't changed since the last call.
    if ( m_uUIState == uState )
        return S_OK;
    // Modify the Explorer menu and status bar.
    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 )
    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 )
    // Tell the browser that we have the focus.
    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:

 [Extension menus - 4K]

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 )
    // Undo our previous changes to the menu.

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 we are being activated, add our stuff to Explorer's menu.
if ( SVUIA_DEACTIVATE != uState )
    // First, create a new menu.
    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 LONGs, 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 )
    // Let the browser insert its standard items first.
    OLEMENUGROUPWIDTHS omw = { 0, 0, 0, 0, 0, 0 };

    m_spShellBrowser->InsertMenusSB ( m_hMenu, &omw );

    // Insert our SimpleExt menu before the Explorer Help menu.
    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.


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.

    // The Edit menu created by Explorer
    // is empty, so we can nuke it.
    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.

            // Set the new menu.
            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 ); // also destroys the SimpleNSExt submenu
            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.


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 the selected menu item is one of ours, show a flyby help string
    // in the Explorer status bar.
    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;
    // Otherwise, pass the message to the default handler.
    return DefWindowProc();

We use IShellBrowser::SetStatusTextSB() to change the status bar text.


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 )
int nClickedItem = pNMH->iItem;
    // Set the sorted column to the column that was just clicked.  If we're
    // already sorting on that column, reverse the sort order.
    if ( nClickedItem == m_nSortedColumn )
        m_bForwardSort = !m_bForwardSort;
        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).

    // Set up a CListSortInfo for the sort function to use.
const ESortedField aFields[] = 
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;

Next, we check the field to sort by. I'll show sorting by drive letter here.

switch ( pSortInfo->nSortedField )
        // Sort alphabetically by drive letter.
        if ( chDrive1 == chDrive2 )
            hrRet = 0;
        else if ( chDrive1 < chDrive2 )
            hrRet = -1;
            hrRet = 1;

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 the sort order is reversed (z->a or highest->lowest),
    // negate the return value.
    if ( !pSortInfo->bForwardSort )
        hrRet *= -1;
    return hrRet;

CShellViewImpl's message map has a COMMAND_ID_HANDLER entry for each of our menu commands:


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)
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;


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.


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;
        return S_OK;

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 )
    if ( NULL == pguidCmdGroup )
        if ( OLECMDID_REFRESH == nCmdID )
            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.)

    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'
                val Attributes = d '671088688'

Here's the namespace extension item with its infotip:

 [Extension infotip - 3K]

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.

  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:

 [Delete confirmation msg - 12K]


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.


This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Written By
Software Developer (Senior) VMware
United States United States
Michael lives in sunny Mountain View, California. He started programming with an Apple //e in 4th grade, graduated from UCLA with a math degree in 1994, and immediately landed a job as a QA engineer at Symantec, working on the Norton AntiVirus team. He pretty much taught himself Windows and MFC programming, and in 1999 he designed and coded a new interface for Norton AntiVirus 2000.
Mike has been a a developer at Napster and at his own lil' startup, Zabersoft, a development company he co-founded with offices in Los Angeles and Odense, Denmark. Mike is now a senior engineer at VMware.

He also enjoys his hobbies of playing pinball, bike riding, photography, and Domion on Friday nights (current favorite combo: Village + double Pirate Ship). He would get his own snooker table too if they weren't so darn big! He is also sad that he's forgotten the languages he's studied: French, Mandarin Chinese, and Japanese.

Mike was a VC MVP from 2005 to 2009.

Comments and Discussions

QuestionFix for CPidlMgr Pin
alexkiri7-Jun-23 12:19
alexkiri7-Jun-23 12:19 
Questioncompile dll from src files Pin
Member 1313051212-Feb-22 1:50
Member 1313051212-Feb-22 1:50 
QuestionNo FCIDM_MENU_HELP on windows 10? Pin
Priyank Bolia13-Feb-18 23:01
Priyank Bolia13-Feb-18 23:01 
QuestionExplorer not calling CreateViewObject with IID_IShellView Pin
Robert Bielik2-May-13 3:23
Robert Bielik2-May-13 3:23 
AnswerRe: Explorer not calling CreateViewObject with IID_IShellView Pin
Kiran Randhawa15-Aug-13 2:03
Kiran Randhawa15-Aug-13 2:03 
GeneralRe: Explorer not calling CreateViewObject with IID_IShellView Pin
Kiran Randhawa15-Aug-13 2:04
Kiran Randhawa15-Aug-13 2:04 
GeneralMy vote of 5 Pin
Menno de Ruiter24-Mar-13 16:59
Menno de Ruiter24-Mar-13 16:59 
Questionwhere is the SimpleNsExt.h file? Pin
Steve Richter23-May-12 13:29
Steve Richter23-May-12 13:29 
QuestionRun - OK, debug - crash Pin
imagiro1-May-12 1:11
imagiro1-May-12 1:11 
Answer...and solved. Pin
imagiro3-May-12 11:35
imagiro3-May-12 11:35 
GeneralTreeView shows the wrong hierarchy Pin
Kostia Khait22-Aug-09 4:32
Kostia Khait22-Aug-09 4:32 
GeneralRe: TreeView shows the wrong hierarchy Pin
andrej33@yandex.ru30-Aug-09 23:12
andrej33@yandex.ru30-Aug-09 23:12 
QuestionDo anybody have code samples of Dino Esposito book Pin
Javed Akhtar Ansari9-Nov-08 23:10
Javed Akhtar Ansari9-Nov-08 23:10 
AnswerRe: Do anybody have code samples of Dino Esposito book Pin
VC++Maniac8-Dec-08 3:40
VC++Maniac8-Dec-08 3:40 
GeneralRe: Do anybody have code samples of Dino Esposito book Pin
Javed Akhtar Ansari9-Dec-08 17:27
Javed Akhtar Ansari9-Dec-08 17:27 
GeneralProblem registering a NSE Pin
PeterWalther28-Sep-08 0:48
PeterWalther28-Sep-08 0:48 
QuestionCan I use MFC controls here. Pin
Javed Akhtar Ansari23-Sep-08 0:27
Javed Akhtar Ansari23-Sep-08 0:27 
Questionwindows explorer crash... on Vista Pin
VC++Maniac11-Sep-08 21:15
VC++Maniac11-Sep-08 21:15 
QuestionRe: windows explorer crash... on Vista Pin
Javed Akhtar Ansari18-Nov-08 23:42
Javed Akhtar Ansari18-Nov-08 23:42 
AnswerRe: windows explorer crash... on Vista Pin
VC++Maniac8-Dec-08 3:36
VC++Maniac8-Dec-08 3:36 
AnswerRe: windows explorer crash... on Vista Pin
Javed Akhtar Ansari9-Dec-08 17:26
Javed Akhtar Ansari9-Dec-08 17:26 
GeneralRe: windows explorer crash... on Vista Pin
InfiniteMort19-Jun-09 1:12
InfiniteMort19-Jun-09 1:12 
GeneralRe: windows explorer crash... on Vista Pin
Javed Akhtar Ansari21-Jun-09 18:27
Javed Akhtar Ansari21-Jun-09 18:27 
AnswerRe: windows explorer crash... on Vista Pin
Thoughtweaver18-Jul-09 23:30
Thoughtweaver18-Jul-09 23:30 
QuestionHow to remove or implement "Select" option in windows vista? Pin
Member 352333523-Jul-08 0:13
Member 352333523-Jul-08 0:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.