💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 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` 结构中传递的子项文本。