Introduction
Some time ago I had need for a �Shell Folder Tree� control �
that is, a tree control that acted similarly to the tree in the left pane of an
Explorer shell window. While it is
technically possible to use the actual shell object in a user application, it�s
not easy. (For ideas, See Leon Finker�s
excellent example
of hosting a shell view.) Also, I
wanted my control to display files, not just folder objects.
Several implementations of such a control exist. Some are MFC, some are WTL, some are
ActiveX, and some are pure Win32. None
that I found had all of the features I desired - at a price I was willing to
pay ($0). So, as any C++ programmer
worth his salt would do, I wrote my own.
This article publishes that work, and discusses some of the more
interesting features. I cannot support
this code � if you use it in your application you�re fully responsible. But I would like to hear from you if you do
use it in a shipping application!
ShellControls is an ActiveX control library written in
ATL. It contains a single control
called �ShellFolderTree� (although I had originally conceived of a family of
�shell controls�, this is the only one I completed with any robustness
whatsoever). This version of the
library compiles with VC++ .NET and ATL 7.0.
It should be trivial to make it work in MSVC 6.0.
The original source was App-wizard generated as a standard ATL
Control. I asked the wizard to generate
support for Dual interfaces and Connection Points. Beyond that, all additional capability is hand-coded. You�ll also note that I tend to hand-tune
(and hand-reformat) the wizard code. My
personal preference is to separate the implementation from the definition, so
the majority of code lives in the .cpp file.
Before I get started, I�d like to credit some code I used in
the project written by Oz Solomonvitch.
In particular, I borrowed his CPIDL class, which he uses to implement
portions of his popular WndTabs addin for
Visual C++ 6.0.
The control is a �windowed only� control and contains a
Win32 tree control, encapsulated by the ATL CContainedWindow
class. The function that initializes the tree control,
and performs other initiation tasks dependant on the tree is CShellFolderTree::OnCreate
.
A couple things to take note of here:
- The
tree control is created with Windows styles that can be set by
manipulating properties on the control.
In this way, you can preset the look-and-feel of the control after
it is instantiated but before the container prompts it to create its
window.
- I
create a mutex that is particular to the instance of the control. This will be used to synchronize
operations against the control that are invoked from different threads.
- I
launch a background thread that will be used to monitor file-system
changes for folders that are visible in the tree.
- I
initialize the tree-control�s image list with an image list we loaded in
FinalConstruct
.
This list
contains the system-defined images for folders and many shell
objects. I also borrowed the code
for GetSystemImageList
from somewhere � but my apologies that I don�t
remember exactly who wrote it.
Kudos and thanks to whoever did.
It�s only necessary on Windows NT 4.0 and prior systems � later
versions of the OS have this functionality in an API.
- Lastly,
I enable drag-drop for the control (as defined by a property), and
initialize it with a specified folder, which by default is the Desktop
object.
The first interesting thing about the control is the code
that populates the tree. For speed, the
tree only ever populates folders that are expanded and visible. That is, a node (folder) that can be
expanded doesn�t actually populate until the user expands that folder for the
first time. For this reason, you may
notice a brief pause when a folder with many sub items is expanded. This is by design, and mimics the behavior
of how the �real� shell tree works in Explorer.
CShellFolderTree::AddFolderContents
is the method
responsible for the initial population of any given folder in the tree. (CShellFolderTree::RefreshFolderContents
is similar in function but is used once
a folder has been expanded once.) It is
called with a HTREEITEM
representing the node/folder to populate. Tree-items cache the relative PIDL (shell
Item ID List) that (usually) uniquely identifies the shell object for that node. A simple recursion through the tree-item�s
parents enables an absolute PIDL to be built.
The actual work is done by CShellFolderTree::BuildInsertList
. An
InsertList
is a STL vector of InsertStructs
, a datatype that derives from and
extends the system TVINSERTSTRUCT
. Once
the absolute PIDL is obtained for the shell object represented by the tree-item,
the code binds to the associated shell object and retrieves its IShellFolder
interface. IShellFolder
provides an
enumeration method, which retrieves an IEnumIDList
interface. Calling Next
on this interface enables the
code to visit each child PIDL.
HRESULT CShellFolderTree::BuildInsertList(HTREEITEM hFolder,
V_InsertStruct* pvecInserts,
char* pszFolderPathRet,
bool bIncludePaths)
{
CPIDL pidlFolder;
if (FALSE == GetTreeItemAbsPIDL(hFolder, pidlFolder))
return FALSE;
if (pszFolderPathRet)
::SHGetPathFromIDList(pidlFolder, pszFolderPathRet);
IShellFolderPtr piFolder;
HRESULT hr = m_piDesktopFolder->BindToObject(pidlFolder, NULL,
(IShellFolder),
reinterpret_cast<void**>(&piFolder));
if (FAILED(hr))
m_piDesktopFolder.QueryInterface(&piFolder);
IEnumIDListPtr piEnum;
hr = piFolder->EnumObjects(NULL, SHCONTF_FOLDERS |
(m_bShowHidden ? SHCONTF_INCLUDEHIDDEN : 0) |
(m_bShowFiles ? SHCONTF_NONFOLDERS : 0) ,
&piEnum);
if (FAILED(hr)) return hr;
INT iOverlayIndex(0);
DWORD dwStyle(0);
LPITEMIDLIST pidlNext;
SHFILEINFO sfi;
MSG msg;
while (S_OK == piEnum->Next(1, &pidlNext, NULL))
{
while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
::DispatchMessage(&msg);
CPIDL* ppidlChild = new CPIDL(pidlNext);
if (NULL == ppidlChild) return E_OUTOFMEMORY;
CPIDL pidlAbsChild;
CPIDL::Concat(pidlFolder, *ppidlChild, pidlAbsChild);
sfi.iIcon = 0;
::SHGetFileInfo((LPCSTR)(LPCITEMIDLIST)pidlAbsChild, 0,
&sfi, sizeof(SHFILEINFO),
SHGFI_PIDL | SHGFI_DISPLAYNAME | SHGFI_ATTRIBUTES |
(m_bHasIcons ? SHGFI_SYSICONINDEX | SHGFI_SMALLICON : 0));
if (0xFFFFFFFF == sfi.dwAttributes)
sfi.dwAttributes = 0;
if (sfi.dwAttributes & SFGAO_FOLDER &&
!(sfi.dwAttributes & SFGAO_HASSUBFOLDER))
{
char szPath[_MAX_PATH];
if (::SHGetPathFromIDList(pidlAbsChild, szPath))
{
lstrcat(szPath, "\\*.*");
BOOL bRet(FALSE);
WIN32_FIND_DATA fd;
HANDLE hff = ::FindFirstFile(szPath, &fd);
while (hff && fd.cFileName[0] == '.' &&
(bRet = ::FindNextFile(hff, &fd)));
if (bRet)
sfi.dwAttributes |= SFGAO_HASSUBFOLDER;
::FindClose(hff);
}
}
dwStyle = sfi.dwAttributes & SFGAO_GHOSTED ? TVIS_CUT : 0;
if (m_bHasIcons && m_bHasOverlayIcons)
{
dwStyle |= sfi.dwAttributes & SFGAO_LINK ?
INDEXTOOVERLAYMASK(2) : 0;
dwStyle |= sfi.dwAttributes & SFGAO_SHARE ?
INDEXTOOVERLAYMASK(1) : 0;
}
SFolderTreeInsertStruct ftis(hFolder);
ftis.Set(ppidlChild, sfi.szDisplayName, sfi.dwAttributes,
sfi.iIcon, sfi.iIcon, dwStyle);
if (bIncludePaths)
::SHGetPathFromIDList(pidlAbsChild, ftis.m_szPath);
pvecInserts->push_back(ftis);
}
return S_OK;
}
An old Win32 trick is used to maintain responsiveness of the
user application while this potentially lengthy enumeration takes place. In the loop that comprises the enumeration
we have a mini message pump that dispatches any queued up messages in real
time.
For each visited item we build an absolute PIDL and
determine some properties of the object.
What icon represents this item?
What is its display name? Is it
a folder? Is it hidden, etc.
This data is collected and cached in an InsertStruct
, which
is appended to the vector that will ultimately be returned.
Once completed, CShellFolderTree::AddFolderContents
traverses the vector and inserts the items.
Then it sorts the tree-node.
Why not just add the folder contents directly? Why use the InsertList
? The primary reason is to reduce the amount
of code and simplify the implementation.
Other methods also update the tree (see CShellFolderTree::RefreshFolderContents
)
and they all make use of CShellFolderTree::BuildInsertList
even though they
have different logic for ultimately inserting the items. Also, when I was writing the code, I was
experimenting with different sorting schemes; at one point sorting the vector
prior to insertion.
Another interesting aspect of the control is its ability to
monitor folders that are visible, on a background thread, and update the tree
in real time as the file-system undergoes changes.
The static CShellFolderTree::MonitorThreadProc
runs
efficiently in the background, using Win32 file-system change notification and ::MsgWaitForMultipleObjects
.
It also
processes any requests to begin or end monitoring a folder that arrive in the
thread message queue.
DWORD CShellFolderTree::MonitorThreadProc(LPVOID pvThis)
{
CShellFolderTree* pThis = reinterpret_cast<CShellFolderTree*>(pvThis);
M_HandleToTreeitem mapHdlToTi;
M_TreeitemToHandle mapTiToHdl;
for (;;)
{
typedef std::vector<HANDLE> V_Handle;
V_Handle vecHandles;
for (M_HandleToTreeitem::iterator it = mapHdlToTi.begin() ;
it != mapHdlToTi.end() ; it++)
vecHandles.push_back(it->first);
DWORD dwWaitRet = ::MsgWaitForMultipleObjects(vecHandles.size(),
&vecHandles[0],
FALSE, INFINITE,
QS_ALLINPUT);
if (dwWaitRet >= WAIT_OBJECT_0 &&
dwWaitRet < WAIT_OBJECT_0 + vecHandles.size())
{
M_HandleToTreeitem::iterator it = mapHdlToTi.find(vecHandles[dwWaitRet]);
if (it != mapHdlToTi.end())
pThis->RefreshFolderContents(it->second);
::FindNextChangeNotification(vecHandles[dwWaitRet]);
}
else
if (dwWaitRet == WAIT_OBJECT_0 + vecHandles.size())
{
MSG msg;
while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
switch (msg.message)
{
case WM_QUIT:
case WM_USER_MONITOR_RESET:
{
for (M_HandleToTreeitem::iterator it = mapHdlToTi.begin() ;
it != mapHdlToTi.end() ; it++)
::FindCloseChangeNotification(it->first);
mapHdlToTi.clear();
mapTiToHdl.clear();
if (WM_QUIT == msg.message)
::ExitThread(0);
}
break;
case WM_USER_MONITOR_FOLDER:
if (0x1 == pThis->m_bAutoUpdate)
{
CTreeLock lock(pThis);
pThis->MonitorFolder(mapHdlToTi, mapTiToHdl,
reinterpret_cast<HTREEITEM>(msg.wParam),
msg.lParam ? true : false);
}
break;
default:
break;
}
}
}
}
return 0;
}
When a tree-view item is expanded (CShellFolderTree::OnItemExpanded
), the node is refreshed, and a message is
posted to the file-system-monitoring thread, requesting that the corresponding
folder be monitored. If the file system
triggers a change-notification for that folder, the monitoring thread
automatically cycles and refreshes the node in the tree that represents the
folder.
When a tree-view item is collapsed, a message is posted to
the monitoring thread telling it to stop monitoring the corresponding folder.
The control supports various other features as well, and the
implementation of these is mostly straight forward. The most complex are the features that emulate the behavior of
the Explorer shell: double-clicking an
item invokes the default action on the object, right-clicking displays a
context menu for the object, shell-compatible drag-and-drop, etc.
Now, would anyone like to see a MC++ version for .NET?
Enjoy!