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

WTL for MFC Programmers, Part X - Implementing a Drag and Drop Source

0.00/5 (No votes)
16 Jun 2006 2  
A tutorial on using drag and drop in your WTL application.

Contents

Introduction

Drag and drop is a feature of many modern applications. While implementing a drop target is rather straightforward, the drop source is much more complicated. MFC has the classes COleDataObject and COleDropSource that assist in managing the data that the source must provide, but WTL has no such helper classes. Fortunately for us WTL users, Raymond Chen wrote an MSDN article ("The Shell Drag/Drop Helper Object Part 2") back in 2000 that has a plain C++ implementation of IDataObject, which is a huge help in writing a complete drag and drop source for a WTL app.

This article's sample project is a CAB file viewer that lets you extract files from a CAB by dragging them from the viewer to an Explorer window. The article will also discuss some new frame window topics such as File-Open handling and data management analogous to the document/view framework in MFC. I'll also demonstrate WTL's MRU (most-recently-used) file list class, and some new UI features in the version 6 list view control.

Important: You will need to download and install the CAB SDK from Microsoft to compile the sample code. A link to the SDK is in KB article Q310618. The sample project assumes the SDK is in a directory called "cabsdk" that is in the same directory as the source files.

Remember, if you encounter any problems installing WTL or compiling the demo code, read the readme section of Part I before posting questions here.

Starting the Project

To begin our CAB viewer app, run the WTL AppWizard and create a project called WTLCabView. It will be an SDI app, so choose SDI Application on the first page:

On the next page, uncheck Command Bar, and change the View Type to List View. The wizard will create a C++ class for our view window, and it will derive from CListViewCtrl.

The view window class looks like this:

class CWTLCabViewView :
  public CWindowImpl<CWTLCabViewView, CListViewCtrl>
{
public:
  DECLARE_WND_SUPERCLASS(NULL, CListViewCtrl::GetWndClassName())
 
  // Construction

  CWTLCabViewView();
 
  // Maps

  BEGIN_MSG_MAP(CWTLCabViewView)
  END_MSG_MAP()
 
  // ...

};

As with the view window we used in Part II, we can set the default window styles using the third template parameter of CWindowImpl:

#define VIEW_STYLES \
  (LVS_REPORT | LVS_SHOWSELALWAYS | \
   LVS_SHAREIMAGELISTS | LVS_AUTOARRANGE )
#define VIEW_EX_STYLES (WS_EX_CLIENTEDGE)
 
class CWTLCabViewView :
  public CWindowImpl<CWTLCabViewView, CListViewCtrl,
                     CWinTraitsOR<VIEW_STYLES,VIEW_EX_STYLES> >
{
//...

};

Since there is no document/view framework in WTL, the view class will do double-duty as both the UI and the place where information about the CAB is held. The data structure passed around during a drag and drop operation is called CDraggedFileInfo:

struct CDraggedFileInfo
{
  // Data set at the beginning of a drag/drop:

  CString sFilename;      // name of the file as stored in the CAB

  CString sTempFilePath;  // path to the file we extract from the CAB

  int nListIdx;           // index of this item in the list ctrl

 
  // Data set while extracting files:

  bool bPartialFile;  // true if this file is continued in another cab

  CString sCabName;   // name of the CAB file

  bool bCabMissing;   // true if the file is partially in this cab and

                      // the CAB it's continued in isn't found, meaning

                      // the file can't be extracted

 
  CDraggedFileInfo ( const CString& s, int n ) :
    sFilename(s), nListIdx(n), bPartialFile(false),
    bCabMissing(false)
  { }
};

The view class also has methods for initialization, managing the list of files, and setting up a list of CDraggedFileInfo at the start of a drag and drop operation. I don't want to get too sidetracked on the inner workings of the UI, since this article is about drag and drop, so check out WTLCabViewView.h in the sample project for all the details.

File-Open Handling

To view a CAB file, the user uses the File-Open command and selects a CAB file. The wizard-generated code for CMainFrame includes a handler for the File-Open menu item:

  BEGIN_MSG_MAP(CMainFrame)
    COMMAND_ID_HANDLER_EX(ID_FILE_OPEN, OnFileOpen)
  END_MSG_MAP()

