# Part X - Implementing a Drag and Drop Source
原作 :[**Michael Dunn**](http://www.codeproject.com/wtl/WTL4MFC10.asp)
翻译 :[yaker](http://www.yakergong.com/blog)
## 内容
* [简介](#intro)
* [创建工程](#starting)
* [处理 File-Open 操作](#fileopen)
* [拖动源](#dragsource)
* [拖动源的接口](#dragsrcitf)
* [调用者的辅助方法](#dragsrchelpers)
* [IDropSource接口的方法](#IDropSource)
* [查看器里拖放操作的实现](#appdragdrop)
* [添加一个最近使用文件列表](#mru)
* [设置MRU对象](#mrusetup)
* [处理MRU命令并更新列表](#handlingmru)
* [保存MRU列表](#savemru)
* [其他的UI相关的东西](#otherui)
* [半透明的拖放效果](#dragimage)
* [半透明的矩形选择框](#alphamarquee)
* [按列排序](#lvsortcol)
* [使用平铺视图模式](#lvtilemode)
* [设置平铺视图图像列表](#tileimglistsetup)
* [使用平铺视图图片列表](#tileimglistusing)
* [设置而外的几行文字](#tileaddllines)
* [版权与协议](#copying)
* [修订历史](#revisionhistory)
## 简介
支持拖放操作是很多现代程序的特性。虽然实现拖动源很直接,但是释放目标则要复杂得多。MFC 中的类 `COleDataObject` 和 `COleDropSource` 可以辅助管理拖动源所必须提供的数据,可是WTL中并没有提供这样的辅助类。对我们这些WTL用户来说,幸运的是: [Raymond Chen](http://blogs.msdn.com/oldnewthing/) 写了一篇MSDN文章 ("[The Shell Drag/Drop Helper Object Part 2](http://msdn.microsoft.com/library/en-us/dnwui/html/ddhelp_pt2.asp)") ,文中提供了一个 `IDataObject`的纯C++语言实现,这对于在WTL程序中实现拖放操作来说是一个巨大的帮助。
这篇文章的样例工程是一个CAB文件查看工具,它支持通过将文件从查看工具窗口拖动到windows文件夹窗口来实现解压操作。这篇文章也将讨论一些关于框架窗口的主题,比如处理 File-Open 操作和与MFC中文档视图框架类似的数据管理。我也将介绍 WTL的 MRU (最近经常使用,most-recently-used) 文件列表类,还有一些6.0版本列表视图空间的一些新特性。
**注意**: 你需要下载安装 Microsoft 的 CAB SDK才能编译样例代码。Microsoft的Konwledge Base网站中的一篇文章里有CAB SDK的链接: [Q310618](http://support.microsoft.com/?kbid=310618). 样例程序假定SDK被放置在源代码目录下名为"cabsdk"的目录里。
注意,如果你在安装WTL或者编译样例代码时遇到任何问题,在提问之前请阅读 [第一部分里 readme 这一节](./parti.html#readme)
## 创建工程
现在开始创建我们的 CAB 查看器程序,运行WTL AppWizard 然后创建一个名为 _WTLCabView_ 的工程。它是一个SDI(single document interface,单文档界面)应用程序,在第一页选择“SDI Application”:
![](https://box.kancloud.cn/2016-01-13_56962fba8fb62.png)
下一页,取消选中 _Command Bar_ ,然后将 _View Type_ 改为 _List View_. 向导会为我们的视图窗口创建一个C++类,宾切它继承自 `CListViewCtrl` 类。
![](https://box.kancloud.cn/2016-01-13_56962fba9c6ba.png)
视图窗口类看起来像这样:
```
class CWTLCabViewView :
public CWindowImpl<CWTLCabViewView, CListViewCtrl>
{
public:
DECLARE_WND_SUPERCLASS(NULL, CListViewCtrl::GetWndClassName())
// Construction
CWTLCabViewView();
// Maps
BEGIN_MSG_MAP(CWTLCabViewView)
END_MSG_MAP()
// ...
};
```
和[第二部分](./PartII.htm)我们使用的视图窗口一样,我们可以使用`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` 类,在 [第四部分](./PartIX.htm#usingcfiledialog) 中介绍的改进版的 `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文件时的效果:
![](https://box.kancloud.cn/2016-01-13_56962fbebda3a.gif)
`EnumCabContents()` 在视图类中使用了两个方法来填充UI: `AddFile()` 和 `AddPartialFile()`。当一个文件部分存储于该CAB文件(其余的部分在另外的CAB文件内)时调用 `AddPartialFile()` 方法。上图所示的截图中,列表中的第一个文件就是部分存储于该CAB文件中。剩余的项使用 `AddFile()` 方法添加到视图窗口中。这两种方法都为添加的文件创建了同一种数据结构,所以视图能够获得它所显示的文件的细节信息。
如果 `EnumCabContents()` 返回值是 true,那说明枚举过程和UI建立都成功的执行。如果我们仅仅是想写个简单的CAB查看器,现在做的这些就已经足够了,但是程序就不会那么有趣了。要让这个工具变得真正易用起来,我们要为它添加拖放操作使得用户可以通过拖动来解压文件。
## 拖动源
拖动源是实现了以下两个接口的 COM对象: `IDataObject` 和 `IDropSource`. `IDataObject` 用来存储拖放操作过程中客户端想要传输的所有数据;对我们来说就是一个 `HDROP` 结构,结构体里保存要从CAB文件里解压出来的文件列表 。OLE在拖放操作过程中调用 `IDropSource` 接口来通知事件的来源。
### 拖动源的接口
实现了拖动源的C++类是 `CDragDropSource`. 它开始于 [这篇MSDN文章](http://msdn.microsoft.com/library/en-us/dnwui/html/ddhelp_pt2.asp) 里描述的 `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`的管理和拖放操作过程中的通信。一次拖放操作遵循以下模式:
1. 用户开始一次拖放操作时主框架得到通知。
2. 主框架调用视图窗口的方法来创建一个被拖动的文件的列表。视图窗口类使用一个 `vector<CDraggedFileInfo>`结构返回这些信息。
3. 主框架创建一个 `CDragDropSource` 对象并且把 vector<CDraggedFileInfo>传递给它,这样它就可以了解要从CAB里解压的文件的信息。
4. 主框架开始拖放操作。
5. 如果用户在一个适当的位置释放目标,`CDragDropSource` 对象会解压缩相应的文件。
6. 主框架更新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 中,并且显示出下一步分所在文件的情形:
![](https://box.kancloud.cn/2016-01-13_56962fbecd335.png)
如果后续 CAB 文件无法找到,程序会通过设置该项样式为 `LVIS_CUT` 表明该文件无法解压,同时图标变为灰色。
![](https://box.kancloud.cn/2016-01-13_56962fbeefd80.png)
出于安全的考虑,程序将解压出的文件留在临时目录中,而不是拖放操作完成后立即清除它们。当 `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特性,只需要几步。
1. 插入一个ID为 `ID_FILE_MRU_FIRST` 的菜单项。菜单项文字设置为若MRU列表是空时你希望显示的消息。
2. 添加一个ID为 `ATL_IDS_MRU_FILE`的字符串表(string table)。这个字符串表用来显示MRU项选中时的浮动提示。如果你使用 WTL AppWizard 来生成工程,该字符串默认已经创建。
3. 向 `CMainFrame` 添加一个 `CRecentDocumentList` 对象。
4. 在 `CMainFrame::Create()` 里初始化这个对象。
5. 处理ID在`ID_FILE_MRU_FIRST` 和 `ID_FILE_MRU_LAST` 之间的 `WM_COMMAND` 消息。
6. 打开一个CAB文件时更新MRU列表。
7. 应用程序关闭时保存MRU列表。
另外,如果 `ID_FILE_MRU_FIRST` and `ID_FILE_MRU_LAST` 对于你的程序来说不合适,你可以通过一个新的特化的 `CRecentDocumentListBase`类来替换它们。
### 设置MRU对象
第一步是添加一个菜单项指明MRU列表的位置。通常将MRU文件列表放置于 _File_ 菜单下,我们的程序里也是这么做的。菜单项的位置如下图所示:
![](https://box.kancloud.cn/2016-01-13_56962fbf0e534.png)
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` 消息,并且创建一个做为拖动图片的位图回应消息。幸运的是,列表视图控件支持这个特性,所以不需要太多工作就可以得到拖动图片。效果图如下图所示:
![](https://box.kancloud.cn/2016-01-13_56962fbf19850.gif)
如果我们使用其他类型的窗口做为视图类,这种况口恰好不能处理 `DI_GETDRAGIMAGE` 消息,我们可以自己创建拖动图并调用 `InitializeFromBitmap()` 方法来存储到drag/drop helper对象中。
### 半透明的矩形选择框
从Windows XP开始,列表视图空间可以显示一个半透明的矩形选择覆盖框。这个特性是默认关闭的,可以通过在控件上设置 `LVS_EX_DOUBLEBUFFER` 属性来开启它。我们的程序在视图窗口初始化函数 `CWTLCabViewView::Init()` 里完成了这些工作。结果如下图说示。
![](https://box.kancloud.cn/2016-01-13_56962fbf292a7.png)
如果半透明覆盖区域没有出现,检查你的系统是否开启了这个特性:
![](https://box.kancloud.cn/2016-01-13_56962fbf3b8f0.png)
### 按列排序
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` 消息的一个包装,用来将我们排序的列设置为选中状态。
按文件大小排序的情况如下图所示:
![](https://box.kancloud.cn/2016-01-13_56962fbf48e84.gif)
### 使用平铺视图模式
在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` 数组包含了要显示的子项的数据,在这个例子中子项一是文件类型,子项二是文件大小。查看器如下图所示:
![](https://box.kancloud.cn/2016-01-13_56962fbf58f43.gif)
注意,在你按列排序列表之后这两行文本的内容会相应改变。当选中的列拥有 `LVM_SETSELECTEDCOLUMN` 样式的时候,子项的文本总是优先显示,覆盖了我们在 `LVTILEINFO` 结构中传递的子项文本。
- 中文版序言
- Part I - ATL GUI Classes
- Part II - WTL GUI Base Classes
- Part III - Toolbars and Status Bars
- Part IV - Dialogs and Controls
- Part V - Advanced Dialog UI Classes
- Part VI - Hosting ActiveX Controls
- Part VII - Splitter Windows
- Part VIII - Property Sheets and Wizards
- Part IX - GDI Classes, Common Dialogs, and Utility Classes
- Part X - Implementing a Drag and Drop Source