Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / ATL

The Mini Shell Extension Framework – Part III

4.96/5 (11 votes)
18 Sep 200516 min read 6   1.4K  
Discussion of a small C++ framework to create Windows shell extensions (IShellFolderImpl).

Contents

Introduction

This is part 3 of the mini shell framework article series. Here, I'll discuss how to create a namespace extension by creating a custom ShellFolder that works together with standard system FolderView. ShellFolders are complex shell extensions due to the large amount of features that need to be implemented. SDK shell documentation is often incomplete or missing. Even some critical Win32 shell API functions are undocumented! The ShellFolderImpl class is explained with help of the VVV sample. Code for this sample is included in the framework. This sample operates on .vvv files (renamed .ini files). This is the same sample I had used in part I and part II, but here it is extended for sub-folders. There are two kinds of namespace extensions: rooted and non-rooted. The VVV sample demonstrates a non-rooted type.

SHITEMID and PIDLs

Everyone who has browsed through the MSDN documentation to learn and understand how to create shell extensions will sooner or later discover the terms SHITEMIDs and PIDLs. These are the key data structures used by the shell to identify shell objects. The current MSDN documentation gives a thorough explanation of the concepts.

The definition of an ITEMIDLIST is:

typedef struct _ITEMIDLIST
{
    SHITEMID  mkid;
} ITEMIDLIST;

The definition of a SHITEMID is:

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

Every item in the shell needs to have its own internal unique identification. This shell item identifier (SHITEMID) needs to be unique within its own (virtual) folder. SHITEMID can be combined into a list of IDs (the PIDL). A typical PIDL for an item of a file system would be:

[C:] – [My Documents] – [sample.vvv] – [0]

This PIDL contains three SHITEMID and a terminating SHITEMID with size 0.

The main question is which identifier to use for a certain item. The MSDN documentation is not very clear about this so here are some guidelines:

  • Don’t use pointers as item identifiers. While pointers are unique, this will cause problems when two instances of a ShellFolder display the same item. Item IDs need to be identical if they point to the same item. The typical use case for this scenario is when the user opens two Windows to the same folder location. Tip: Always verify that your extension works when using two shell views pointing to the same location.
  • For a simple view (one that need not support item delete, copy paste, etc.) an array based solution can be used with the index in the array as identifiers.
  • For file system items the most logical selection is the filename. Most file systems enforce that file names should be unique in a directory. The shell can handle that, an ID of an item is changed as long as there is a relationship with the display name. The SHChangeNotify function can be used to fire a 'RENAME' to notify the system that a certain PIDL is replaced by another PIDL.
  • The shell will always call the ShellFolder instance to parse the SHITEMID. This allows you to store any info into the SHITEMID structure. The vvv sample stores all the info into the SHITEMID.
  • For performance reasons caching of items is required. Be careful to refresh the cache when more than one view can be used to edit the folder.

For the VVV sample I started by storing all items in a std::vector and using the index as a PIDL. This was a dead end when I tried to add edit functionality to the sample. The current sample uses a unique slot position ID to identify items. This also allows the sample to support sub folders and identical display names.

Basic design concept

A shell namespace extension consists of a couple of COM objects that work together. The four most important objects are:

  • A COM object that supports the IShellView interface.
  • A COM object that supports the IShellFolder(2) interface.
  • A COM object that supports the IEnumIDList interface.
  • A COM object that supports the IDataObject interface.

IShellView interface

Windows (shell32.dll) provides a default implementation for a COM object that supports the IShellView interface. This default system COM object supports the common view modes (details, icons, etc.) and can handle most events. A callback interface called IShellFolderViewCBImpl is used by the system view object to pass events and allow control of the behavior of the system view object. The IShellFolderImpl class uses by default this standard system view object. The advantages of using the default ShellView are identical look and feel and behavior as the rest of the explorer UI.

IShellFolder(2) interface

The core of a namespace extension is the ShellFolder object. This object is responsible for maintaining the items that are inside the folder. Every operation on these items is controlled by the ShellFolder object. The framework provided IShellFolderImpl template class is designed to make it easy to create a ShellFolder object. It expects two template arguments: TDerived and TItem. TDerived is the type name of the derived class. All pre processed events are forwarded to this class. The TDerived class needs to provide the following functions:

CComPtr<IEnumIDList> CreateEnumIDList(HWND hwnd, 
                                   DWORD grfFlags);

SFGAOF GetAttributeOf(unsigned int cidl, 
        const TItem& item, SFGAOF sfgofMask) const;

All other functions have a default implementation provided by the IShellFolderImpl class.

TItem is the type name of a wrapper around a PIDL item. The IShellFolderImpl will wrap every incoming PIDL in a TItem object. The TItem class needs to support the following functions:

TItem(const SHITEMID& shitemid);

CString GetDisplayName(SHGDNF shgdnf = SHGDN_NORMAL) 
                                               const;

int Compare(const TItem& item, int nCompareBy, 
                         bool bCanonicalOnly) const;

IEnumIDList interface

The ShellFolder must return an enumeration object to the ShellView. The ShellView uses this enumerator object to fill it’ internal display list. The framework depends currently on the ATL class CComEnumOnSTL to provide this functionality.

IDataObject interface

To interact with copy\paste and drag and drop the ShellFolder must support an object of IDataObject that can interact with shell clipboard operations. The framework provides a CShellFolderDataObjectImpl template class and a couple of clipboard format handlers to implement the required functionality.

Error handling

The framework relies on exceptions to handle runtime errors. Two types of exceptions are expected to be thrown (see also part I): _com_error exceptions and exceptions derived from std::exception. A couple of IShellFolder functions have a HWND argument that can be used to display messages to the user. If an exception occurs, the IShellFolderImpl class will catch it and forward the exception to an OnError function. The default implementation of OnError is empty but the derived class can translate the error condition to a string and display it to the user. The VVV sample code is shown below:

void OnError(HRESULT hr, HWND hwnd, 
                   EErrorContext /*errorcontext*/)
{
  CString strMsg = 
     LoadString(IDS_SHELLFOLDER_CANNOT_PERFORM) +
         FormatLastError(static_cast<DWORD>(hr));

  IsolationAwareMessageBox(hwnd, strMsg,
        LoadString(IDS_SHELLEXT_ERROR_CAPTION), 
        MB_OK | MB_ICONERROR);
}

Features

Before we begin to discuss the implementation in detail it is good to have a list of features that our sample namespace extension should be able to perform:

Read-only features

  • View the items stored in a .vvv file.
  • Sort items in detailed view mode.
  • Supports a custom item infotip.
  • Supports a custom property sheet.
  • Supports sub folders.

Edit features

  • rename items,
  • delete items,
  • change attributes of items using the custom property sheet,
  • copy/cut paste/drag-drop items to the file system from a .vvv file,
  • copy/cut paste/drag-drop items from the file system to a .vvv file.

COM registration of the ShellFolder

As with all other shell extensions a namespace extension must be registered as a COM object before it can be used. The framework provides a registration function and ATL registration scripts for this purpose. Windows 98 needs a special registration script as it doesn’t have version 5 of shell32.dll. The code below shows the static registration function of the sample that will be called by ATL when registration is required.

static HRESULT WINAPI
CVVVSample::UpdateRegistry(BOOL bRegister) throw()
{
  return
    IShellFolderImpl<CShellFolder, CVVVItem>::UpdateRegistry(
           IDR_SHELLFOLDER, IDR_SHELLFOLDER_WIN98, bRegister,
           L"Sample ShellExtension ShellFolder", wszVVVExtension,
           IDS_SHELLFOLDER_TYPE);
}

Construction and initialization

Two template arguments are required . The first is the class name of the class. The other is the name of a class that can wrap a PIDL and act on it. A lot of requests are forwarded to this CVVVItem class.

class ATL_NO_VTABLE CShellFolder :
  public IShellFolderImpl<CShellFolder, CVVVItem>,

The constructor of the sample ‘registers’ the columns the folder supports. The ShellView object will query these columns when it is configured to display the items in detailed mode.

CShellFolder::CShellFolder()
{
  // Register the columns the folder supports 
  // in 'detailed' mode.
  RegisterColumn(IDS_SHELLEXT_NAME, LVCFMT_LEFT);
  RegisterColumn(IDS_SHELLEXT_SIZE, LVCFMT_RIGHT);
}

