Contents
Introduction
This article shows you an almost complete implementation of a Shell Namespace
Extension. It is the result of nights passed to achieve two goals:
- Use the system provided ShellView object.
- Be able to use this extension in the FileDialog (CommonDialog).
The first goal is an answer to the article by Henk Devos
'Namespace extensions - the
undocumented Windows Shell'. Several users requested a demo project for his
article, here it is!
The second goal was the spec I had to implement. This sample will in fact act
as a Favorites 'shortcut' system for the DirectoryOpus file manager (well, far
more than a file manager, see DOpus).
Before going on with this article, you should read Michael Dunn's 'The Complete
Idiot's Guide to Writing Namespace Extensions - Part I ', which is the base
I used to realize this one.
Some more infos are available in Microsoft Knowledge Base Article - 216954,
named "HOWTO: Support Common Dialog Browsing in a Shell Namespace Extension".
Requirements
System
Because this extension makes use of the
SHCreateShellFolderView
function, you must have IE 4.0 minimum
installed on your system. Note that for WinNT4 (and also Win95 if applicable)
Active Desktop (ships with IE 4) has to be installed even if not enabled.
I tested it on several Windows platforms, here is the list with some
comments:
OS version |
Shell version |
Comment |
Win98 |
4.7x |
- The removal message (@%MODULE%,-300) is not supported - Didn't test
Office FileDialog |
WinNT4 SP6 |
4.7x |
- The removal message (@%MODULE%,-300) is not supported - Didn't test
Office FileDialog |
Win2K |
5.0 |
Okay, OfficeXP FileDialog works ok |
WinXP Pro |
6.0 |
Okay, OfficeXP FileDialog (through MSDev 7) works
ok |
Since this extension works well on the previously listed platforms, it should
also for these (but I didn't test them):
- WinME
- WinXP Home
- Server edition of NT4, W2K, W2003
I didn't care about Windows 95,
at the time I write this article (2004 A.D.) I consider this OS as obsolete.
Note though that it should work because the dependencies are highly related to
Internet Explorer and not really to the OS.
Development Environement
The environement I used to create this sample
is MSDev 6.0 with ATL 3.0. I also successfully compiled it (though not the
MinSize builds) under MSDev 7.0 with ATL 7.0.
You must have the Microsoft Platform SDK, I used the one from February 2003.
This is important because I use recently documented API. So if you have an old
SDK (for example shipped with MSDev) it will not compile.
What will this extension do?
This extension will act as a Favorites shortcut system. Unlike the system
Favorites folder, I do not want to use a .lnk file for each shortcut (which is a
text file representing the shortcut). The shortcuts I want are stored in the
Registry. As told in the intro of this article, the shortcuts are the one stored
by DirectoryOpus.
What composes a shortcut?
From now on,
I will use the term Favorite instead of shortcut, so of what a Favorite is made?
- A name (example: "_/# Fancy Temp Dir #\_")
- A path (example: "E:\Temp")
- A rank
The rank determines the position in the Favorites List. The
lower value, the upper in the list.
You certainly noticed that the name can contain any chars, including the one
that are not allowed in paths.
Wanted behaviour
The root view of
my namespace has to show the Favorites, their names, and if in 'Details' mode,
the path and the rank.
The behaviour when the user selects one of the Favorite is to go to the real
path of the item. This is the only behaviour needed, what the user makes after
this does not concern us, but the target path namespace.
This sounds like it should be simple and stupid to implement. Aaaah, but like
always, there are ten ways to do the same thing with computers, and different
programs (read different MS teams) will use all of them and maybe one or two
more.
How is it implemented?
Okay, let's go to the real
work. What should we implement to achieve the wanted behaviour? Valuable
informations lie in the Platform SDK, check it for details.
PIDL layout
First of all, we must choose a PIDL layout. What are our items? They are
Favorites. What is a Favorite composed of? A name, a path and a rank. I choosed
to embed all these infos in the PIDL. For this I have a class CPidlMgr
that will do general PIDL handling, and a CDataFavo
class
that handles embedded info.
Example to get the Path from a PIDL:
LPOLESTR pOleStr = CDataFavo::GetPath( pidl );
See that string type: LPOLESTR
, all string info are stored as
UNICODE strings in my PIDL, even for ANSI builds. This is a design choice, read
the SDK to know why not use TCHAR
strings.
The CDataFavo
class inherits from CPidlData
which
is used by CPidlMgr
to create new PIDLs. To create a new PIDL,
first create a CDataFavo
object and then populate it with the
SetXXX()
methods. Then use CPidlMgr::Create()
to get
the PIDL. Example:
LPITEMIDLIST pidl;
CDataFavo Favo;
Favo.SetName(_T("_/# Fancy Temp Dir #\_"));
Favo.SetPath(_T("E:\\Temp"));
Favo.SetRank(3);
pidl = m_PidlMgr.Create(Favo);
Use cases for Explorer
I'll
describe the control flow of the extension with use cases. I'll not go into deep
explanations for all these use cases, please read the code, trace it, modify it.
If you stil miss something, ask it on the message board.
You will note that there are differences between the Explorer behaviour and
the FileDialog behaviour.
I assume you have an explorer with the TreeView (at the left) enabled. I'll
call the right view (where you see the items) the ShellView.
The first thing Explorer or any other controler will do is call our
IPersistFolder::Initialize()
method. As described in Michael Dunn's
article, we simply save here the passed pidl which is the position of our
extension in the system namespace.
Then the user will do one of the followings:
Clicking on the Namespace icon in the TreeView
So what happens when you click on the namespace icon in the TreeView? Another
method to have exactly the same behaviour is when you double-click the namespace
icon in the ShellView (when the Desktop content is shown).
Explorer will want to show the namespace items, for this it calls
IShellFoler::CreateViewObject()
requesting IShellView. Here I
create the view as requested. Because it is the system view (see Shell View) several methods of IShellFolder
will be called to populate the view.
Clicking the plus sign of the Namespace icon in the TreeView
Explorer will call this sequence:
EnumObjects()
once
CompareIDs()
several times
GetAttributesOf()
for each item
GetDisplayNameOf()
for each item
GetUIObjectOf()
for each item to get IExtractIcon
The result is the tree that gets expanded with our items (if they are
SFGAO_FOLDER
), showing their names. Note that at this stage the
ShellView still displays another directory.
Clicking a favorite item in the TreeView
Now if the user clicks on one of our item in the tree,
BindToObject()
is called passing the PIDL of the item. What we want
is explorer to display the target path content in the ShellView.
We have the target path (in our pidl) and we must BindToObject()
to this target path. So we create an absolue pidl (from the Desktop) and pass it
to BindToObject()
. Here it what it looks:
HRESULT hr;
CComPtr<IShellFolder> DesktopPtr;
hr = SHGetDesktopFolder(&DesktopPtr);
if (FAILED(hr))
return hr;
LPITEMIDLIST pidlLocal;
hr = DesktopPtr->ParseDisplayName(NULL, pbcReserved,
CDataFavo::GetPath(pidl), NULL, &pidlLocal, NULL);
if (FAILED(hr))
return hr;
hr = DesktopPtr->BindToObject(pidlLocal, pbcReserved, riid, ppvOut);
ILFree(pidlLocal);
return hr;
Double-clicking a favorite item in the ShellView
Although the behaviour should be the same as clicking a favorite item in the
TreeView, explorer does it in another fashion.
In fact, double-clicking an item in the ShellView corresponds to invoking the
default entry of the item's context menu. When exploring folders, the default
entry is "Explore", this is why the behaviour is the same. Note that you can
implement a context menu with a different default entry and thus changing the
'double-click' behaviour.
This means that explorer will call our
IShellFolder::GetUIObjectOf()
method, requesting IContextMenu. In
our case we do not need to implement IContextMenu, what we'll do is simply
delegate this call to the target path.
To see how this can be done, read the next paragraph.
Right-clicking a favorite
item in the ShellView
Here a context menu has to be displayed. This menu is related to the selected
item. If several items are selected, the menu has to display entries that
applies to all of the selected items.
For my extension, I choosed to display the context menu of the target path,
so I don't have to implement one myself. I also only handle single selected
items, this is because each item can point to a different storage. So, I can't
easily delegate (that means without tons of code) the context menu to different
storages at the same time.
Explorer will call our IShellFolder::GetUIObjectOf()
method. To
delegate it to the target path we have to get a IShellFolder
to the
parent of the target path in order to call its GetUIObjectOf()
with
the single-item pidl.
To achieve this, we must first convert the target path to an absolute pidl,
this is done like that:
hr = SHGetDesktopFolder(&DesktopPtr);
if (FAILED(hr))
return hr;
LPITEMIDLIST pidlLocal;
hr = DesktopPtr->ParseDisplayName(NULL, NULL,
CDataFavo::GetPath(*pPidl), NULL, &pidlLocal, NULL);
if (FAILED(hr))
return hr;
Now pidlLocal
contains the absolute pild to the target path. We
must now get the parent IShellFolder
, this could be done with
SHBindToParent()
, but this function is only available from Shell
version 5.0, so here is an equivalent code:
LPITEMIDLIST pidlRelative;
LPITEMIDLIST pidlTmp = ILFindLastID(pidlLocal);
pidlRelative = ILClone(pidlTmp);
ILRemoveLastID(pidlLocal);
hr = DesktopPtr->BindToObject(pidlLocal, NULL,
IID_IShellFolder, (void**)&TargetParentShellFolderPtr);
ILFree(pidlLocal);
if (FAILED(hr))
{
ILFree(pidlRelative);
return hr;
}
TargetParentShellFolderPtr
has now the parent IShellFolder
so that we can call its GetUIObjectOf
method with the
single-item pidl which is in pidlTmp
.
hr = TargetParentShellFolderPtr->GetUIObjectOf(hwndOwner, 1,
(LPCITEMIDLIST*)&pidlRelative,
riid, puReserved, ppvReturn);
The redirection is made, if any of
the previous function fails the context menu is simply not shown. This can
happen when the target path doesn't exist or is inaccessible (network).
Use cases for the
FileDialog
The FileDialog which is the one from Common Dialogs, behaves a
little different than explorer.
First the namespace icon has to be displayed in the upper ComboBox, for that
we must register our extension with the following flags:
SFGAO_FILESYSANCESTOR, SFGAO_FILESYSTEM, SFGAO_FOLDER,
SFGAO_BROWSABLE
. This correspond to the Attributes
value in
the registry under the ShellFolder
key, see the project .rgs file.
I don't remember exactly at which condition (OS, shell version, use case) but
not implementing IShellFolder2 can lead to problems, so implement it. The only
added method is GetCurFolder()
which simply return a copy of the
pidl passed to IShellFolder::Initialize()
.
Choosing the Namespace
icon in the upper ComboBox
The FileDialog will simply call
IShellFolder::CreateViewObject()
, because we use the system
ShellView several methods of IShellFolder
will be called in
response to creating the view object. They are (in no particular order):
EnumObjects()
CompareIDs()
GetAttributesOf()
GetDisplayNameOf()
GetUIObjectOf()
Double-clicking a
favorite item in the View
For a reason I don't really understand, the FileDialog will not simply call
IShellFolder::BindToObject()
with the item pidl.
It first callsIShellFolder::GetUIObjectOf()
requesting a
IDataObject
. Looking at this interface in the SDK informs us that
it is used to exchange any form of data between modules. It is used for
clipboard, drag and drop and the like.
So how are we concerned about this? The FileDialog will use a
IDataObject
as a container of the item pidl.
The called sequence for this use case is (removing calls to
GetAttributesOf()
and GetDisplayNameOf()
):
GetUIObjectOf()
BindToObject()
The only method of IDataObject that is
called by the FileDialog is
GetData()
. The purpose is to get the
item pidl. All other methods can simply return
E_NOTIMPL
.
Let's take a look at it:
STDMETHODIMP CDataObject::GetData(LPFORMATETC pFE, LPSTGMEDIUM pStgMedium)
{
if (pFE->cfFormat == m_cfShellIDList)
{
pStgMedium->hGlobal = CreateShellIDList(m_pidlParent,
(LPCITEMIDLIST*)&m_pidl, 1);
if (pStgMedium->hGlobal)
{
pStgMedium->tymed = TYMED_HGLOBAL;
pStgMedium->pUnkForRelease = NULL;
return S_OK;
}
}
return E_INVALIDARG;
}
When GetUIObjectOf()
is called, we create a IDataObject
object and set its m_pidlParent and m_pidl with the item
pidl,GetData()
simply return them in a CIDA structure which is done
by CreateShellIDList()
.
Beware of the line pStgMedium->pUnkForRelease = NULL;
, as
described in the comment WinXP could crash if you omit it.
Right-clicking a
favorite item in the ShellView
Like in Explorer, this will show a context menu. The behaviour is the same as
for explorer (see Right-clicking a favorite item in
the ShellView) but before calling GetUIObjectOf()
, it also gets
the item pidl through IDataObject
, like described above.
Problems with Office
FileDialog
Yes, the Office FileDialog is not the one from CommonDialog, it
is a modified one.
Note that MSDev 7.x (.net) also has this modified
FileDialog.
It has two drawbacks:
FileSystem existance check
The first drawback is that it will check every folder that you are browsing,
and complain if it is not a valid File System folder (a valid path).
It gets the folder path by calling our IShellFolder::GetDisplayNameOf()
method with the SHGDN_FORPARSING
flag. For virtual folders
(like ours) the SDK states that in response to this call we should return the
namespace extension GUID preceded by double semi-colons, like this: "::{GUID}".
Of course this string is not a valid path, so the extension can't be used from
the Office FileDialog, sigh!
To resolve this, we must return a valid path instead of "::{GUID}". Because
my extension doesn't have an install folder, I decided to return a path that
should be present in all systems: the temporary directory.
The first part of the GetDisplayNameOf()
method looks like this:
STDMETHODIMP CShellFolder::GetDisplayNameOf(
LPCITEMIDLIST pidl, DWORD uFlags, LPSTRRET lpName)
{
if ((pidl == NULL) || (lpName == NULL))
return E_POINTER;
if (pidl->mkid.cb == 0)
{
switch (uFlags)
{
case SHGDN_NORMAL | SHGDN_FORPARSING :
TCHAR TempPath[MAX_PATH];
if (GetTempPath(MAX_PATH, TempPath) == 0)
return E_FAIL;
return SetReturnString(TempPath, *lpName) ? S_OK : E_FAIL;
}
return E_FAIL;
}
...
...
}
There is one more thing to do. Like I said before, the FileDialog will call
GetDisplayNameOf()
to retrieve the folder name you are browsing.
This is true only if you inform it that you want to be called for parsing these
names. By default it will not call you. Enabling this behaviour needs a registry
value to be set. Create an empty string value named 'wantsFORPARSING' under the
key HKCR\CLSID\{extension guid here}\ShellFolder
.
Sub-items browsing
Another modified behaviour is when the Office FileDialog browses sub-items.
It still calls the root IShellFolder
BindToObject()
method, but with multi-level pidl. Note that this is totaly legal (see SDK), but
it adds complexity for our code.
Until now our BindToObject()
expected a single-level pidl which
contained the favorite target path. We now must handle pidl that will still
start with our item but with sub-items related to the target path sub-folders.
I modified the code of BindtoObject()
if (!m_PidlMgr.IsSingle(pidl))
{
HRESULT hr;
hr = SHGetDesktopFolder(&DesktopPtr);
if (FAILED(hr))
return hr;
LPITEMIDLIST pidlLocal;
hr = DesktopPtr->ParseDisplayName(NULL, pbcReserved,
CDataFavo::GetPath(pidl), NULL, &pidlLocal, NULL);
if (FAILED(hr))
return hr;
CComPtr<IShellFolder> RootFolderPtr;
hr = DesktopPtr->BindToObject(pidlLocal, NULL,
IID_IShellFolder, (void**)&RootFolderPtr);
ILFree(pidlLocal);
if (FAILED(hr))
return hr;
return RootFolderPtr->BindToObject(m_PidlMgr.GetNextItem(pidl),
pbcReserved, riid, ppvOut);
}
...
First I check if it's a single level pidl or not. Then I get the target path
pidl, like in the previous code. Then I get the IShellFolder of the target path
with the first BindToObject()
, this permits us to call its
BindToObject()
with the reminder of the pidl which is the sub (or
sub-sub, or ...) folder of it.
Shell View
The system ShellView provided by the newly documented API,
but existing from shell 4.7x,
SHCreateShellFolderView()
does much
of the work. To populate and handle the items, it will call our
IShellFolder
methods and also
IShellFolder2
methods.
Basically the only thing we have to do is implement these methods which, for the
majority of them, is already done.
Be aware that not implementing IShellFolder2
will lead to a
'script error' when Web View is enabled (almost on Win2K). So implement
IShellFolder2
even if you return E_NOTIMPL
from every
methods.
WARNING: I didn't use SHCreateShellFolderViewEx()
like
described in Henk Devos article. I used to, but I changed because this function
didn't work (didn't create the view) in the FileDialog.
When using SHCreateShellFolderView()
, you have to provide a
IShellFolderViewCB
which contains the MessageSFVCB
method that the ShellView will call to let you handle some messages. Because I
use ATL, I made a base class to encapsulate the View creation but better, to
handle the view messages through a standard message map. So to handle messages,
just sub-class it and add the message handlers you are interested in.
Here is an example, handling one message:
#include <ShellFolderView.h> // This one contains the base class
class CShellView : public CShellFolderViewImpl
{
public:
BEGIN_MSG_MAP(CShellView)
MESSAGE_HANDLER(SFVM_COLUMNCLICK, OnColumnClick)
END_MSG_MAP()
LRESULT OnColumnClick(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL &bHandled)
{
SendFolderViewMessage(SFVM_REARRANGE, wParam);
return S_OK;
}
The bHandled
parameters works like in standard message map, if
you didn't handle the message, put FALSE
in it (when entering your
function it is TRUE
). If you handled the message, the return value
(LRESULT
) will be returned from MessageSFVCB
. This
permit you to return any value as described in the SDK.
Sometimes your handler has to send messages to the view, this is done with
the method SendFolderViewMessage()
which will call internally
SHShellFolderView_Message()
.
To provide a standard view, you do not have to handle all the messages
described in the SDK. In my extension I handle only two messages and it works
great.
Explorer will request a ShellView by calling your
IShellFolder::CreateViewObject()
method, here is the code to create
a view of our class:
STDMETHODIMP CShellFolder::CreateViewObject(HWND hwndOwner,
REFIID riid, void** ppvOut)
{
if (riid == IID_IShellView)
{
CComObject<CShellView>* pViewObject;
hr = CComObject<CShellView>::CreateInstance(&pViewObject);
if (FAILED(hr))
return hr;
pViewObject->AddRef();
hr = pViewObject->Create((IShellView**)ppvOut, hwndOwner,
(IShellFolder*)this);
pViewObject->Release();
return hr;
}
return E_NOINTERFACE;
}
That's it!
Check the code of my extension, it contains some error checking and also a
mechanism to trace all the ShellView messages. This can be usefull to
investigate a little more.
Points of Interest
ATL builds
When creating an ATL project, the _ATL_MIN_CRT
symbol is
defined. This is to avoid linking with the standard CRT (the goal was to
minimize the executable file size). Because I use some CRT features, I did
remove it, but then I checked again to see what features from the CRT I used.
I didn't use much of them, mostly string and memory (str* and mem*)
functions. Also static objects. This brings me to using AtlAux which does
minimal CRT-like things and redirects string functions to Windows API. You can
find it here on CodeProject. For the memory functions, I simply got them from
the CRT sources and put them in stdafx.cpp
So here are the different configurations:
- MinSize makes use of _ATL_MIN_CRT with auto-redirects to non CRT APIs
(via AtlAux)
- MinDependencies makes use of the CRT (statically linked)
This shows you a sample which is non-CRT dependant. While testing this
under VC7 and thus ATL 7.0, I found that most of the MinCRT things gone away, so
take this as an example of what could be done. Note also that the MinSize builds
will dynamically link to ATL.DLL which is not found by default in the OS.
How to add the extension in the Places Bar of the File Dialog?
Take a
look at the picture at the top of this article. See that icon in the left pane
of the FileDialog? This is a shortcut icon that will act like selecting our
namespace icon in the upper ComboBox. But how to do that?
Everything lie in the registry, take a look at
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\comdlg32\PlacesBar
.
There, create a string value named 'PlaceN' where N is from 0 to 4. Set its
value to "::{E477F21A-D9F6-4B44-AD43-A95D622D2910}" which is our extension CLSID
prepended by two semicolons. This will do the trick. Further details can be
found here.
Conclusion
This is the first version of this
namespace extension, it implements basic features but it shows how to achieve
them, this was the goal of this article. At the time of writing I'm already
developing the second version which will cover folders and sub-folders. I'll
update this article once I've finished with it.
I wrote this article after finishing and not during the development, so I may
have loosed focus on some issues. Let me know if you miss some information or if
parts of this article are too obscure.
History