OnFileOpen() uses the CMyFileDialog class, the enhanced version of WTL's CFileDialog introduced in Part IX, to show a standard file open dialog.

void CMainFrame::OnFileOpen (
  UINT uCode, int nID, HWND hwndCtrl )
{
CMyFileDialog dlg ( true, _T("cab"), 0U,
                    OFN_HIDEREADONLY|OFN_FILEMUSTEXIST,
                    IDS_OPENFILE_FILTER, *this );
 
  if ( IDOK == dlg.DoModal(*this) )
    ViewCab ( dlg.m_szFileName );
}

OnFileOpen() calls the helper function ViewCab():

void CMainFrame::ViewCab ( LPCTSTR szCabFilename )
{
  if ( EnumCabContents ( szCabFilename ) )
    m_sCurrentCabFilePath = szCabFilename;
}

EnumCabContents() is rather complex, and uses the CAB SDK calls to enumerate the contents of the file that was selected in OnFileOpen() and fill the view window. While ViewCab() doesn't do much right now, we will add code to it later to support the MRU list. Here's what the viewer looks like when showing the contents of one of the Windows 98 CAB files:

EnumCabContents() uses two methods in the view class to fill the UI: AddFile() and AddPartialFile(). AddPartialFile() is called when a file is only partially stored in the CAB, because it began in a previous CAB. In the screen shot above, the first file in the list is a partial file. The remaining items were added with AddFile(). Both of these methods allocate a data structure for the file being added, so the view knows all the details about each file that it's showing.

If EnumCabContents() returns true, all of the enumeration and UI setup completed successfully. If we were writing a simple CAB viewer, we would be done, although the app wouldn't be all that interesting. To make it really useful, we'll add drag and drop support so the user can extract files from the CAB.

The Drag Source

A drag and drop source is a COM object that implements two interfaces: IDataObject and IDropSource. IDataObject is used to store whatever data the client wants to transfer during the drag and drop operation; in our case this data will be an HDROP struct that lists the files being extracted from the CAB. The IDropSource methods are called by OLE to notify the source of events during the drag and drop operation.

Drag Source Interfaces

The C++ class that implements our drop source is CDragDropSource. It begins with the IDataObject implementation from the MSDN article I mentioned in the introduction. You can find all the details about that code in the MSDN article, so I won't repeat them here. We then add IDropSource and its two methods to the class:

class CDragDropSource :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CDragDropSource>,
  public IDataObject,
  public IDropSource
{
public:
  // Construction

  CDragDropSource();
 
  // Maps

  BEGIN_COM_MAP(CDragDropSource)
    COM_INTERFACE_ENTRY(IDataObject)
    COM_INTERFACE_ENTRY(IDropSource)
  END_COM_MAP()
 
  // IDataObject methods not shown...

 
  // IDropSource

  STDMETHODIMP QueryContinueDrag (
                 BOOL fEscapePressed, DWORD grfKeyState );
  STDMETHODIMP GiveFeedback ( DWORD dwEffect );
};

Helper Methods for the Caller

CDragDropSource wraps the IDataObject management and drag/drop communication using a few helper methods. A drag/drop operation follows this pattern:

  1. The main frame is notified that the user is beginning a drag/drop operation.
  2. The main frame calls the view window to build a list of the files being dragged. The view returns this info in a vector<CDraggedFileInfo>.
  3. The main frame creates a CDragDropSource object and passes it that vector so it knows what files to extract from the CAB.
  4. The main frame beings the drag/drop operation.
  5. If the user drops on a suitable drop target, the CDragDropSource object extracts the files.
  6. The main frame updates the UI to indicate any files that could not be extracted.

Steps 3-6 are handled by helper methods. Initialization is done with the Init() method:

   bool Init(LPCTSTR szCabFilePath, vector<CDraggedFileInfo>& vec);