The IShellFolderImpl class has a default implementation for the IShellFolder::Initialise function. This default implementation will just store the root folder. This root folder can be retrieved with the GetRootFolder function.

Read-only features

Viewing items in a Shellfolder

The primary task of a ShellFolder is to manage the list of items in that folder. The ShellView object will call the IShellFolder::EnumObjects function to retrieve an enumerator for all items. IShellFolderImpl will forward this request to the CreateEnumIDList function. This function needs to be implemented by the user class.

CComPtr<IEnumIDList> CreateEnumIDList(HWND /*hwnd*/, 
                                       DWORD grfFlags)
{
  auto_ptr<vector<CVVVItem> > qitems =
                         CVVVFile(GetPathFolderFile(), 
                         _strSubFolder).GetItems(grfFlags);
  return CEnumIDList::CreateInstance(GetUnknown(), qitems);
}

To construct the requested enumerator object the sample asks the CVVVFile object for a list of current items. The CVVVFile object opens and parses the current .vvv file and returns a std::vector with the items. This vector is then used to create a enumerator object around it. This enumerator object is based on the ATL CComEnumOnSTL template base class. The shell will use the enumerator to get all the PIDLs and then release the enumerator object.

Sorting items in a Shellfolder

When the system ShellView displays the items in a 'detailed' mode the user can sort the items by clicking on the header of the ListView. The ShellView will forward this event to the ColumnClick function. The framework's default implementation returns S_FALSE to indicate that the system ShellView should handle the event. The system ShellView object will then sort the column. If the framework detects that it runs on a version of shell32.dll that doesn’t support this default handling (Win 95\Win 98) it will explicitly trigger the ShellView to do the sorting. During the sorting process the ShellView calls IShellFolder::CompareIDs every time it needs to compare two items for the sorting process. IShellFolderImpl will handle this request by wrapping the PIDLs in TItems and then forwarding the call to the item. The code below shows how the sample handles it:

int CVVVItem::Compare(const CVVVItem& item, 
           int nCompareBy, bool /*bCanonicalOnly*/) const
{
  switch (nCompareBy)
  {
  case COLUMN_NAME:
    return CompareByName(item);

  case COLUMN_SIZE:
    return UIntCmp(_nSize, item._nSize);

  default:
    ATLASSERT(!"Illegal nCompare option detected");
    RaiseException();
  }
}

Providing an infotip for items in a Shellfolder

When the user hovers his mouse pointer over an item the system ShellView either shows a infotip for that item or displays a status text in the status bar. The ShellFolder object determines and controls the text to be displayed. The ShellView will call GetUIObjectOf with a request for an IQueryInfo interface. The IShellFolderImpl class will check if the interface is requested only for one item and then wrap the PIDL in a TItem. This TItem is then queried for its tooltip text. If this string is not empty a QueryInfo object is created and returned.

Supporting sub folders

Depending on the requirements for the shell folder it may be necessary to support sub folders. To support sub folders the following steps must be followed:

The first step is to return the attribute SFGAO_FOLDER for items that are sub folders. The tree view in explorer can then be used to access these sub folders. When the shell needs access to a folder it will follow the given procedure:

  • Create an instance of the ShellFolder.
  • Call BindToObject for a new instance bound to the correct subfolder. This sub folder can be more than one level deep.
  • Release the original ShellFolder.

The IShellFolderImpl class has support for handling sub folders. It will parse the item list containing the sub folders to a vector of TItems. It will then create a new ShellFolder instance, initialize the root folder and pass the vector of items to the function InitializeSubFolder. The InitializeSubFolder function must be implemented by the derived class. The sample code demonstrates this.

To support the standard feature where a user can open a sub folder inside the folder view the ShellFolder must provide a context menu for sub folder items.

HRESULT CShellFolder::OnDfmMergeContextMenu(
          IDataObject* pdataobject, UINT /*uFlags*/, 
          QCMINFO& qcminfo)
{
  CCfShellIdList cfshellidlist(pdataobject);

  if (cfshellidlist.GetItemCount() == 1)
  {
    // Add 'open' if only 1 item is selected.
    CMenu menu(true);
    menu.AddDefaultItem(ID_DFM_CMD_OPEN, _T("&Open"));
    MergeMenus(qcminfo, menu);

    // Note: XP will automatic make 
    // the first menu item the default.
    // Win98, ME and 2k don't do this, 
    // so must add as default item.
  }

  return S_OK;
}

