![]() |
Platforms, Frameworks & Libraries »
WTL »
Beginners
Beginner
WTL for MFC Programmers, Part X - Implementing a Drag and Drop SourceBy Michael DunnA tutorial on using drag and drop in your WTL application. |
VC6, VC7.1Win2K, WinXP, ATL, WTL, VS.NET2003, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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.
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.
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.
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.
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 ); };
CDragDropSource wraps the IDataObject management and drag/drop communication using
a few helper methods. A drag/drop operation follows this pattern:
vector<CDraggedFileInfo>.
CDragDropSource object and passes it that vector so it knows what files
to extract from the CAB.
CDragDropSource object extracts the files.
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.
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.
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.
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
TCRecentDocumentListBase.
t_cchItemLenTCHARs of the strings to be stored in the MRU items. This must be at least 6.
t_nFirstIDt_nLastIDt_nFirstID.
To add the MRU feature to our app, we need to follow a few steps:
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.
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.
CRecentDocumentList object to CMainFrame.
CMainFrame::Create().
WM_COMMAND messages where the command ID is between ID_FILE_MRU_FIRST and
ID_FILE_MRU_LAST inclusive.
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.
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.
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.
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.
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.
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:

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:
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 ); } }
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.
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.
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.
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.
June 16, 2006: Article first published.
Series Navigation: « Part IX (GDI Wrappers)
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 16 Jun 2006 Editor: Michael Dunn |
Copyright 2006 by Michael Dunn Everything else Copyright © CodeProject, 1999-2009 Web20 | Advertise on the Code Project |