Init() copies the data into protected members, fills in an HDROP struct, and stores that struct in the data object with the IDataObject methods. Init() also does another important step: it creates a zero-byte file in the TEMP directory for each file being dragged. For example, if the user drags buffy.txt and willow.txt from a CAB file, Init() will make two files with those same names in the TEMP directory. This is done in case the drag target validates the filenames it reads from the HDROP; if the files were not present, the target might reject the drop.

The next method is DoDragDrop():

  HRESULT DoDragDrop(DWORD dwOKEffects, DWORD* pdwEffect);

DoDragDrop() takes a set of DROPEFFECT_* flags in dwOKEffects, indicating which actions the source will allow. It queries for the necessary interfaces, then calls the DoDragDrop() API. If the drag/drop succeeds, *pdwEffect is set to the DROPEFFECT_* value that the user wanted to perform.

The last method is GetDragResults():

  const vector<CDraggedFileInfo>& GetDragResults();

The CDragDropSource object maintains a vector<CDraggedFileInfo> that is updated as the drag/drop operation progresses. When a file is found that is continued in another CAB, or can't be extracted, the CDraggedFileInfo structs are updated as necessary. The main frame calls GetDragResults() to get this vector, so it can look for errors and update the UI accordingly.

IDropSource Methods

The first IDropSource method is GiveFeedback(), which notifies the source of what action the user wants to do (move, copy, or link). The source can also change the cursor if it wants to. CDragDropSource keeps track of the action, and tells OLE to use the default drag/drop cursors.

STDMETHODIMP CDragDropSource::GiveFeedback(DWORD dwEffect)
{
  m_dwLastEffect = dwEffect;
  return DRAGDROP_S_USEDEFAULTCURSORS;
}

The other IDropSource method is QueryContinueDrag(). OLE calls this method as the user moves the cursor around, and tells the source which mouse buttons and keys are pressed. Here is the boilerplate code that most QueryContinueDrag() implementations use:

STDMETHODIMP CDragDropSource::QueryContinueDrag (
    BOOL fEscapePressed, DWORD grfKeyState )
{
  // If ESC was pressed, cancel the drag.

  // If the left button was released, do drop processing.

  if ( fEscapePressed )
    return DRAGDROP_S_CANCEL;
  else if ( !(grfKeyState & MK_LBUTTON) )
    {
    // If the last DROPEFFECT we got in GiveFeedback()

    // was DROPEFFECT_NONE, we abort because the allowable

    // effects of the source and target don't match up.

    if ( DROPEFFECT_NONE == m_dwLastEffect )
      return DRAGDROP_S_CANCEL;
 
    // TODO: Extract files from the CAB here...

 
    return DRAGDROP_S_DROP;
    }
  else
    return S_OK;
}

When we see that the left button has been released, that's the point where we extract the selected files from the CAB.

STDMETHODIMP CDragDropSource::QueryContinueDrag (
    BOOL fEscapePressed, DWORD grfKeyState )
{
  // If ESC was pressed, cancel the drag.

  // If the left button was released, do the drop.

  if ( fEscapePressed )
    return DRAGDROP_S_CANCEL;
  else if ( !(grfKeyState & MK_LBUTTON) )
    {
    // If the last DROPEFFECT we got in GiveFeedback()

    // was DROPEFFECT_NONE, we abort because the allowable

    // effects of the source and target don't match up.

    if ( DROPEFFECT_NONE == m_dwLastEffect )
      return DRAGDROP_S_CANCEL;
 
    // If the drop was accepted, do the extracting here,

    // so that when we return, the files are in the temp dir

    // and ready for Explorer to copy.

    if ( ExtractFilesFromCab() )
      return DRAGDROP_S_DROP;
    else
      return E_UNEXPECTED;
    }
  else
    return S_OK;
}

CDragDropSource::ExtractFilesFromCab() is another complex bit of code that uses the CAB SDK to extract the files to the TEMP directory, overwriting the zero-byte files we created earlier. When QueryContinueDrag() returns DRAGDROP_S_DROP, that tells OLE to complete the drag/drop operation. If the drop target is an Explorer window, Explorer will copy the files from the TEMP directory into the folder where the drop happened.

