Extending MFC shell controls functionality






4.11/5 (9 votes)
Overriding CMFCShellTreeCtrl and CMFCShellListCtrl classes
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
:
// Same as GetItemData in CTreeCtrl DWORD_PTR GetItemDataEx(HTREEITEM hItem) const; // Same as SetItemData in CTreeCtrl BOOL SetItemDataEx(HTREEITEM hItem, DWORD_PTR dwData);
Overridables
// Called to free custom data when shell tree item is deleted virtual void FreeItemData(HTREEITEM hItem, DWORD_PTR dwItemData); // Notifies derived class that a new item has been inserted virtual void OnItemInserted(HTREEITEM hItem, LPCTSTR szFolderPath);
Similarly the following functions were added to CMFCShellListCtrlEx
:
// Same as GetItemData in CListCtrl DWORD_PTR GetItemDataEx(int nItem) const; // Same as SetItemData in CListCtrl BOOL SetItemDataEx(int nItem, DWORD_PTR dwData);
Overridables
// Called to free custom data when shell list item is deleted virtual void FreeItemData(int nItem, DWORD_PTR dwItemData); // Notifies derived class that a new item has been inserted 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; // Shell tree view CProjectListCtrl m_cListCtrlSel; // Shell list control containing selected files CProjectListCtrl m_cListCtrlRem; // Shell list control containing remaining (not selected) files
CProjectListCtrl is a class derived from CMFCShellListCtrlEx. It supports the following file filtering modes:
enum LISTFILTER { LISTFILTER_ALL, // No filtering, all folder files are displayed in the list LISTFILTER_SELECTED, // Displays folder files selected by the user LISTFILTER_REMAINING, // Displays folder files not selected by the user (remaining files) LISTFILTER_NONE // Not displaying any files (empty list) };
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); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon CheckRadioButton(IDC_RADIO_DEFAULT, IDC_RADIO_CUSTOM, IDC_RADIO_DEFAULT); // Not displaying subfolders m_cListCtrlSel.SetItemTypes(SHCONTF_NONFOLDERS); m_cListCtrlRem.SetItemTypes(SHCONTF_NONFOLDERS); // Passing project files map pointer to both shell list controls m_cListCtrlSel.SetProjectFiles(&m_cProjFileMap); m_cListCtrlRem.SetProjectFiles(&m_cProjFileMap); // Setting list control filters m_cListCtrlSel.SetFilter(LISTFILTER_SELECTED); m_cListCtrlRem.SetFilter(LISTFILTER_REMAINING); // Selecting and expanding top (desktop) folder item 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)) { // Retrieving an existing (or adding a new: bAddIfNotFound = TRUE) project files array CStringArray *pFilesArr = m_cProjFileMap.GetFiles(cFolderPath, TRUE); // and using the resultant pointer as the selected tree item data m_cTreeCtrl.SetItemDataEx(hItem, (DWORD_PTR)pFilesArr); // Refreshing both list controls contents m_cListCtrlSel.DisplayFolder(cFolderPath); m_cListCtrlRem.DisplayFolder(cFolderPath); } }
// Here is GetFiles method
// Getting the list of selected files (a CStringArray object) corresponding to the folder // specified by szFolderPath parameters. // If bAddIfNotFound is TRUE and the map does not contain the required key // a new entry with an empty string array is added to the map. 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; // Moving files from one list control to the other 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) { // Copying selected files from source to destination list control pDstList->CopyItems((const CMFCShellListCtrlEx&)*pSrcList, cItemPosArr); // and removing them from the source list control pos = pSrcList->GetFirstSelectedItemPosition(); while (pos) { int nItem = pSrcList->GetNextSelectedItem(pos); pSrcList->DeleteItem(nItem); pos = pSrcList->GetFirstSelectedItemPosition(); } } // Updating project files map 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; // Check if non of the items to be copied is already in the list 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 item already exists => remove the appropriate m_cCopyNamesArr element if (!bRemove) m_cCopyNamesArr.Add(cItemName); } } bResult = m_cCopyNamesArr.GetSize() > 0; // If copy items array isn't empty... if (bResult) { CWaitCursor wait; SetRedraw(FALSE); // call EnumObjects to add new files to the list bResult = SUCCEEDED(EnumObjects(m_psfCurFolder, m_pidlCurFQ)); m_cCopyNamesArr.RemoveAll(); // and re-sort the list if (bResult && (GetStyle() & LVS_REPORT)) Sort(AFX_ShellList_ColumnName); SetRedraw(TRUE); RedrawWindow(); } return bResult; }