Part X - Implementing a Drag and Drop Source
原作 :Michael Dunn
翻译 :yaker
内容
简介
支持拖放操作是很多现代程序的特性。虽然实现拖动源很直接,但是释放目标则要复杂得多。MFC 中的类 COleDataObject
和 COleDropSource
可以辅助管理拖动源所必须提供的数据,可是WTL中并没有提供这样的辅助类。对我们这些WTL用户来说,幸运的是: Raymond Chen 写了一篇MSDN文章 ("The Shell Drag/Drop Helper Object Part 2") ,文中提供了一个 IDataObject
的纯C++语言实现,这对于在WTL程序中实现拖放操作来说是一个巨大的帮助。
这篇文章的样例工程是一个CAB文件查看工具,它支持通过将文件从查看工具窗口拖动到windows文件夹窗口来实现解压操作。这篇文章也将讨论一些关于框架窗口的主题,比如处理 File-Open 操作和与MFC中文档视图框架类似的数据管理。我也将介绍 WTL的 MRU (最近经常使用,most-recently-used) 文件列表类,还有一些6.0版本列表视图空间的一些新特性。
注意: 你需要下载安装 Microsoft 的 CAB SDK才能编译样例代码。Microsoft的Konwledge Base网站中的一篇文章里有CAB SDK的链接: Q310618. 样例程序假定SDK被放置在源代码目录下名为"cabsdk"的目录里。
注意,如果你在安装WTL或者编译样例代码时遇到任何问题,在提问之前请阅读 第一部分里 readme 这一节
创建工程
现在开始创建我们的 CAB 查看器程序,运行WTL AppWizard 然后创建一个名为 WTLCabView 的工程。它是一个SDI(single document interface,单文档界面)应用程序,在第一页选择“SDI Application”:
下一页,取消选中 Command Bar ,然后将 View Type 改为 List View. 向导会为我们的视图窗口创建一个C++类,宾切它继承自 CListViewCtrl
类。
视图窗口类看起来像这样:
class CWTLCabViewView :
public CWindowImpl<CWTLCabViewView, CListViewCtrl>
{
public:
DECLARE_WND_SUPERCLASS(NULL, CListViewCtrl::GetWndClassName())
// Construction
CWTLCabViewView();
// Maps
BEGIN_MSG_MAP(CWTLCabViewView)
END_MSG_MAP()
// ...
};
和第二部分我们使用的视图窗口一样,我们可以使用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> >
{
//...
};
因为WTL不包含 文档/视图 框架,视图类要承担UI和保存CAB文件信息。拖放操作过程中操作的数据结构是 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)
{ }
};
视图类对于初始化,操作文件列表和在开始拖放操作时建立一个 CDraggedFileInfo
的列表相应的方法(函数)。我不想花费太多时间解释UI的内部工作原理,因为这篇文章是关于拖放操作的实现的,所以关于UI的部分请参考工程里的 WTLCabViewView.h 文件。
处理 File-Open 操作
想要查看一个CAB文件,用户可以使用 File-Open 命令,然后选择一个CAB文件。向导为 CMainFrame
生成的代码包含了处理 File-Open 菜单项的代码:
BEGIN_MSG_MAP(CMainFrame)
COMMAND_ID_HANDLER_EX(ID_FILE_OPEN, OnFileOpen)
END_MSG_MAP()
OnFileOpen()
使用了 CMyFileDialog
类,在 第四部分 中介绍的改进版的 CFileDialog
类,来显示一个标准的打开文件对话框。
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()
调用了 ViewCab()
的帮助函数:
void CMainFrame::ViewCab ( LPCTSTR szCabFilename )
{
if ( EnumCabContents ( szCabFilename ) )
m_sCurrentCabFilePath = szCabFilename;
}
EnumCabContents()
函数比较复杂,并且使用了 CAB SDK 调用来枚举 OnFileOpen()
里选中CAB文件中的内容,并且填充视图窗口。虽然目前 ViewCab()
的功能还不够,我们会逐渐添加代码来实现更多的功能。这里 CAB查看器 打开一个CAB文件时的效果:
EnumCabContents()
在视图类中使用了两个方法来填充UI: AddFile()
和 AddPartialFile()
。当一个文件部分存储于该CAB文件(其余的部分在另外的CAB文件内)时调用 AddPartialFile()
方法。上图所示的截图中,列表中的第一个文件就是部分存储于该CAB文件中。剩余的项使用 AddFile()
方法添加到视图窗口中。这两种方法都为添加的文件创建了同一种数据结构,所以视图能够获得它所显示的文件的细节信息。
如果 EnumCabContents()
返回值是 true,那说明枚举过程和UI建立都成功的执行。如果我们仅仅是想写个简单的CAB查看器,现在做的这些就已经足够了,但是程序就不会那么有趣了。要让这个工具变得真正易用起来,我们要为它添加拖放操作使得用户可以通过拖动来解压文件。
拖动源
拖动源是实现了以下两个接口的 COM对象: IDataObject
和 IDropSource
. IDataObject
用来存储拖放操作过程中客户端想要传输的所有数据;对我们来说就是一个 HDROP
结构,结构体里保存要从CAB文件里解压出来的文件列表 。OLE在拖放操作过程中调用 IDropSource
接口来通知事件的来源。
拖动源的接口
实现了拖动源的C++类是 CDragDropSource
. 它开始于 这篇MSDN文章 里描述的 IDataObject
的实现 ,简介里我们介绍了这篇文章。在那篇文章里你能找到关于这段代码的全部细节信息,这里我就不在赘述了。接下来我们向类中添加了 IDropSource
和它的两个方法:
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
使用了一些辅助方法包装了 IDataObject
的管理和拖放操作过程中的通信。一次拖放操作遵循以下模式:
- 用户开始一次拖放操作时主框架得到通知。
- 主框架调用视图窗口的方法来创建一个被拖动的文件的列表。视图窗口类使用一个
vector<CDraggedFileInfo>
结构返回这些信息。 - 主框架创建一个
CDragDropSource
对象并且把 vector<CDraggedFileInfo>传递给它,这样它就可以了解要从CAB里解压的文件的信息。 - 主框架开始拖放操作。
- 如果用户在一个适当的位置释放目标,
CDragDropSource
对象会解压缩相应的文件。 - 主框架更新UI来指出任何未能解压的文件。
第 3-6 步是通过辅助方法来实现的。初始化功能由 Init()
方法实现:
bool Init(LPCTSTR szCabFilePath, vector<CDraggedFileInfo>& vec);
Init()
会复制数据到受保护(protected)的成员变量里,填充到一个 HDROP
结构里,并且存储起来。Init()
所做的另外一项重要工作就是:它在临时目录为每个被拖放的文件创建了一个0比特的临时文件。举个例子,比如用户拖动了CAB文件内的 buffy.txt 和 willow.txt 两个文件, Init()
函数会在临时目录创建两个相应的同名文件。仅当释放目标验证了从HDROP
里读出的文件名的合法性之后才会产生这样的操作,如果文件不存在,释放操作会失败。
下一个要介绍的函数是 DoDragDrop()
:
HRESULT DoDragDrop(DWORD dwOKEffects, DWORD* pdwEffect);
DoDragDrop()
从参数 dwOKEffects
里获取了一系列 DROPEFFECT_*
标志位,说明了拖动源上允许进行的操作。它查询必要的借口,然后调用系统API DoDragDrop()
。若果拖放成功,*pdwEffect
被置为 DROPEFFECT_*
系列的值,该值正好反映了用户想做的操作。
最后一个方法是 GetDragResults()
:
const vector<CDraggedFileInfo>& GetDragResults();
CDragDropSource
对象维护了一个 vector<CDraggedFileInfo>
结构,在拖放操作过程中这个结构也被更新了。如果一个文件只是部分的存在于这个CAB文件中,或者解压缩错误,CDraggedFileInfo
都会被更新。主框架调用 GetDragResults()
来获取这个vector,所以它能够检查错误,并相应地更新UI。
IDropSource接口的方法
IDropSource
接口要提供的第一个方法是 GiveFeedback()
,它用来通知拖动源用户想要做的操作(移动,复制或者链接)。如果需要的话,拖动源也可以更改光标。CDragDropSource
跟踪用户操作,并且通知OLE使用默认的拖放图标。
STDMETHODIMP CDragDropSource::GiveFeedback(DWORD dwEffect)
{
m_dwLastEffect = dwEffect;
return DRAGDROP_S_USEDEFAULTCURSORS;
}
另外一个 IDropSource
方法是 QueryContinueDrag()
. 当用户移动光标的时候OLE调用这个方法,并且通知拖动源哪些鼠标键和键盘按键被按下。如下是多数 QueryContinueDrag()
实现所采用的样例代码。
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;
}
CDragDropSource::ExtractFilesFromCab()
是另外一段比较复杂的代码,它使用了 CAB SDK 来解压文件到临时目录,覆盖我们之前创建的0字节文件。QueryContinueDrag()
返回 DRAGDROP_S_DROP
时,它通知OLE完成拖放操作。如果释放目标是一个Windows资源浏览器窗口,Explorer会从临时目录复制文件到拖放操作的目标文件夹。
查看器里拖放操作的实现
我们已经说明了实现拖放逻辑的类,接下来让我们看一下查看器是如何使用这些类的。当主框架窗口接收到一个 LVN_BEGINDRAG
消息,它调用视图来获取一个被选中文件的列表,然后建立一个 CDragDropSource
对象:
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;
}
第一个调用的方法是视图的 GetDraggedFileInfo()
方法,用来获取被选择文件的列表。该方法返回一个 vector<CDraggedFileInfo>
结构,我们使用这个结构来初始化 CDragDropSource
对象。如果被选中的文件都不能解压缩(比如文件都部分的存储于该CAB中),GetDraggedFileInfo()
可能会失败。如果GetDraggedFileInfo()
失败, OnListBeginDrag()
也会失败并切不做任何操作直接返回。最后我们调用 DoDragDrop()
进行拖放操作,由 CDragDropSource
完成剩下的事情。
上面所提到的列表的第六步--即更新UI,在拖放操作之后完成。处于CAB压缩包末尾的文件可能只是部分的存储于该CAB中,剩下的部分在后面的CAB文件中。(这对于 Windows 9x 系列安装文件来说很普通,因为需要限制单个 CAB 文件的大小使得能够放入软盘中)。我们试图解压这样一个文件的时候,CAB SDK会告诉我们包含该文件剩余部分的CAB文件么名。它会在相同目录下寻找包含该文件的起始CAB文件,并且解压接下来的CAB文件(如果存在)。
当我们想要指出视图窗口中的部分存储文件的时候,,OnListBeginDrag()
检查拖放结果看是否有部分存储文件:
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;
}
我们调用 GetDragResults()
来获取更新过得 vector<CDraggedFileInfo>
结构,它反映了拖放操作的输出结果。如果成员变量 bPartialFile
被设置为 true
,那说明该文件部分存储于 CAB 文件中。我们使用 UpdateContinuedFile()
来处理剩下的工作,把相应的 CDraggedFileInfo 结构体传给它,使得它能够更新该文件相应的视图列表项目。下图说明了当程序指出一个文件部分的存储于该 CAB 中,并且显示出下一步分所在文件的情形:
如果后续 CAB 文件无法找到,程序会通过设置该项样式为 LVIS_CUT
表明该文件无法解压,同时图标变为灰色。
出于安全的考虑,程序将解压出的文件留在临时目录中,而不是拖放操作完成后立即清除它们。当 CDragDropSource::Init()
创建0字节文件的时候,它也把每个文件名添加到一个全局 vector g_vecsTempFiles
中。当主框架窗口关闭的时候临时文件才会被清除。
添加一个最近使用文件列表
下面我们要探讨的文档/视图样式特性就是一个最近使用文件列表(MRU)。WTL的MRU实现是一个模板类: CRecentDocumentListBase
. 如果你不需要重载默认MRU的任何行为(默认行为通常很重要),你可以使用派生类 CRecentDocumentList
.
CRecentDocumentListBase
模板类有如下参数:
template <class T, int t_cchItemLen = MAX_PATH,
int t_nFirstID = ID_FILE_MRU_FIRST,
int t_nLastID = ID_FILE_MRU_LAST> CRecentDocumentListBase
T
用来特化 CRecentDocumentListBase
的派生类名。
t_cchItemLen
要存在MRU列表中的项的长度,以 TCHAR
计。该项至少为6。
t_nFirstID
MRU项所使用的ID中的最小ID。
t_nLastID
MRU项所使用的ID中的最大ID。 该项必须大于 t_nFirstID
。
要为我们的程序加入MRU特性,只需要几步。
- 插入一个ID为
ID_FILE_MRU_FIRST
的菜单项。菜单项文字设置为若MRU列表是空时你希望显示的消息。 - 添加一个ID为
ATL_IDS_MRU_FILE
的字符串表(string table)。这个字符串表用来显示MRU项选中时的浮动提示。如果你使用 WTL AppWizard 来生成工程,该字符串默认已经创建。 - 向
CMainFrame
添加一个CRecentDocumentList
对象。 - 在
CMainFrame::Create()
里初始化这个对象。 - 处理ID在
ID_FILE_MRU_FIRST
和ID_FILE_MRU_LAST
之间的WM_COMMAND
消息。 - 打开一个CAB文件时更新MRU列表。
- 应用程序关闭时保存MRU列表。
另外,如果 ID_FILE_MRU_FIRST
and ID_FILE_MRU_LAST
对于你的程序来说不合适,你可以通过一个新的特化的 CRecentDocumentListBase
类来替换它们。
设置MRU对象
第一步是添加一个菜单项指明MRU列表的位置。通常将MRU文件列表放置于 File 菜单下,我们的程序里也是这么做的。菜单项的位置如下图所示:
WTL AppWizard already 添加了ID为 ATL_IDS_MRU_FILE
字符串到字符串表里,我们将它的内容修改为 "Open this CAB file"。接下来我们添加一个 CRecentDocumentList
成员变量到 CMainFrame
中,变量名是 m_mru
,然后在 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 );
// ...
}
前两个被调用的方法用于设置MRU中项的数目(默认值是16),并且将该成员变脸关联到菜单上。ReadFromRegistry()
从注册表中读取MRU列表。它接受我们传递的键,然后在相应位置创建一个新的键来保存列表。以我们的程序为例,键的值是 HKCU\Software\Mike's Classy Software\WTLCabView\Recent Document List
。
导入文件列表后, ReadFromRegistry()
调用另外一个 CRecentDocumentList
方法UpdateMenu()
,它查找MRU菜单项并且使实际的MRU项替代它的内容。
处理MRU命令并更新列表
当用户选中一个MRU项时,主框架窗口会收到一个 WM_COMMAND
消息,消息的command ID等于菜单项的ID。我们可以使用一条宏语句来处理整个消息映射。
BEGIN_MSG_MAP(CMainFrame)
COMMAND_RANGE_HANDLER_EX(
ID_FILE_MRU_FIRST, ID_FILE_MRU_LAST, OnMRUMenuItem)
END_MSG_MAP()
消息处理函数从MRU对象中获取选中项的完整路径,然后调用 ViewCab()
方法,这样应用程序就显示出该文件的内容。
void CMainFrame::OnMRUMenuItem (
UINT uCode, int nID, HWND hwndCtrl )
{
CString sFile;
if ( m_mru.GetFromList ( nID, sFile ) )
ViewCab ( sFile, nID );
}
正如前面提到的一样,我们扩展了 ViewCab()
方法使得它能够获取MRU对象的信息,并且更新MRU文件列表。ViewCab() 方法原型如下:
void ViewCab ( LPCTSTR szCabFilename, int nMRUID = 0 );
如果 nMRUID
值为 0,那么ViewCab()
方法是通过 OnFileOpen()
调用的。否则,就是用户选中MRU菜单项调用的,并且 nMRUID
的值为 OnMRUMenuItem()
所接收到的值。下面是更新后的代码:
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 );
}
}
如果 EnumCabContents()
没有失败,我们就根据选中该文件的不同情况来更新MRU列表。如果是通过 File-Open 选中的,我们调用 AddToList()
方法把文件添加到MRU列表中。如果是通过MRU菜单项选中的,我们使用 MoveToTop()
方法把它移动到列表的顶端。如果 EnumCabContents()
方法失败,我们要调用 RemoveFromList()
方法从列表中移除该文件。这些方法都会在内部调用 UpdateMenu()
方法,所以 File 菜单也会自动得到更新。
保存MRU列表
应用程序关闭时,我们保存MRU列表到注册表中。这个很简单,一行代码搞定:
m_mru.WriteToRegistry ( APP_SETTINGS_KEY );
这行代码在 CMainFrame
里与 WM_DESTROY
和 WM_ENDSESSION
对应的消息处理函数中调用。
其他的UI相关的东西
半透明的拖放效果
Windows 2000 以及后续版本的windows操作系统有一个内置的 COM 对象: drag/drop helper,用来在拖放操作过程中提供一个很好的半透明效果。拖动源可以通过 IDragSourceHelper
接口使用这个对象。下面是些额外的代码,加粗标记过,把它添加到 OnListBeginDrag()
方法来使用helper 对象:
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);
// ...
}
我们从创建drag/drop helper COM对象开始。如果成功了,我们调用 InitializeFromWindow()
方法并且传递三个参数:拖动源窗口的 HWND
句柄,光标的位置,以及一个 CDragDropSource
对象上的 IDataObject
接口。drag/drop helper 使用这个接口来存储它自己的数据,并且如果释放目标也使用了helper 对象,这些数据用来生成拖动图像。
为了使 InitializeFromWindow()
工作起来,拖动源窗口需要处理DI_GETDRAGIMAGE
消息,并且创建一个做为拖动图片的位图回应消息。幸运的是,列表视图控件支持这个特性,所以不需要太多工作就可以得到拖动图片。效果图如下图所示:
如果我们使用其他类型的窗口做为视图类,这种况口恰好不能处理 DI_GETDRAGIMAGE
消息,我们可以自己创建拖动图并调用 InitializeFromBitmap()
方法来存储到drag/drop helper对象中。
半透明的矩形选择框
从Windows XP开始,列表视图空间可以显示一个半透明的矩形选择覆盖框。这个特性是默认关闭的,可以通过在控件上设置 LVS_EX_DOUBLEBUFFER
属性来开启它。我们的程序在视图窗口初始化函数 CWTLCabViewView::Init()
里完成了这些工作。结果如下图说示。
如果半透明覆盖区域没有出现,检查你的系统是否开启了这个特性:
按列排序
Windows XP 以及之后的windows操作体统中,一个report 模式的列表视图控件可以拥有一个选中的列,用一种不同的背景色显示。这个特性通常用来指出列表按这个列进行了排序,我们的CAB查看器也是这么做的。头部空间也有两种样式,在列的顶端显示一个向上或者向下的箭头。这个通常用来显示排序的方向(从小到大或者从大到小)。
视图窗口通过响应 LVN_COLUMNCLICK
消息进行排序操作。下面用黑体高亮显示的代码用来按列排序。
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;
}
第一部分的高亮代码移除之前用作排序的列头部的箭头。如果之前没有列做为排序的依据,这一步被跳过。接下来,在用户单击过的列的顶端添加箭头。如果按升序排列则肩头向上,按降序排列箭头向下。排序完成之后,我们调用 SetSelectedColumn()
方法,它是 LVM_SETSELECTEDCOLUMN
消息的一个包装,用来将我们排序的列设置为选中状态。
按文件大小排序的情况如下图所示:
使用平铺视图模式
在Windows XP以及后续的windows操作系统中,列表视图空间有一种显得样式叫做 平铺视图模式. 做为视图窗口初始化的一部分,如果程序运行在XP级后续版本的系统上,会设置视图列表模式为平铺视图模式。 使用了 SetView()
方法(它是对 LVM_SETVIEW
消息的一个封装)。然后填充一个 LVTILEVIEWINFO
结构来设置空间的一些属性控制平铺过程。成员变量 cLines
被设置为2,在每个平铺视图图标的旁边显示两行文本。成员变量 dwFlags
被设置为 LVTVIF_AUTOSIZE
,使得控件能够自动缩放平铺区域。
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 );
}
}
设置平铺视图图像列表
对于平铺视图模式来说,我们使用了一个特大的系统图片列表 (默认显示设置下有 48x48 个图标 )。我们使用了 SHGetImageList()
API来获取这个图片列表。SHGetImageList()
不同于 SHGetFileInfo()
,它返回一个图片列表对象上的COM接口。视图窗口有两个成员变量用来管理这个图片列表:
CImageList m_imlTiles; // the image list handle
CComPtr<IImageList> m_TileIml; // COM interface on the image list
视图窗口将这个特大图片列表保存在 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;
}
如果 SHGetImageList()
操作成功,我们可以强制转换 IImageList*
接口为 HIMAGELIST
类型,然后像其他图片列表一样使用它。
使用平铺视图图片列表
因为列表控件没有为平铺视图模式生成一个单独的图片列表,我们需要当用户切换显示模式时动态改变视图列表。视图类有一个 SetViewMode()
方法,它用来处理切换视图列表和查看模式:
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
}
}
如果空间进入视图模式,我们设置控件的列表为48x48的那一个图片列表,否则设置为32x32的那个。
设置而外的几行文字
初始化过程中,我们建立平铺视图来显示额外的两行文本。第一行文本是项目名称,这一点和在大图标/小图标模式下一样。额外的两行显示的是子项内容,和report模式下的列接近。我们可以为每个项单独设置子项文本。下列代码说明了视图如何使用 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 );
}
aCols
数组包含了要显示的子项的数据,在这个例子中子项一是文件类型,子项二是文件大小。查看器如下图所示:
注意,在你按列排序列表之后这两行文本的内容会相应改变。当选中的列拥有 LVM_SETSELECTEDCOLUMN
样式的时候,子项的文本总是优先显示,覆盖了我们在 LVTILEINFO
结构中传递的子项文本。