Contents
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.
Everyone who has browsed through the MSDN documentation to learn and understand how to create shell extensions will sooner or later discover the terms SHITEMID
s and PIDL
s. 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;
BYTE abID[1];
} 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.
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.
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.
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;
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.
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 )
{
CString strMsg =
LoadString(IDS_SHELLFOLDER_CANNOT_PERFORM) +
FormatLastError(static_cast<DWORD>(hr));
IsolationAwareMessageBox(hwnd, strMsg,
LoadString(IDS_SHELLEXT_ERROR_CAPTION),
MB_OK | MB_ICONERROR);
}
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:
- 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.
- 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.
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);
}
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()
{
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.
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 ,
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.
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 ) const
{
switch (nCompareBy)
{
case COLUMN_NAME:
return CompareByName(item);
case COLUMN_SIZE:
return UIntCmp(_nSize, item._nSize);
default:
ATLASSERT(!"Illegal nCompare option detected");
RaiseException();
}
}
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.
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 TItem
s. 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 ,
QCMINFO& qcminfo)
{
CCfShellIdList cfshellidlist(pdataobject);
if (cfshellidlist.GetItemCount() == 1)
{
CMenu menu(true);
menu.AddDefaultItem(ID_DFM_CMD_OPEN, _T("&Open"));
MergeMenus(qcminfo, menu);
}
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.
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.
SFGAOF CShellFolder::GetAttributesOfGlobal(UINT cidl,
SFGAOF )
{
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.
CShellFolderViewCB() :
IShellFolderViewCBImpl<CShellFolderViewCB>(
SHCNE_RENAMEITEM | SHCNE_RENAMEFOLDER | SHCNE_DELETE)
{
};
static CComPtr<IShellFolderViewCB>
CreateInstance(const ITEMIDLIST* pidlFolder)
{
CComObject<CShellFolderViewCB>* pinstance;
RaiseExceptionIfFailed(
CComObject<CShellFolderViewCB>::CreateInstance(
&pinstance));
CComPtr<IShellFolderViewCB> shellfolderviewcb(
pinstance);
pinstance->SetFolder(pidlFolder);
return shellfolderviewcb;
}
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.
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 , CVVVItem& item,
const TCHAR* szNewName, SHGDNF shgndf)
{
RaiseExceptionIf(shgndf != SHGDN_NORMAL &&
shgndf != SHGDN_INFOLDER);
item.SetDisplayName(szNewName, shgndf);
CVVVFile(GetPathFolderFile()).SetItem(item);
}
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
(Titem
s). The code below shows how the sample handles this:
long CShellFolder::OnDelete(HWND hwnd, CVVVItems& items)
{
if (hwnd != NULL && !UserConfirmsFileDelete(hwnd, items))
return 0;
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.
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).
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)
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.
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.
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));
}
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);
}
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.
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.