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.

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:

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:

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:

The main frame is notified that the user is beginning a drag/drop operation.

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>.

The main frame creates a CDragDropSource object and passes it that vector so it knows what files
to extract from the CAB.

The main frame beings the drag/drop operation.

If the user drops on a suitable drop target, the CDragDropSource object extracts the files.

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.

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:

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;
elseif ( !(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;
elsereturn E_UNEXPECTED;
}
elsereturn 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:

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:

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 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:

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.

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.

Add a CRecentDocumentList object to CMainFrame.

Initialize the object in CMainFrame::Create().

Handle WM_COMMAND messages where the command ID is between ID_FILE_MRU_FIRST and
ID_FILE_MRU_LAST inclusive.

Update the MRU list when a CAB file is opened.

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():

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:

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:

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:

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:

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.

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:

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:

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():

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.

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

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.

Share

About the Author

Michael lives in sunny Mountain View, California. He started programming with an Apple //e in 4th grade, graduated from UCLA with a math degree in 1994, and immediately landed a job as a QA engineer at Symantec, working on the Norton AntiVirus team. He pretty much taught himself Windows and MFC programming, and in 1999 he designed and coded a new interface for Norton AntiVirus 2000.
Mike has been a a developer at Napster and at his own lil' startup, Zabersoft, a development company he co-founded with offices in Los Angeles and Odense, Denmark. Mike is now a senior engineer at VMware.

He also enjoys his hobbies of playing pinball, bike riding, photography, and Domion on Friday nights (current favorite combo: Village + double Pirate Ship). He would get his own snooker table too if they weren't so darn big! He is also sad that he's forgotten the languages he's studied: French, Mandarin Chinese, and Japanese.