The actual 'open' request must be handled:

void CShellFolder::OnOpen(HWND hwnd, 
                         IDataObject* pdataobject)
{
  CVVVItems items;

  RetrieveItems(pdataobject, items);
  ATLASSERT(items.size() == 1);

  if (items[0].IsFolder())
  {
    CPidl pidlFolder(items[0].CreateShellItemIdList());

    GetShellBrowser().BrowseObject(pidlFolder, 
      SBSP_DEFBROWSER | SBSP_RELATIVE);
  }
  else
  {
    CString strMessage = 
                _T("Open on: ") + items[0].GetName();
    IsolationAwareMessageBox(hwnd, strMessage, 
                _T("Open"), MB_OK | MB_ICONQUESTION);
  }
}

The sample supports open on folders and non-folders. An 'open' command on a folder item will actually open the sub folder, but non folder items will just show a message box.

Edit features

Preparing for the edit features

Two important requirements need to be fulfilled before edit features start to work. When the user creates a selection of items and wants to perform an operation on it the ShellView asks the ShellFolder what it can do with this collection of items. The IShellFolderImpl class provides a default implementation that will first try to detect if a global setting exists. If no global settings exist it will call GetAttributeOf for every item in the selection. The next sample code shows how to handle the global setting request. The sample will return that, items can be renamed, deleted, copied and moved. If only one item is selected it reports that it supports a property option. The sfgofMask parameter contains the bit mask of what the caller really wants to know and can be used to skip expensive computations.

// Purpose: called by MSF to detect if a 
// global attribute setting exists.
SFGAOF CShellFolder::GetAttributesOfGlobal(UINT cidl, 
                                   SFGAOF /*sfgofMask*/)
{
  // Tell the shell what it can do with 
  // an item inside a VVV file.
  SFGAOF sfgaof = SFGAO_CANRENAME | SFGAO_CANDELETE |
                          SFGAO_CANCOPY | SFGAO_CANMOVE;

  if (cidl == 1)
  {
    sfgaof |= SFGAO_HASPROPSHEET;
  }

  return sfgaof;
}

The second requirement is that we need to tell the system folder view that it should register itself for the 'change' events our ShellFolder generates. To tell the system folder view, we need to provide an IShellFolderViewCB interface while creating the system folder view and handle the SFVM_GETNOTIFY message.

The framework provides a default COM object implementation for the IShellFolderViewCB interface. The following code sample shows how to use this default implementation.

// Create a derived class and set the event bitmask.
CShellFolderViewCB() :
  IShellFolderViewCBImpl<CShellFolderViewCB>(
    SHCNE_RENAMEITEM | SHCNE_RENAMEFOLDER | SHCNE_DELETE)
{
};
// Create a helper function that can 
// create an instance of this COM
// object and initialize it.
static CComPtr<IShellFolderViewCB> 
CreateInstance(const ITEMIDLIST* pidlFolder)
{
  CComObject<CShellFolderViewCB>* pinstance;
  RaiseExceptionIfFailed(
    CComObject<CShellFolderViewCB>::CreateInstance(
                                          &pinstance));
  CComPtr<IShellFolderViewCB> shellfolderviewcb(
                                            pinstance);

  pinstance->SetFolder(pidlFolder);

   return shellfolderviewcb;
}
// Replace the default framework 
// CreateShellFolderViewCB.
CComPtr<IShellFolderViewCB> CShellFolder::CreateShellFolderViewCB()
{
  return CShellFolderViewCB::CreateInstance(GetRootFolder());
}

Tip: During the testing of edit functionality it is useful to have two explorer views open on the same folder. This gives quick feedback if the notifications of an action reaches all the views.

Renaming an item

To support renaming of items the ShellFolder function GetAttributeOf(Global) needs to return SFGAO_CANRENAME. This will trigger the FolderView to offer a 'rename' option to the user. When the user has renamed the item the view will call IShellFolder::SetNameOf passing the new name. The IShellFolderImpl will wrap the PIDL in a TItem and then pass the call to the OnSetNameOf function to perform the actual update. If the name could actually be changed the IShellFolderImpl will fire a SHCNE_RENAMEITEM notification to update all open explorer views. The code below shows how the sample handles the rename request:

void CShellFolder::OnSetNameOf(HWND /*hwnd*/, CVVVItem& item,
                          const TCHAR* szNewName, SHGDNF shgndf)
{
  RaiseExceptionIf(shgndf != SHGDN_NORMAL && 
                                      shgndf != SHGDN_INFOLDER);

  item.SetDisplayName(szNewName, shgndf);
  CVVVFile(GetPathFolderFile()).SetItem(item);
}

Deleting items

The IShellFolderImpl provides support for deleting items. When the ShellView asks for the attributes of an item (forwarded to the GetAttributesOf function) and the ShellFolder returns SFGAO_CANDELETE the explorer offers the user the option to delete the item(s). When the user requests to delete the current selected items the request is forwarded to the ShellFolder. The IShellFolderImpl performs a quick check to make sure every item has the SFGAO_CANDELETE attribute set and will then call OnDelete (Titems). The code below shows how the sample handles this:

long CShellFolder::OnDelete(HWND hwnd, CVVVItems& items)
{
  if (hwnd != NULL && !UserConfirmsFileDelete(hwnd, items))
    return 0; // user wants to abort the 
              // file deletion process.

  CVVVFile(GetPathFolderFile(), 
               m_strSubFolder).DeleteItems(items);

  return SHCNE_DELETE;
}

When the items are deleted the OnDelete function should return the event that must be broadcasted to the system. This event ensures that all other views will update itself.

Edit properties

An alternative method of renaming an item with the ShellView is to use a property page. This also allows the user to view and change other attributes of the item. The IShellFolderImpl will call the function OnProperties when it receives a request to show the 'properties' dialog.

long CShellFolder::OnProperties(HWND hwnd, CVVVItems& items)
{
  ATLASSERT(items.size() == 1);
  CVVVItem& item = items[0];

  long wEventId;
  if (CVVVPropertySheet(item, this).DoModal(hwnd, 
                             wEventId) > 0 && wEventId != 0)
  {
    CVVVFile vvvfile(GetPathFolderFile());
    vvvfile.SetItem(item);
  }

  return wEventId;
}

The shellfolder controls the GUI of the properties dialog window. It is up to the implementation to select an appropriate GUI window. It can be a simple dialog or a property sheet. The framework provides a simple CPropertySheet class that can be used in combination with the ATL CSnapInPropertyPageImpl class to create a property sheet (with pages).

Clipboard interaction and drag \ drop

IShellFolderImpl has built in support for clipboard transactions and drag and drop operations. To enable drag and drop support the derived class must:

Add the IDropTarget interface to the interface map.

COM_INTERFACE_ENTRY(IDropTarget) // enable drag and drop support.

Create a CShellFolderDataObject class that supports the IDataObject interface. To implement this class the framework provides the CShellFolderDataObjectImpl template base class. The special thing about a DataObject that participates in shell drag and drop operations is that it needs to accept additional clipboard formats. Windows provides the function CIDLData_CreateFromIDArray to create such a DataObject. The CShellFolderDataObjectImpl class is a wrapper around this DataObject. Besides wrapping it allows registered clipboard formats to take precedence. The sample code below shows how the sample registers two handlers for the standard clipboard formats (CFSTR_FILEDESCRIPTOR and CFSTR_FILECONTENTS).

void CShellFolderDataObject::Init(const ITEMIDLIST* pidlFolder, 
                               UINT cidl, const ITEMIDLIST** ppidl,
  IPerformedDropEffectSink* pperformeddropeffectsink)
{
  __super::Init(pidlFolder, cidl, ppidl, pperformeddropeffectsink);

  auto_ptr<CCfHandler>
    qfiledescriptorhandler(new CCfFileDescriptorHandler(this));
  RegisterCfHandler(qfiledescriptorhandler);

  auto_ptr<CCfHandler>
    qfilecontentshandler(new CCfFileContentsHandler(this));
  RegisterCfHandler(qfilecontentshandler);
}

