Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

Extending MFC shell controls functionality

4.11/5 (9 votes)
24 Jan 2016CPOL4 min read 27K   2.3K  
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 bFullPathTRUE 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;
}

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)