MFC 程序员的 WTL 教程 ( 10 ) — 实现一个拖放源
链接:上一部分
内容简介拖放是许多流行应用的特性之一。尽管实现一个放下目标相当简单,但拖动源却要复杂的多。MFC 中有两个类
本文的示例工程是一个 CAB 文件查看器,可以使你从 CAB 中提取文件,只要把它们从查看器里拖到资源浏览器窗口中即可。本文还会讨论几个新的框架窗口话题,例如对文件打开的处理以及类似于 MFC 的文档/视图框架的数据管理。我还会演示 WTL 的 MRU(most-recently-used,最近使用)文件列表类,以及第六版的列表视图控件的几个新的 UI 特性。 重要提示:你需要从微软下载并安装 CAB SDK 来编译示例代码。在 KB 文章 Q310618 中有此 SDK 的链接。示例工程假定 SDK 位于和源代码相同的路径下的名为“cabsdk”的目录中。 记住,如果你在安装 WTL 或者编译示例代码时遇到了问题,请在这儿提问之前先阅读第一部分的 readme 一节。 开始工程要开始我们的 CAB 查看器应用,需要运行 WTL AppWizard 并创建一个名为 WTLCabView 的工程。它应该是一个 SDI 应用,所以在第一页中选择 SDI Application: 在下一页里,去掉 Command Bar,并把 View Type 改为 List View。向导会为我们的视图窗口创建一个派生自 视图窗口类看起来就是这样:
class CWTLCabViewView :
public CWindowImpl<CWTLCabViewView, CListViewCtrl>
{
public:
DECLARE_WND_SUPERCLASS(NULL, CListViewCtrl::GetWndClassName())
// Construction
CWTLCabViewView();
// Maps
BEGIN_MSG_MAP(CWTLCabViewView)
END_MSG_MAP()
// ...
};
就像我们在第二部分里使用视图类一样,我们可以使用
#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> >
{
//...
};
由于在 WTL 中没有文档/视图框架,视图类需要做双份的工作,既是 UI,也是存放有关 CAB 信息的地方。在拖放操作中传递的数据结构为
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)
{ }
};
视图类中还有如下方法:初始化、管理文件列表,以及在拖放操作开始的时候准备一个 文件打开处理要查看一个 CAB 文件,用户可以使用 File-Open 命令并选择一个 CAB 文件。向导为
BEGIN_MSG_MAP(CMainFrame)
COMMAND_ID_HANDLER_EX(ID_FILE_OPEN, OnFileOpen)
END_MSG_MAP()
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 );
}
void CMainFrame::ViewCab ( LPCTSTR szCabFilename )
{
if ( EnumCabContents ( szCabFilename ) )
m_sCurrentCabFilePath = szCabFilename;
}
如果 拖动源拖放源是一个 COM 对象,它实现了两个接口: 拖动源接口实现了我们的拖放源的 C++ 类为
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 );
};
用于调用者的辅助方法
步骤 3 到 6 由辅助方法处理。初始化在 bool Init(LPCTSTR szCabFilePath, vector<CDraggedFileInfo>& vec);
接下来的方法是 HRESULT DoDragDrop(DWORD dwOKEffects, DWORD* pdwEffect);
最后一个方法是 const vector<CDraggedFileInfo>& GetDragResults();
IDropSource 的方法第一个
STDMETHODIMP CDragDropSource::GiveFeedback(DWORD dwEffect)
{
m_dwLastEffect = dwEffect;
return DRAGDROP_S_USEDEFAULTCURSORS;
}
另一个
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;
}
当我们发现左键被释放了,就到了我们要从 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;
}
从查看器中拖放我们已经看过了实现拖放操作逻辑的类,现在,我们来看一下我们的查看器应用是怎样使用这个类的。当主框架窗口接收到
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;
}
第一个调用的是视图的 上面列出的步骤 6 提到了拖放结束后对 UI 的更新。因为有可能在 CAB 末尾的一个文件仅仅是部分存储于此 CAB 中,而剩余的则在后续的一个 CAB 里。(这在 Windows 9x 的安装文件里非常普遍,在那儿 CAB 需要能符合软盘的大小)当我们试图提取这样的一个文件时,CAB SDK 会告诉我们含有该文件剩余部分的 CAB 的名字。它还会在原始 CAB 所在的相同目录下寻找那个 CAB,如果存在的话则从中提取文件的剩余部分。 因为我们要在视图窗口中标示分块文件,所以
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;
}
我们调用 如果找不到后续的 CAB,应用会通过设置 出于安全考虑,应用把提取出的文件留在了 TEMP 目录里,而不是在拖放操作结束后立刻清除它们。在 加入 MRU 列表我们将要看到的另一个文档/视图风格的特性是最近使用文件列表。WTL 的 MRU 实现为一个模板类
template <class T, int t_cchItemLen = MAX_PATH,
int t_nFirstID = ID_FILE_MRU_FIRST,
int t_nLastID = ID_FILE_MRU_LAST> CRecentDocumentListBase
要把 MRU 特性添加到我们的应用里,需要做以下几步:
记住,如果 设置 MRU 对象第一步就是添加一个菜单项,以标明 MRU 项应该处于什么位置。通常是在 File 菜单下,这也是我们的应用用到的。下面就是占位菜单项: AppWizard 已经把
#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 );
// ...
}
前两个方法设置了我们想在维持的项的数目(缺省为 16)以及包含占位项的菜单句柄。 加载文件列表之后, 处理 MRU 命令并更新列表当用户选择了某个 MRU 项时,主框架会收到一个
BEGIN_MSG_MAP(CMainFrame)
COMMAND_RANGE_HANDLER_EX(
ID_FILE_MRU_FIRST, ID_FILE_MRU_LAST, OnMRUMenuItem)
END_MSG_MAP()
消息处理其从 MRU 对象处获取该项的全路径,然后调用
void CMainFrame::OnMRUMenuItem (
UINT uCode, int nID, HWND hwndCtrl )
{
CString sFile;
if ( m_mru.GetFromList ( nID, sFile ) )
ViewCab ( sFile, nID );
}
如上文提到的,我们要扩展 void ViewCab ( LPCTSTR szCabFilename, int nMRUID = 0 ); 如果
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 );
}
}
当 保存 MRU 列表在应用关闭时,我们把 MRU 列表保存回注册表中。这事简单,只需要一行: m_mru.WriteToRegistry ( APP_SETTINGS_KEY ); 这行放到了 其他 UI Goodies透明的拖动图像Windows 2000 及以后有一个内建的 COM 对象,叫做拖放助手,其目的是在拖放操作中提供良好的透明拖动图像。拖动源通过
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);
// ...
}
我们从创建此拖放助手 COM 对象开始。如果成功,我们就调用 要使 如果我们使用一些别的不处理 透明的选择矩形从 Windows XP 开始,列表视图控件可以显示一个透明的选择框。此特性缺省是关闭的,但通过给控件设置 如果没有显示出透明选择框,那就需要检查你的系统属性,确保下面的特性是启用了的: 标示排序的列在 Windows XP 及之后,详细信息模式的列表视图控件有一个被选中的列,以不同的背景颜色显示。这一特性通常用来标示那一列是被排序了的,而这也正是我们的 CAB 查看器要做的。标头控件也有了两个新的格式风格,使得在一列中标头可以显示一个向上或者向下的箭头。这通常用来显示排序的方向。 视图类在
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;
}
加亮代码的第一小节去除了先前的排序列的排序箭头。如果没有排序的列的话,就省略这一步。然后,把箭头添加到用户刚刚点击的列上。如果以升序排序则箭头朝上,降序则朝下。排序结束后,我们调用 以下是文件以大小排序后的列表控件的样子: 使用平铺视图模式在 Windows XP 及之后,列表视图控件还有一种新的风格称为平铺视图模式。作为视图窗口初始化的一部分,如果应用是运行于 XP 或之后上,它就会使用
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 );
}
}
设置平铺视图的图像列表在平铺视图模式下,我们会使用特大系统图形列表(在缺省的显示设置下图标为 48×48 大小)。我们使用 CImageList m_imlTiles; // the image list handle CComPtr<IImageList> m_TileIml; // COM interface on the image list 视图窗口在
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;
}
如果 使用平铺视图的图像列表由于列表控件并不能为平铺视图模式持有单独的图像列表,所以我们需要在运行时,当用户选择大图标或者平铺视图模式时改变图像列表。视图类有一个
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
}
}
如果控件即将进入平铺视图模式,那我们就把控件的图像列表设置为 48×48 的那个,否则设置为 32×32 的那个。 设置附加的文本行在初始化时,我们将平铺效果设置为要显示附加的两行文本。第一行总是项的文字,就像在大图标和小图标模式里一样。显示在两行附加的文本里的文字取自于子项,类似于详细信息模式下的列。我们可以为每个图标设置单独的子项。下面就是在
// 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 );
}
注意,附加行当你在详细信息模式下排序某列之后会改变。当使用 Copyright and licenseThis 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. 修订历史2006 年 6 月 16 日:首次发布 |
链接:上一部分

近期评论