Which clipboard formats to support and how to convert a PIDL, for example into a file system object, depends completely on the implementation. The framework provides only the skeleton to make it easy to pack that functionality into a clipboard format handler class.

Selecting items for clipboard operations

Coping items to the clipboard is a process that consists of several steps. The user makes first a selection of the items in the view window. The ShellView will then call the GetUIObjectOf function with a request for a IDataObject interface for these selected items. The framework will forward this request to the CreateDataObject function. The standard pattern to handle this request is shown in the sample code below. The folder object creates a new CShellFolderDataObject COM object.

CComPtr<IDataObject>
CShellFolder::CreateDataObject(const ITEMIDLIST* pidlFolder, 
                            UINT cidl, const ITEMIDLIST** ppidl)
{
  return
    CShellFolderDataObject::CreateInstance(pidlFolder, 
                                         cidl, ppidl, this);
}

When the ShellView has this DataObject it depends on the next action of the user what the ShellView will do with it. The user can delete, copy, cut or request the context menu for the selected items.

Copying the DataObject to the Clipboard

As the ShellView passes the DataObject and the copy request together the IShellFolderImpl class can handle the copy operation by itself. It will check whether SFGAO_CANCOPY is set for all selected items and then put the DataObject on the clipboard.

void IShellFolderImpl::OnCopy(HWND, IDataObject* pdataobject)
{
  VerifyAttribute(pdataobject, SFGAO_CANCOPY);

  CCfPreferredDropEffect::Set(pdataobject, DROPEFFECT_COPY);

  RaiseExceptionIfFailed(OleSetClipboard(pdataobject));
}

Cutting the Dataobject to the clipboard

The 'cut' operation is also completely handled by the framework. The main difference with the copy operation is that the ShellView must be notified about a ‘cut’ DataObject being put on the clipboard. This will trigger the ShellView to make the ‘cut’ items dimmed and allow the user to cancel the cut operation with the ‘Esc’ key.

void IShellFolderImpl::OnCut(HWND, IDataObject* pdataobject)
{
  VerifyAttribute(pdataobject, SFGAO_CANMOVE);

  CCfPreferredDropEffect::Set(pdataobject, DROPEFFECT_MOVE);

  RaiseExceptionIfFailed(OleSetClipboard(pdataobject));

  ShellFolderView_SetClipboard(GetHwndOwner(), DFM_CMD_CUT);
}

Receiving an external DataObject

There are two ways a ShellFolder can receive an external DataObject. The user can paste a DataObject from the clipboard or it can drag and drop a selection of items on the ShellView. Both operations are supported by the IShellFolderImpl class. The derived ShellFolder class needs to implement two functions to receive the external DataObject: IsSupportedClipboardFormat and AddItemsFromDataObject.

The code below is from the vvv sample. The IsSupportedClipboardFormat returns true when the DataObject supports the CF_HDROP (list of files) clipboard format, other formats are not supported. CCfHDrop is a class provided by the framework.

bool CShellFolder::IsSupportedClipboardFormat(
                                 IDataObject* pdataobject)
{
  return CCfHDrop::IsFormat(pdataobject);
}

The IsSupportedClipboardFormat is called when the user has 'pasted' something or when the drag-drop object enters the ShellView window.

When the ShellFolder supports the clipboard format the framework will call the AddItemsFromDataObject function. The sample code below demonstrates how to handle this:

DWORD CShellFolder::AddItemsFromDataObject(
           DWORD dwDropEffect, IDataObject* pdataobject)
{
  CCfHDrop cfhdrop(pdataobject);

  unsigned int nFiles = cfhdrop.GetFileCount();
  for (unsigned int i = 0; i < nFiles; ++i)
  {
    AddItem(cfhdrop.GetFile(i));
  }

  return dwDropEffect;
}

Depending on the action of the user the dwDropEffect argument is set to DROPEFFECT_MOVE or DROPEFFECT_COPY. The VVV sample always copies files from the filesystem into the VVV file. Other implementations can perform a filesystem file move as an optimization.

Future extensions

There are a lot of improvements that can be done to the current framework implementation; support for XP task bands, toolbar buttons, performance improvements, a rooted example, better sub folder support etc.

The current version provides a basic foundation for creating a ShellFolder namespace extension. With the basic implementation ready, it is easy to add small improvements.

License

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