Dragging and Dropping from the Viewer

Now that we've seen the class that implements the drag/drop logic, let's look at how our viewer app uses that class. When the main frame window receives an LVN_BEGINDRAG notification message, it calls the view to get a list of the selected files, and then sets up a CDragDropSource object:

LRESULT CMainFrame::OnListBeginDrag(NMHDR* phdr)
{
vector<CDraggedFileInfo> vec;
CComObjectStack<CDragDropSource> dropsrc;
DWORD dwEffect = 0;
HRESULT hr;
 
  // Get a list of the files being dragged (minus files

  // that we can't extract from the current CAB).

  if ( !m_view.GetDraggedFileInfo(vec) )
    return 0;   // do nothing

 
  // Init the drag/drop data object.

  if ( !dropsrc.Init(m_sCurrentCabFilePath, vec) )
    return 0;   // do nothing

 
  // Start the drag/drop!

  hr = dropsrc.DoDragDrop(DROPEFFECT_COPY, &dwEffect);
 
  return 0;
}

The first call is to the view's GetDraggedFileInfo() method to get the list of selected files. This method returns a vector<CDraggedFileInfo>, which we use to initialize the CDragDropSource object. GetDraggedFileInfo() may fail if all of the selected files are ones we know we can't extract (such as files that are partially stored in a different CAB file). If this happens, OnListBeginDrag() fails silently, and returns without doing anything. Finally, we call DoDragDrop() to start the operation, and let CDragDropSource handle the rest.

Step 6 in the list above mentioned updating the UI after the drag/drop is finished. It is possible for a file at the end of a CAB to be only partially stored in that CAB, with the rest being in a subsequent CAB. (This is quite common in the Windows 9x setup files, where the CABs are sized to fit on floppy disks.) When we try extracting such a file, the CAB SDK will tell us the name of the CAB that has the remainder of the file. It will also look for the CAB in the same directory as the initial CAB, and extract the rest of the file if the subsequent CAB is present.

Since we want to indicate partial files in the view window, OnListBeginDrag() checks the drag/drop results to see if any partial files were found:

LRESULT CMainFrame::OnListBeginDrag(NMHDR* phdr)
{
//...

  
  // Start the drag/drop!

  hr = dropsrc.DoDragDrop(DROPEFFECT_COPY, &dwEffect);
 
  if ( FAILED(hr) )
    ATLTRACE("DoDragDrop() failed, error: 0x%08X\n", hr);
  else
    {
    // If we found any files continued into other CABs, update the UI.

    const vector<CDraggedFileInfo>& vecResults = dropsrc.GetDragResults();
    vector<CDraggedFileInfo>::const_iterator it;
 
    for ( it = vecResults.begin(); it != vecResults.end(); it++ )
      {
      if ( it->bPartialFile )
        m_view.UpdateContinuedFile ( *it );
      }
    }
 
  return 0;
}

We call GetDragResults() to get an updated vector<CDraggedFileInfo> that reflects the outcome of the drag/drop operation. If the bPartialFile member of a struct is true, then that file was only partially in the CAB. We call the view method UpdateContinuedFile(), passing it the info struct, so it can update the file's list view item accordingly. Here's how the app indicates a partial file, when the subsequent CAB was found:

If the subsequent CAB cannot be found, the app indicates that the file can't be extracted by setting the LVIS_CUT style, so the icon appears ghosted:

To be on the safe side, the app leaves the extracted files in the TEMP directory, instead of cleaning them up immediately after the drag/drop is finished. As CDragDropSource::Init() is creating the zero-byte temp files, it also adds each file name to a global vector g_vecsTempFiles. The temp files are deleted when the main frame window closes.

Adding an MRU List

The next doc/view-style feature we'll look at is a most-recently-used file list. WTL's MRU implementation is the template class CRecentDocumentListBase. If you don't need to override any of the default MRU behavior (and the defaults are usually sufficient), you can use the derived class CRecentDocumentList.

The CRecentDocumentListBase template has these parameters:

template <class T, int t_cchItemLen = MAX_PATH,
          int t_nFirstID = ID_FILE_MRU_FIRST,
          int t_nLastID = ID_FILE_MRU_LAST> CRecentDocumentListBase
T
The name of the derived class that is specializing CRecentDocumentListBase.
t_cchItemLen
The length in TCHARs of the strings to be stored in the MRU items. This must be at least 6.
t_nFirstID
The lowest ID in the range of IDs to use for the MRU items.
t_nLastID
The highest ID in the range of IDs to use for the MRU items. This must be greater than t_nFirstID.

To add the MRU feature to our app, we need to follow a few steps:

  1. Insert a menu item with ID ID_FILE_MRU_FIRST in the place that we want the MRU items to appear. This item's text will be shown if the MRU list is empty.
  2. Add a string table entry with ID ATL_IDS_MRU_FILE. This string is used for the flyby help when an MRU item is selected. If you use the WTL AppWizard, this string is already created for you.
  3. Add a CRecentDocumentList object to CMainFrame.
  4. Initialize the object in CMainFrame::Create().
  5. Handle WM_COMMAND messages where the command ID is between ID_FILE_MRU_FIRST and ID_FILE_MRU_LAST inclusive.
  6. Update the MRU list when a CAB file is opened.
  7. Save the MRU list when the app closes.

Remember that you can always change the ID range if ID_FILE_MRU_FIRST and ID_FILE_MRU_LAST are unsuitable for your app, by making a new specialization of CRecentDocumentListBase.

Setting Up the MRU Object

The first step is to add a menu item that indicates where the MRU items will go. The usual place is the File menu, and that's what we'll use in our app. Here's our placeholder menu item:

The AppWizard already added the string ATL_IDS_MRU_FILE to our string table; we'll change it to read "Open this CAB file". Next, we add a CRecentDocumentList member variable to CMainFrame called m_mru, and initialize it in OnCreate():

#define APP_SETTINGS_KEY \
    _T("software\\Mike's Classy Software\\WTLCabView");
 
LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
HWND hWndToolBar = CreateSimpleToolBarCtrl(...);
 
  CreateSimpleReBar ( ATL_SIMPLE_REBAR_NOBORDER_STYLE );
  AddSimpleReBarBand ( hWndToolBar );
 
  CreateSimpleStatusBar();
 
  m_hWndClient = m_view.Create ( m_hWnd, rcDefault );
  m_view.Init();
 
  // Init MRU list

CMenuHandle mainMenu = GetMenu();
CMenuHandle fileMenu = mainMenu.GetSubMenu(0);
 
  m_mru.SetMaxEntries(9);
  m_mru.SetMenuHandle ( fileMenu );
  m_mru.ReadFromRegistry ( APP_SETTINGS_KEY );
 
  // ...

}

The first two methods set the number of items we want in the MRU (the default is 16), and the menu handle that contains the placeholder item. ReadFromRegistry() reads the MRU list from the registry. It takes the key name we pass it, and creates a new key under it to hold the list. In our case, the key is HKCU\Software\Mike's Classy Software\WTLCabView\Recent Document List.

After loading the file list, ReadFromRegistry() calls another CRecentDocumentList method, UpdateMenu(), which finds the placeholder menu item and replaces it with the actual MRU items.

Handling MRU Commands and Updating the List

When the user selects an MRU item, the main frame receives a WM_COMMAND message with the command ID equal to the menu item ID. We can handle these commands with one macro in the message map:

  BEGIN_MSG_MAP(CMainFrame)
    COMMAND_RANGE_HANDLER_EX(
        ID_FILE_MRU_FIRST, ID_FILE_MRU_LAST, OnMRUMenuItem)
  END_MSG_MAP()

The message handler gets the full path of the item from the MRU object, then calls ViewCab() so the app shows the contents of that file.

void CMainFrame::OnMRUMenuItem (
  UINT uCode, int nID, HWND hwndCtrl )
{
CString sFile;
 
  if ( m_mru.GetFromList ( nID, sFile ) )
    ViewCab ( sFile, nID );
}

