Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ShellFolderTree

0.00/5 (No votes)
14 May 2002 1  
Mimicking and extending the shell�s folder-tree control functionality

Sample Image - image002.jpg

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)
{
    // get item's pidl:

    CPIDL pidlFolder;
    if (FALSE == GetTreeItemAbsPIDL(hFolder, pidlFolder))
             return FALSE;
    if (pszFolderPathRet)
             ::SHGetPathFromIDList(pidlFolder, pszFolderPathRet);

    // get folder interface:

    IShellFolderPtr piFolder;
    HRESULT hr = m_piDesktopFolder->BindToObject(pidlFolder, NULL, 
                                      (IShellFolder), 
                                      reinterpret_cast<void**>(&piFolder));
    if (FAILED(hr))
        m_piDesktopFolder.QueryInterface(&piFolder);


    // enumerate objects in folder:

    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))
    {
        // pump messages

        while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
            ::DispatchMessage(&msg);

        // wrap the pidl, generate an absolute pidl:

        CPIDL* ppidlChild = new CPIDL(pidlNext);
        if (NULL == ppidlChild) return E_OUTOFMEMORY;
        CPIDL pidlAbsChild; 
        CPIDL::Concat(pidlFolder, *ppidlChild, pidlAbsChild);

        // get object info: icon, display name, attributes:

        sfi.iIcon = 0;
        ::SHGetFileInfo((LPCSTR)(LPCITEMIDLIST)pidlAbsChild, 0, 
                        &sfi, sizeof(SHFILEINFO), 
                        SHGFI_PIDL | SHGFI_DISPLAYNAME | SHGFI_ATTRIBUTES | 
                        (m_bHasIcons ? SHGFI_SYSICONINDEX | SHGFI_SMALLICON : 0));

        // fix odd bug where all bits get lit for control panel objects

        if (0xFFFFFFFF == sfi.dwAttributes)
            sfi.dwAttributes = 0;

        // determine if folder is empty or not:

        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);
            }
        }

        // ghosted image for hidden files, overlay image for shortcuts, shares:

        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;
        }

        // initialize new insert item:

        SFolderTreeInsertStruct ftis(hFolder);
        ftis.Set(ppidlChild, sfi.szDisplayName, sfi.dwAttributes, 
                 sfi.iIcon, sfi.iIcon, dwStyle);

        // store path too?

        if (bIncludePaths)
            ::SHGetPathFromIDList(pidlAbsChild, ftis.m_szPath);

        // store

        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)
{
    // get our context:

    CShellFolderTree* pThis = reinterpret_cast<CShellFolderTree*>(pvThis);

    // maps track the change-handles/tree items we're monitoring:

    M_HandleToTreeitem mapHdlToTi;
    M_TreeitemToHandle mapTiToHdl;

    // cycle

    for (;;)
    {
        // generate an array of handles to monitor:

        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);

        // wait efficiently for something to happen:

        DWORD dwWaitRet = ::MsgWaitForMultipleObjects(vecHandles.size(), 
                                                      &vecHandles[0], 
                                                      FALSE, INFINITE, 
                                                      QS_ALLINPUT);

        // what triggered?

        if (dwWaitRet >= WAIT_OBJECT_0 && 
            dwWaitRet < WAIT_OBJECT_0 + vecHandles.size())
        {
            // change notification:


            // refresh folder:

            M_HandleToTreeitem::iterator it = mapHdlToTi.find(vecHandles[dwWaitRet]);
            if (it != mapHdlToTi.end())
               pThis->RefreshFolderContents(it->second);

            // get next change notification for this folder:

            ::FindNextChangeNotification(vecHandles[dwWaitRet]);
        }
        else
        if (dwWaitRet == WAIT_OBJECT_0 + vecHandles.size())
        {
            // message in thread message queue:

            MSG msg;
            while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
            {
               switch (msg.message)
               {
               case WM_QUIT:
               case WM_USER_MONITOR_RESET:
                  {
                  // close any remaining change-notification handles:

                  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)
                   {
                      // add or remove folder monitor:

                      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!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here