Introduction
Both MFC shell controls (CMFCShellTreeCtrl
and CMFCShellListCtrl
) have substantial limitations. With CMFCShellTreeCtrl
it’s not possible to set custom root folder, - tree view always starts with the “desktop”. As to CMFCShellListCtrl
there is no clear way of filtering files. The developer can obviously override EnumObjects but doing this in every derived class is not very efficient. Besides the developer cannot assign custom data to a CMFCShellTreeCtrl
or CMFCShellListCtrl
items. To overcome all this problems I had to develop the appropriate derived classes: CMFCShellTreeCtrlEx
and CMFCShellListCtrlEx
.
Background
First of all I had to extend AFX_SHELLITEMINFO
structure used by both shell controls by adding DWORD_PTR
dwItemData member to hold custom item data. The new AFX_SHELLITEMINFOEX
structure is defined in the new CMFCShellUtils
class header. To support custom item data in the shell tree view the following functions were added to CMFCShellTreeCtrlEx
:
DWORD_PTR GetItemDataEx(HTREEITEM hItem) const;
BOOL SetItemDataEx(HTREEITEM hItem, DWORD_PTR dwData);
Overridables
virtual void FreeItemData(HTREEITEM hItem, DWORD_PTR dwItemData);
virtual void OnItemInserted(HTREEITEM hItem, LPCTSTR szFolderPath);
Similarly the following functions were added to CMFCShellListCtrlEx
:
DWORD_PTR GetItemDataEx(int nItem) const;
BOOL SetItemDataEx(int nItem, DWORD_PTR dwData);
Overridables
virtual void FreeItemData(int nItem, DWORD_PTR dwItemData);
virtual void OnItemInserted(int nItem);
New CMFCShellTreeCtrlEx methods
Constructor:
CMFCShellTreeCtrlEx(DWORD dwProp = 0);
DWORD dwProp
Specifies additional shell tree view properties. This parameter can be a combination of the following flags:
SHELLTREEEX_QUICK_CHLDDETECT
– If this flag is set shell tree view will use FGAO_HASSUBFOLDER|SFGAO_REMOVABLE
folder attributes instead of SFGAO_HASSUBFOLDER|SFGAO_FILESYSANCESTOR
to detect whether current folder has subfolders. In this case shell tree view will look more like the one in Windows Explorer. In CMFCShellTreeCtrl
most of the items are originally expandable since nearly every folder has SFGAO_FILESYSANCESTOR
attribute.
SHELLTREEEX_KEEP_CHILDREN
– CMFCShellTreeCtrl
removes and recreates child items every time parent item is collapsed or expanded respectively. If this flag is set shell tree view operates more efficiently keeping previously inserted items intact. To avoid a situation where a new folder created outside of the current instance of CMFCShellTreeCtrlEx
class is not displayed in the tree view the developer can preserve the appropriate key accelerator (such as F5) to call CMFCShellTreeCtrlEx::RefreshEx
(see below).
SHELLTREEEX_EXPAND_ALL
– If this flag is set CMFCShellTreeCtrlEx
expands all folders and subfolders on creation. Ignored if a custom root folder has not been set.
void SetRootFolder(LPCTSTR szRootDir, BOOL bFullPath = FALSE, DWORD *pdwProp = NULL);
Sets custom root folder.
Parameters
LPCTSTR szRootDir
– Fully qualified folder path
BOOL bFullPath
– TRUE
to display fully qualified folder path (szRootDir
parameter) as root folder name, FALSE
otherwise;
DWORD *pdwProp
- Optional pointer to shell tree view properties (see CMFCShellTreeCtrlEx
constructor above)
void RefreshEx();
A replacement for CMFCShellTreeCtrl :Refresh
void SetFlagsEx(DWORD dwFlags, BOOL bRefresh);
A replacement for CMFCShellTreeCtrl :SetFlags
New CMFCShellListCtrlEx methods
BOOL CopyItems(const CMFCShellListCtrlEx& cSrcListCtrl, const CUIntArray& cItemPosArr);
Copies selected items from one shell list control to another.
Parameters
const CMFCShellListCtrlEx& cSrcListCtrl
–Reference to a source shell list control whose items are to be copied to the current shell list control.
const CUIntArray&
cItemPosArr
- Reference to a CUIntArray
containing indices of the items to be copied.
Return Value
TRUE
if the selected items have been successfully copied, FALSE
otherwise.
Overridables
virtual void PreEnumObjects(LPCTSTR szFolderPath);
Called before shell list control starts enumerating current folder items.
Parameters
LPCTSTR szFolderPath
– Fully qualified path of the current folder.
virtual BOOL IncludeItem(LPCTSTR szFileName);
Parameters
LPCTSTR szFileName
– Name of the item to be added to the shell list control. To construct a fully qualified item pathname use szFolderPath
parameter passed to PreEnumObjects
method (see above). For example, if szFileName
is “Default.aspx” and szFolderPath
is “C:\MyProjects\MyWebsite” the fully qualified item pathname will be “C:\MyProjects\MyWebsite\Default.aspx”
Returns TRUE if the item is to be included, FALSE otherwise.
Using the code
In many occasions we need two shell list controls to select certain files from a current folder or multiple folders. Normally the first list control would contain all available files and the second one - files selected by the user. Two buttons (“Add”, “Remove”) would accomplish the job. Instead of all available files the first list control could also contain “remaining” (that is "not selected") files. That’s how it was done in the old Visual SourceSafe. To me the second option is preferable. In this case the selected files are just moved from one list control to the other.
This demo project allows to select files from multiple folders. The list of files selected for each folder is remembered using SetItemDataEx method. The application also demonstrates the use of CMFCShellTreeCtrlEx::SetRootFolder
method.
The main dialog window (CMFCShellExtensionDlg) contains three shell controls represented by the following variables:
CMFCShellTreeCtrlEx m_cTreeCtrl;
CProjectListCtrl m_cListCtrlSel;
CProjectListCtrl m_cListCtrlRem;
CProjectListCtrl is a class derived from CMFCShellListCtrlEx. It supports the following file filtering modes:
enum LISTFILTER
{
LISTFILTER_ALL,
LISTFILTER_SELECTED,
LISTFILTER_REMAINING,
LISTFILTER_NONE
};
Initial filtering mode is passed to the CProjectListCtrl constructor:
CProjectListCtrl(LISTFILTER nFilter = LISTFILTER_NONE);
and can be changed by SetFilter function:
void SetFilter(LISTFILTER nFilter) { m_nFilter = nFilter; }
CMFCShellExtensionDlg stores all selected files in m_cProjFileMap variable:
CProjFilesArray m_cProjFileMap;
CProjFilesArray is a class derived from CMapStringToOb where CString contains full folder pathname and CObject is a CStringArray containing selected file names.
Shell controls initialized in CMFCShellExtensionDlg::OnInitDialog:
BOOL CMFCShellExtensionDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
SetIcon(m_hIcon, TRUE);
SetIcon(m_hIcon, FALSE);
CheckRadioButton(IDC_RADIO_DEFAULT, IDC_RADIO_CUSTOM, IDC_RADIO_DEFAULT);
m_cListCtrlSel.SetItemTypes(SHCONTF_NONFOLDERS);
m_cListCtrlRem.SetItemTypes(SHCONTF_NONFOLDERS);
m_cListCtrlSel.SetProjectFiles(&m_cProjFileMap);
m_cListCtrlRem.SetProjectFiles(&m_cProjFileMap);
m_cListCtrlSel.SetFilter(LISTFILTER_SELECTED);
m_cListCtrlRem.SetFilter(LISTFILTER_REMAINING);
HTREEITEM hParentItem = m_cTreeCtrl.GetRootItem();
m_cTreeCtrl.SelectItem(hParentItem);
m_cTreeCtrl.Expand(hParentItem, TVE_EXPAND);
return TRUE;
}
Every time the user selects a folder the selected shell tree node is assigned the appropriate cProjFileMap element value (a pointer to CStringArray) and both list controls get refreshed
void CMFCShellExtensionDlg::OnTvnSelchanged(NMHDR *pNMHDR, LRESULT *pResult)
{
HTREEITEM hItem = m_cTreeCtrl.GetSelectedItem();
CString cFolderPath;
if (m_cTreeCtrl.GetItemPath(cFolderPath, hItem))
{
CStringArray *pFilesArr = m_cProjFileMap.GetFiles(cFolderPath, TRUE);
m_cTreeCtrl.SetItemDataEx(hItem, (DWORD_PTR)pFilesArr);
m_cListCtrlSel.DisplayFolder(cFolderPath);
m_cListCtrlRem.DisplayFolder(cFolderPath);
}
}
// Here is GetFiles method
CStringArray *CProjFilesArray::GetFiles(LPCTSTR szFolderPath, BOOL bAddIfNotFound)
{
CStringArray *pArray = NULL;
BOOL bFound = Lookup(szFolderPath, (CObject*&)pArray);
if (!bFound && bAddIfNotFound)
{
pArray = new CStringArray;
SetAt(szFolderPath, pArray);
}
return pArray;
}
Whenever “Add to project files” or “Remove from project files” button is clicked the selected files moved from one list control to the other:
void CMFCShellExtensionDlg::CopyFiles(BOOL bDelete)
{
HTREEITEM hItem = m_cTreeCtrl.GetSelectedItem();
CString cFolderPath;
CStringArray *pFilesArr = (hItem && m_cTreeCtrl.GetItemPath(cFolderPath, hItem)) ?
m_cProjFileMap.GetFiles(cFolderPath, FALSE) : NULL;
if (!pFilesArr)
return;
CProjectListCtrl *pSrcList = bDelete ? &m_cListCtrlSel : &m_cListCtrlRem;
CProjectListCtrl *pDstList = bDelete ? &m_cListCtrlRem : &m_cListCtrlSel;
CUIntArray cItemPosArr;
POSITION pos = pSrcList->GetFirstSelectedItemPosition();
while (pos)
{
int nItem = pSrcList->GetNextSelectedItem(pos);
cItemPosArr.Add((UINT)nItem);
}
int nCount = cItemPosArr.GetSize();
if (nCount > 0)
{
pDstList->CopyItems((const CMFCShellListCtrlEx&)*pSrcList, cItemPosArr);
pos = pSrcList->GetFirstSelectedItemPosition();
while (pos)
{
int nItem = pSrcList->GetNextSelectedItem(pos);
pSrcList->DeleteItem(nItem);
pos = pSrcList->GetFirstSelectedItemPosition();
}
}
pFilesArr->RemoveAll();
nCount = m_cListCtrlSel.GetItemCount();
for (int i = 0; i < nCount; i++)
{
CString cFileName = m_cListCtrlSel.GetItemText(i, CMFCShellListCtrl::AFX_ShellList_ColumnName);
pFilesArr->Add(cFileName);
}
}
CMFCShellListCtrlEx::CopyItems method is show below:
BOOL CMFCShellListCtrlEx::CopyItems(const CMFCShellListCtrlEx& cSrcListCtrl, const CUIntArray& cItemPosArr)
{
if (!m_psfCurFolder)
return FALSE;
int nItemCount = GetItemCount();
BOOL bResult = TRUE;
for (int i = 0; i < cItemPosArr.GetSize(); i++)
{
int nItem = cItemPosArr[i];
LPAFX_SHELLITEMINFOEX pItem = (nItem >= 0 && nItem < cSrcListCtrl.GetItemCount()) ?
(LPAFX_SHELLITEMINFOEX)cSrcListCtrl.GetItemData(nItem) : NULL;
BOOL bRemove = pItem == NULL || pItem->pParentFolder == NULL;
if (!bRemove)
{
CString cItemName =
CMFCShellUtils::GetDisplayName(pItem->pParentFolder, pItem->pidlRel, FALSE);
for (int j = 0; j < nItemCount; j++)
{
CString cName = GetItemText(j, AFX_ShellList_ColumnName);
if (!cName.CompareNoCase(cItemName))
{
bRemove = TRUE;
break;
}
}
if (!bRemove)
m_cCopyNamesArr.Add(cItemName);
}
}
bResult = m_cCopyNamesArr.GetSize() > 0;
if (bResult)
{
CWaitCursor wait;
SetRedraw(FALSE);
bResult = SUCCEEDED(EnumObjects(m_psfCurFolder, m_pidlCurFQ));
m_cCopyNamesArr.RemoveAll();
if (bResult && (GetStyle() & LVS_REPORT))
Sort(AFX_ShellList_ColumnName);
SetRedraw(TRUE);
RedrawWindow();
}
return bResult;
}