As mentioned earlier, we'll expand ViewCab() to be aware of the MRU object, and update the file list as necessary. The new prototype is:

  void ViewCab ( LPCTSTR szCabFilename, int nMRUID = 0 );

If nMRUID is 0, then ViewCab() is being called from OnFileOpen(). Otherwise, the user selected one of the MRU menu items, and nMRUID is the command ID that OnMRUMenuItem() received. Here's the updated code:

void CMainFrame::ViewCab ( LPCTSTR szCabFilename, int nMRUID )
{
  if ( EnumCabContents ( szCabFilename ) )
    {
    m_sCurrentCabFilePath = szCabFilename;
 
    // If this CAB file was already in the MRU list,

    // move it to the top of the list. Otherwise,

    // add it to the list.

    if ( 0 == nMRUID )
      m_mru.AddToList ( szCabFilename );
    else
      m_mru.MoveToTop ( nMRUID );
    }
  else
    {
    // We couldn't read the contents of this CAB file,

    // so remove it from the MRU list if it was in there.

    if ( 0 != nMRUID )
      m_mru.RemoveFromList ( nMRUID );
    }
}

When EnumCabContents() succeeds, we update the MRU differently depending on how the CAB file was selected. If it was selected with File-Open, we call AddToList() to add the filename to the MRU list. If it was selected with an MRU menu item, we move that item to the top of the list with MoveToTop(). If EnumCabContents() fails, we remove the filename from the MRU list with RemoveFromList(). All of those methods call UpdateMenu() internally, so the File menu will be updated automatically.

Saving the MRU List

When the app closes, we save the MRU list back to the registry. This is simple, and just takes one line:

  m_mru.WriteToRegistry ( APP_SETTINGS_KEY );

This line goes in the CMainFrame handlers for the WM_DESTROY and WM_ENDSESSION messages.

Other UI Goodies

Transparent Drag Images

Windows 2000 and later have a built-in COM object called the drag/drop helper, whose purpose is to provide a fancy transparent drag image during drag/drop operations. The drag source uses this object via the IDragSourceHelper interface. Here is the additional code, indicated in bold, we add to OnListBeginDrag() to use the helper object:

LRESULT CMainFrame::OnListBeginDrag(NMHDR* phdr)
{
NMLISTVIEW* pnmlv = (NMLISTVIEW*) phdr;
CComPtr<IDragSourceHelper> pdsh;
vector<CDraggedFileInfo> vec;
CComObjectStack<CDragDropSource> dropsrc;
DWORD dwEffect = 0;
HRESULT hr;
 
  if ( !m_view.GetDraggedFileInfo(vec) )
    return 0;   // do nothing

 
  if ( !dropsrc.Init(m_sCurrentCabFilePath, vec) )
    return 0;   // do nothing

 
  // Create and init a drag source helper object

  // that will do the fancy drag image when the user drags

  // into Explorer (or another target that supports the

  // drag/drop helper interface).

  hr = pdsh.CoCreateInstance ( CLSID_DragDropHelper );
 
  if ( SUCCEEDED(hr) )
    {
    CComQIPtr<IDataObject> pdo;
 
    if ( pdo = dropsrc.GetUnknown() )
      pdsh->InitializeFromWindow ( m_view, &pnmlv->ptAction, pdo );
    }
 
  // Start the drag/drop!

  hr = dropsrc.DoDragDrop(DROPEFFECT_COPY, &dwEffect);
 
  // ...

}

We start by creating the drag/drop helper COM object. If that succeeds, we call InitializeFromWindow() and pass three parameters: the HWND of the drag source window, the cursor location, and an IDataObject interface on our CDragDropSource object. The drag/drop helper uses this interface to store its own data, and if the drag target also uses the helper object, that data is used to generate the drag image.

For InitializeFromWindow() to work, the drag source window needs to handle the DI_GETDRAGIMAGE message, and in response to that message, create a bitmap to be used as the drag image. Fortunately for us, the list view control supports this feature, so we get the drag image with very little work. Here's what the drag image looks like:

If we were using some other window as our view class, one that didn't handle DI_GETDRAGIMAGE, we would create the drag image ourselves and call InitializeFromBitmap() to store the image in the drag/drop helper object.

