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())
CWTLCabViewView();
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
{
CString sFilename;
CString sTempFilePath;
int nListIdx;
bool bPartialFile;
CString sCabName;
bool bCabMissing;
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:
CDragDropSource();
BEGIN_COM_MAP(CDragDropSource)
COM_INTERFACE_ENTRY(IDataObject)
COM_INTERFACE_ENTRY(IDropSource)
END_COM_MAP()
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:
- 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.
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 ( fEscapePressed )
return DRAGDROP_S_CANCEL;
else if ( !(grfKeyState & MK_LBUTTON) )
{
if ( DROPEFFECT_NONE == m_dwLastEffect )
return DRAGDROP_S_CANCEL;
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 ( fEscapePressed )
return DRAGDROP_S_CANCEL;
else if ( !(grfKeyState & MK_LBUTTON) )
{
if ( DROPEFFECT_NONE == m_dwLastEffect )
return DRAGDROP_S_CANCEL;
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;
if ( !m_view.GetDraggedFileInfo(vec) )
return 0;
if ( !dropsrc.Init(m_sCurrentCabFilePath, vec) )
return 0;
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)
{
hr = dropsrc.DoDragDrop(DROPEFFECT_COPY, &dwEffect);
if ( FAILED(hr) )
ATLTRACE("DoDragDrop() failed, error: 0x%08X\n", hr);
else
{
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
TCHAR
s 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()
:
#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();
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 ( 0 == nMRUID )
m_mru.AddToList ( szCabFilename );
else
m_mru.MoveToTop ( nMRUID );
}
else
{
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;
if ( !dropsrc.Init(m_sCurrentCabFilePath, vec) )
return 0;
hr = pdsh.CoCreateInstance ( CLSID_DragDropHelper );
if ( SUCCEEDED(hr) )
{
CComQIPtr<IDataObject> pdo;
if ( pdo = dropsrc.GetUnknown() )
pdsh->InitializeFromWindow ( m_view, &pnmlv->ptAction, pdo );
}
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 ( nCol == m_nSortedCol )
m_bSortAscending = !m_bSortAscending;
else
m_bSortAscending = true;
if ( g_bXPOrLater )
{
HDITEM hdi = { HDI_FORMAT };
CHeaderCtrl wndHdr = GetHeader();
if ( -1 != m_nSortedCol )
{
wndHdr.GetItem ( m_nSortedCol, &hdi );
hdi.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
wndHdr.SetItem ( m_nSortedCol, &hdi );
}
hdi.mask = HDI_FORMAT;
wndHdr.GetItem ( nCol, &hdi );
hdi.fmt |= m_bSortAscending ? HDF_SORTUP : HDF_SORTDOWN;
wndHdr.SetItem ( nCol, &hdi );
}
m_nSortedCol = nCol;
SortItems ( SortCallback, (LPARAM)(DWORD_PTR) this );
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()
{
if ( g_bXPOrLater )
{
SetExtendedListViewStyle ( LVS_EX_DOUBLEBUFFER,
LVS_EX_DOUBLEBUFFER );
SetView ( LV_VIEW_TILE );
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;
CComPtr<IImageList> m_TileIml;
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) )
{
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
{
}
}
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()
:
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 );
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)