Transparent Selection Rectangle

Starting with Windows XP, the list view control can display a transparent selection marquee. This is turned off by default, but it can be enabled by setting the LVS_EX_DOUBLEBUFFER style on the control. Our app does this as part of the view window initialization in CWTLCabViewView::Init(). Here's the result:

If the transparent marquee isn't showing up for you, check your system properties and make sure the feature is enabled there:

Indicating the Sorted Column

On Windows XP and later, a list view control in report mode can have a selected column, which is shown with a different background color. This feature is normally used to indicate that a column is being sorted, and this is what our CAB viewer does. The header control also has two new formatting styles that make the header show an up- or down-pointing arrow in a column. This is normally used to show the direction of the sort.

The view class handles sorting in the LVN_COLUMNCLICK handler. The code for showing the sorted column is highlighted in bold:

LRESULT CWTLCabViewView::OnColumnClick ( NMHDR* phdr )
{
int nCol = ((NMLISTVIEW*) phdr)->iSubItem;
 
  // If the user clicked the column that is already sorted,

  // reverse the sort direction. Otherwise, go back to

  // ascending order.

  if ( nCol == m_nSortedCol )
    m_bSortAscending = !m_bSortAscending;
  else
    m_bSortAscending = true;
 
  if ( g_bXPOrLater )
    {
    HDITEM hdi = { HDI_FORMAT };
    CHeaderCtrl wndHdr = GetHeader();
 
    // Remove the sort arrow indicator from the

    // previously-sorted column.

    if ( -1 != m_nSortedCol )
      {
      wndHdr.GetItem ( m_nSortedCol, &hdi );
      hdi.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
      wndHdr.SetItem ( m_nSortedCol, &hdi );
      }
 
    // Add the sort arrow to the new sorted column.

    hdi.mask = HDI_FORMAT;
    wndHdr.GetItem ( nCol, &hdi );
    hdi.fmt |= m_bSortAscending ? HDF_SORTUP : HDF_SORTDOWN;
    wndHdr.SetItem ( nCol, &hdi );
    }
 
  // Store the column being sorted, and do the sort

  m_nSortedCol = nCol;
 
  SortItems ( SortCallback, (LPARAM)(DWORD_PTR) this );
 
  // Indicate the sorted column.

  if ( g_bXPOrLater )
    SetSelectedColumn ( nCol );
 
  return 0;
}

The first section of highlighted code removes the sort arrow from the previously-sorted column. If there was no sorted column, this part is skipped. Then, the arrow is added to the column that the user just clicked on. The arrow points up if the sort is ascending, or down if the sort is descending. After the sort is done, we call SetSelectedColumn(), a wrapper around the LVM_SETSELECTEDCOLUMN message, to set the selected column to the column we just sorted.

Here's how the list control appears when the files are sorted by size:

Using Tile View Mode

On Windows XP and later, the list view control has a new style called tile view mode. As part of the view window's initialization, if the app is running on XP or later, it sets the list view mode to tile mode using SetView() (a wrapper for the LVM_SETVIEW message). It then fills in a LVTILEVIEWINFO struct to set some properties that control how the tiles are drawn. The cLines member is set to 2, meaning 2 additional lines of text will appear beside each tile. The dwFlags member is set to LVTVIF_AUTOSIZE, which makes the control resize the tile area as the control itself is resized.

void CWTLCabViewView::Init()
{
  // ...

 
  // On XP, set some additional properties of the list ctrl.

  if ( g_bXPOrLater )
    {
    // Turning on LVS_EX_DOUBLEBUFFER also enables the

    // transparent selection marquee.

    SetExtendedListViewStyle ( LVS_EX_DOUBLEBUFFER,
                               LVS_EX_DOUBLEBUFFER );
 
    // Default to tile view.

    SetView ( LV_VIEW_TILE );
 
    // Each tile will have 2 additional lines (3 lines total).

    LVTILEVIEWINFO lvtvi = { sizeof(LVTILEVIEWINFO),
                             LVTVIM_COLUMNS };
 
    lvtvi.cLines = 2;
    lvtvi.dwFlags = LVTVIF_AUTOSIZE;
    SetTileViewInfo ( &lvtvi );
    }
}

Setting up the tile view image list

For tile view mode, we'll use the extra-large system image list (which has 48x48 icons in the default display settings). We get this image list using the SHGetImageList() API. SHGetImageList() is different from SHGetFileInfo() in that it returns a COM interface on an image list object. The view window has two member variables for managing this image list:

  CImageList m_imlTiles;         // the image list handle

  CComPtr<IImageList> m_TileIml; // COM interface on the image list

The view window gets the extra-large image list in InitImageLists():

HRESULT (WINAPI* pfnGetImageList)(int, REFIID, void**);
HMODULE hmod = GetModuleHandle ( _T("shell32") );
 
  (FARPROC&) pfnGetImageList = GetProcAddress(hmod, "SHGetImageList");
 
  hr = pfnGetImageList ( SHIL_EXTRALARGE, IID_IImageList,
                         (void**) &m_TileIml );
 
  if ( SUCCEEDED(hr) )
    {
    // HIMAGELIST and IImageList* are interchangeable,

    // so this cast is OK.

    m_imlTiles = (HIMAGELIST)(IImageList*) m_TileIml;
    }

If SHGetImageList() succeeds, we can cast the IImageList* interface to an HIMAGELIST and use it just like any other image list.

Using the tile view image list

Since the list control doesn't have a separate image list for tile view mode, we need to change the image list at runtime when the user chooses large icon or tile view mode. The view class has a SetViewMode() method that handles changing the image list and the view styles:

void CWTLCabViewView::SetViewMode ( int nMode )
{
  if ( g_bXPOrLater )
    {
    if ( LV_VIEW_TILE == nMode )
      SetImageList ( m_imlTiles, LVSIL_NORMAL );
    else
      SetImageList ( m_imlLarge, LVSIL_NORMAL );
 
    SetView ( nMode );
    }
  else
    {
    // omitted - no image list changing necessary on

    // pre-XP, just modify window styles

    }
}

If the control is going into tile view mode, we set the control's image list to the 48x48 one, otherwise we set it to the 32x32 one.

Setting the additional lines of text

During initialization, we set up the tiles to show two additional lines of text. The first line is always the item text, just as in the large icon and small icon modes. The text shown in the two additional lines are taken from subitems, similarly to the columns in report mode. We can set the subitems for each tile individually. Here is how the view sets the text in AddFile():

  // Add a new list item.

int nIdx;
 
  nIdx = InsertItem ( GetItemCount(), szFilename, info.iIcon );
  SetItemText ( nIdx, 1, info.szTypeName );
  SetItemText ( nIdx, 2, szSize );
  SetItemText ( nIdx, 3, sDateTime );
  SetItemText ( nIdx, 4, sAttrs );
 
  // On XP+, set up the additional tile view text for the item.

  if ( g_bXPOrLater )
    {
    UINT aCols[] = { 1, 2 };
    LVTILEINFO lvti = { sizeof(LVTILEINFO), nIdx,
                        countof(aCols), aCols };
 
    SetTileInfo ( &lvti );
    }

The aCols array holds the subitems whose text should be shown, in this case we show subitem 1 (the file type) and 2 (the file size). Here's what the viewer looks like in tile view mode:

Note that the additional lines will change after you sort a column in report mode. When a selected column is set with LVM_SETSELECTEDCOLUMN, that subitem's text is always shown first, overriding the subitems we passed in the LVTILEINFO struct.

Copyright and license

This article is copyrighted material, ©2006 by Michael Dunn. I realize this isn't going to stop people from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation, I would just like to be aware of the translation so I can post a link to it here.

The demo code that accompanies this article is released to the public domain. I release it this way so that the code can benefit everyone. (I don't make the article itself public domain because having the article available only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are benefitting from my code) but is not required. Attribution in your own source code is also appreciated but not required.

Revision History

June 16, 2006: Article first published.

Series Navigation: « Part IX (GDI Wrappers)

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