译:MFC 程序员的 WTL 教程(十)(第二版)

特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。

链接:上一部分

第十部分 – 实现一个拖放源

内容

简介

拖放是许多流行应用的特性之一。尽管实现一个放下目标相当简单,但拖动源却要复杂的多。MFC 中有两个类 COleDataObjectCOleDropSource 可以帮助管理拖动源所必须提供的数据,但 WTL 中没有这种辅助类。对于我们这些 WTL 用户来说,幸运的是,Raymond Chen 在 2000 年的时候在 MSDN 上写过一篇文章(“The Shell Drag/Drop Helper Object Part 2”),其中有 IDataObject 的纯 C++ 实现,这对于为 WTL 应用编制一个完整的拖放源提供了巨大的帮助。

本文的示例工程是一个 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。向导会为我们的视图窗口创建一个派生自 CListViewCtrl 的 C++ 类。

视图窗口类看起来就是这样:

就像我们在第二部分里使用视图类一样,我们可以使用 CWindowImpl 的第三个模板参数设置缺省的窗口风格:

由于在 WTL 中没有文档/视图框架,视图类需要做双份的工作,既是 UI,也是存放有关 CAB 信息的地方。在拖放操作中传递的数据结构为 CDraggedFileInfo

视图类中还有如下方法:初始化、管理文件列表,以及在拖放操作开始的时候准备一个 CDraggedFileInfo 列表。由于本文是讲关于拖放的,所以我不会深入到 UI 工作的内部去,需要了解所有细节的话可以检视示例工程中的 WTLCabViewView.h

文件打开处理

要查看一个 CAB 文件,用户可以使用 File-Open 命令并选择一个 CAB 文件。向导为 CMainFrame 生成的代码包含了 File-Open 菜单项的一个处理器:

OnFileOpen() 使用了 CMyFileDialog 类,该类是在第九部分里介绍到的 WTL 的 CFileDialog 的增强版本,用以显示一个标准的文件打开对话框。

OnFileOpen() 调用了辅助函数 ViewCab()

EnumCabContents() 相当的复杂,它使用 CAB SDK 调用来枚举在 OnFileOpen() 中选中的文件的内容,并填充视图窗口。不过 ViewCab() 现在还不完善,我们后面会给它加入支持 MRU 列表的代码。下面是查看器的样子,其中显示着 Windows 98 的 某个 CAB 文件的内容:

EnumCabContents() 使用了视图类中的两个方法来填充 UI: AddFile()AddPartialFile()AddPartialFile() 在当一个文件被部分存储在 CAB 中时被调用,因为其头部是在前面的 CAB 里。在上面的截图里,列表中的第一个文件就是一个部分文件。其余的文件是通过 AddFile() 添加的。这两个方法都会为要添加的文件分配一个数据结构,从而视图可以知道其显示的每个文件的所有相关细节。

如果 EnumCabContents() 返回真,则代表所有的枚举以及 UI 设置工作成功完成。如果我们只是写一个简单的 CAB 查看器,我们就可以收手了,尽管此应用不那么有趣。为了使它真正地有用,我们将对它添加拖放支持,以使用户可以从 CAB 中提取文件。

拖动源

拖放源是一个 COM 对象,它实现了两个接口: IDataObjectIDropSourceIDataObject 用来存放在拖放操作中客户端需要传递的任意数据,在我们这种情况下,此数据应该是一个 HDROP 结构,其中列出了要从 CAB 中提取的文件。 IDropSource 的方法会由 OLE 调用,用以在拖放操作中向源通知事件。

拖动源接口

实现了我们的拖放源的 C++ 类为 CDragDropSource。该类以我在简介中提到过的 MSDN 文章中的 IDataObject 实现为起始。在该文中你可以找到所有代码相关的细节,因此我在这儿就不重复了。然后我们再向类中加入 IDropSource 及其两个方法:

用于调用者的辅助方法

CDragDropSource 使用几个辅助方法封装了 IDataObject 的管理以及拖放的通信。一个拖放操作遵循以下模式:

  1. 主框架得到用户开始拖放操作的通知。
  2. 主框架调用视图窗口来创建一个被拖动的文件的列表。视图在一个 vector<CDraggedFileInfo> 向量中返回此信息。
  3. 主框架创建一个 CDragDropSource 对象并将上述向量传递给它,以使它得知要从 CAB 中提取哪些文件。
  4. 主框架开始拖放操作。
  5. 如果用户在一个适当的拖放目标上放下,则 CDragDropSource 对象提取文件。
  6. 主框架更新 UI 以标示不能被提取的文件。

步骤 3 到 6 由辅助方法处理。初始化在 Init() 方法中完成:

Init() 将数据复制到保护成员中,填入到一个 HDROP 结构,并使用 IDataObject 方法将该结构保存到数据对象中。 Init() 还作了另一个重要的步骤:它在 TEMP 目录下为每个拖动的文件创建了一个零字节的文件。例如,如果用户从 CAB 文件中拖动 buffy.txtwillow.txtInit() 将在 TEMP 目录下使用这两个名字创建两个文件。这是为了预防,万一拖放目标要验证从 HDROP 读入的文件名,如果文件不存在,则目标有可能会拒绝放下。

接下来的方法是 DoDragDrop()

DoDragDrop() 接受 dwOKEffects 中的一组 DROPEFFECT_* 标志,这些标志表明了源所允许的那些动作。它会查询必要的接口,然后调用 DoDragDrop() API。如果拖放成功, *pdwEffect 就被设置为用户希望执行的 DROPEFFECT_* 值。

最后一个方法是 GetDragResults()

CDragDropSource 对象维护的 vector<CDraggedFileInfo> 会在拖放操作过程中被更新。如果某个文件被发现还连着另一个 CAB,或者是不能被提取,则 CDraggedFileInfo 结构会被执行必要的更新。主框架调用 GetDragResults() 来获取此向量,查找错误并相应更新 UI。

IDropSource 的方法

第一个 IDropSource 方法是 GiveFeedback(),它通知源,用户是想采取哪种操作(移动、复制或者链接)。如果愿意的话源可以改变光标。 CDragDropSource 对操作保持了跟踪,并告诉 OLE 要使用缺省的拖放光标。

另一个 IDropSource 方法是 QueryContinueDrag()。OLE 在用户把光标移来移去时调用此方法,并告诉源哪个鼠标键,以及键盘键,被按下了。下边是大多数 QueryContinueDrag() 的实现所采用的样板代码:

当我们发现左键被释放了,就到了我们要从 CAB 中提取选中的文件的地方了。

CDragDropSource::ExtractFilesFromCab() 是另一个复杂点的代码,它使用 CAB SDK 把文件提取到 TEMP 目录下,覆盖掉我们先前创建的零字节的文件。当 QueryContinueDrag() 返回 DRAGDROP_S_DROP 时,也即告诉了 OLE 完成此拖放操作。如果拖放目标是一个资源浏览器窗口,资源浏览器会把文件从 TEMP 目录复制到发生拖放的目录。

从查看器中拖放

我们已经看过了实现拖放操作逻辑的类,现在,我们来看一下我们的查看器应用是怎样使用这个类的。当主框架窗口接收到 LVN_BEGINDRAG 通知消息时,它会调用视图以获取选中文件的列表,而后设置 CDragDropSource 对象:

第一个调用的是视图的 GetDraggedFileInfo() 方法,用以得到选中文件的列表。此方法返回一个 vector<CDraggedFileInfo>,我们要用它来初始化 CDragDropSource 对象。 GetDraggedFileInfo() 在选定的文件都不能被提取的情况下(例如文件被分块存放在不同的 CAB 文件中)有可能失败。如果发生了这种情况,则 OnListBeginDrag() 也静静地失败,不做任何事情就返回。最后,我们调用 DoDragDrop() 来开始操作,并让 CDragDropSource 处理剩余的事情。

上面列出的步骤 6 提到了拖放结束后对 UI 的更新。因为有可能在 CAB 末尾的一个文件仅仅是部分存储于此 CAB 中,而剩余的则在后续的一个 CAB 里。(这在 Windows 9x 的安装文件里非常普遍,在那儿 CAB 需要能符合软盘的大小)当我们试图提取这样的一个文件时,CAB SDK 会告诉我们含有该文件剩余部分的 CAB 的名字。它还会在原始 CAB 所在的相同目录下寻找那个 CAB,如果存在的话则从中提取文件的剩余部分。

因为我们要在视图窗口中标示分块文件,所以 OnListBeginDrag() 会检查拖放的结果,看是否找到了分块文件:

我们调用 GetDragResults() 来获取反映了拖放操作结果的更新过的 vector<CDraggedFileInfo>。如果结构中的 bPartialFile 成员为 true,则表示该文件仅部分存在于此 CAB 中。我们再调用视图方法 UpdateContinuedFile(),并将信息结构传递给它,因而它可以相应地文件列表视图中的项。下面就是当发现有后续 CAB 时,应用程序如何标示分块文件:

如果找不到后续的 CAB,应用会通过设置 LVIS_CUT 风格来标示该文件不可以被提取,所以图标看起来是虚的:

出于安全考虑,应用把提取出的文件留在了 TEMP 目录里,而不是在拖放操作结束后立刻清除它们。在 CDragDropSource::Init() 创建零字节的临时文件时,它同时也把文件名添加到了全局向量 g_vecsTempFiles 中。临时文件在主框架窗口关闭时才删除。

加入 MRU 列表

我们将要看到的另一个文档/视图风格的特性是最近使用文件列表。WTL 的 MRU 实现为一个模板类 CRecentDocumentListBase。如果你不需要覆盖 MRU 任何的缺省行为(当然缺省通常已经足够用了),你可以使用派生类 CRecentDocumentList

CRecentDocumentListBase 模板具有以下参数:

T
特化 CRecentDocumentListBase 的派生类的名字。
t_cchItemLen
TCHAR 为单位的存储在 MRU 项中的字符串长度。至少必须为 6。
t_nFirstID
用于 MRU 项的 ID 范围的最小 ID。
t_nLastID
用于 MRU 项的 ID 范围的最大 ID。此值必须大于 t_nFirstID

要把 MRU 特性添加到我们的应用里,需要做以下几步:

  1. 在我们希望 MRU 菜单项出现的地方插入一个 ID 为 ID_FILE_MRU_FIRST 的菜单项。此项的文本当列表为空时会显示出来。
  2. 添加一个 ID 为 ATL_IDS_MRU_FILE  的字符串表项。此字符串用作 MRU 项被选中时的动态帮助。如果你使用 WTL AppWizard,则此字符串已经帮你创建好了。
  3. 添加一个 CRecentDocumentList 对象到 CMainFrame 中。
  4. CMainFrame::Create() 里初始化该对象。
  5. 处理命令 ID 介于 ID_FILE_MRU_FIRSTID_FILE_MRU_LAST 之间(含)的 WM_COMMAND 消息。
  6. 当打开一个 CAB 文件时更新 MRU 列表。
  7. 应用关闭时保存 MRU 列表。

记住,如果 ID_FILE_MRU_FIRSTID_FILE_MRU_LAST 不适合你的应用的话,你还是可以更改 ID 范围的,只要生成 CRecentDocumentListBase 的一个新的特化版本就可以。

设置 MRU 对象

第一步就是添加一个菜单项,以标明 MRU 项应该处于什么位置。通常是在 File 菜单下,这也是我们的应用用到的。下面就是占位菜单项:

AppWizard 已经把 ATL_IDS_MRU_FILE 字符串添加到了字符串表里,我们将之改为读作“打开此 CAB 文件”。接下来,我们向 CMainFrame 中加入一个名为 m_mruCRecentDocumentList 类型成员变量并在 OnCreate() 中初始化它:

前两个方法设置了我们想在维持的项的数目(缺省为 16)以及包含占位项的菜单句柄。 ReadFromRegistry() 从注册表中把 MRU 列表读出。它接收我们传递给它的键名,并在其下创建一个新键来保存列表。当我们这里,键为 HKCU\Software\Mike's Classy Software\WTLCabView\Recent Document List

加载文件列表之后, ReadFromRegistry() 调用另一个 CRecentDocumentList 方法,即 UpdateMenu(),该方法会找到占位菜单项病将之以实际的 MRU 项代替。

处理 MRU 命令并更新列表

当用户选择了某个 MRU 项时,主框架会收到一个 WM_COMMAND 消息,命令 ID 等于菜单项的 ID。我们可以在消息映射中用一个宏来处理这些命令:

消息处理其从 MRU 对象处获取该项的全路径,然后调用 ViewCab() 以使应用显示该文件的内容。

如上文提到的,我们要扩展 ViewCab() 以使之感知 MRU 对象,并在必要时更新文件列表。新的原型为:

如果 nMRUID 为 0,则 ViewCab() 是被 OnFileOpen() 调用的,否则的话,则是用户选择了某个 MRU 菜单项, nMRUID 就是 OnMRUMenuItem() 接收到的命令 ID。下面是更新过的代码:

EnumCabContents() 成功了,我们就根据 CAB 文件是如何被选中的以不同的方式更新 MRU。如果是通过 File-Open 选中的,我们就调用 AddToList() 来把文件名加入到 MRU 列表里;如果是通过 MRU 菜单项选中的,我们就用 MoveToTop() 把该项移动到列表的顶部。如果 EnumCabContents() 失败,我们就用 RemoveFromList() 把文件名从 MRU 列表中移除。所有这几个方法都会在内部调用 UpdateMenu(),因此 File 菜单会自动被更新。

保存 MRU 列表

在应用关闭时,我们把 MRU 列表保存回注册表中。这事简单,只需要一行:

这行放到了 CMainFrameWM_DESTROYWM_ENDSESSION 的消息处理器里。

其他 UI Goodies

透明的拖动图像

Windows 2000 及以后有一个内建的 COM 对象,叫做拖放助手,其目的是在拖放操作中提供良好的透明拖动图像。拖动源通过 IDragSourceHelper 接口使用此对象。下面是我们加入到 OnListBeginDrag() 中的使用此助手对象的额外代码,用粗体标示:

我们从创建此拖放助手 COM 对象开始。如果成功,我们就调用 InitializeFromWindow() 并传递三个参数:拖动源的窗口 HWND,光标位置,和一个基于我们的 CDragDropSource 对象的 IDataObject 接口。拖放助手使用此接口保存其数据,而且,如果拖放目标也使用助手对象的话,此数据用来生成拖动图像。

要使 InitializeFromWindow() 可以工作,拖放源的窗口需要处理 DI_GETDRAGIMAGE 消息,而且在对该消息的效应里,要创建一个用作拖动图像的位图。对我们而言幸运的是,列表视图控件支持这一特性,因此我们仅需很少的工作就可以得到拖动图像。下面是拖动图像的样子:

如果我们使用一些别的不处理 DI_GETDRAGIMAGE 的窗口做我们的视图类,我们就要自己来创建拖动图像并调用 InitializeFromBitmap() 来把图像保存到拖放助手对象中。

透明的选择矩形

从 Windows XP 开始,列表视图控件可以显示一个透明的选择框。此特性缺省是关闭的,但通过给控件设置 LVS_EX_DOUBLEBUFFER 风格即可启用。我们的应用把这件事在 CWTLCabViewView::Init() 中作为视图窗口初始化工作的一部分来做。下面是成果:

如果没有显示出透明选择框,那就需要检查你的系统属性,确保下面的特性是启用了的:

标示排序的列

在 Windows XP 及之后,详细信息模式的列表视图控件有一个被选中的列,以不同的背景颜色显示。这一特性通常用来标示那一列是被排序了的,而这也正是我们的 CAB 查看器要做的。标头控件也有了两个新的格式风格,使得在一列中标头可以显示一个向上或者向下的箭头。这通常用来显示排序的方向。

视图类在 LVN_COLUMNCLICK 处理器中处理了排序。显示排序列的代码用粗体作了加亮:

加亮代码的第一小节去除了先前的排序列的排序箭头。如果没有排序的列的话,就省略这一步。然后,把箭头添加到用户刚刚点击的列上。如果以升序排序则箭头朝上,降序则朝下。排序结束后,我们调用 SetSelectedColumn() —— 对 LVM_SETSELECTEDCOLUMN 消息的一个封装 —— 来把选中列设置为我们刚刚排序的列。

以下是文件以大小排序后的列表控件的样子:

使用平铺视图模式

在 Windows XP 及之后,列表视图控件还有一种新的风格称为平铺视图模式。作为视图窗口初始化的一部分,如果应用是运行于 XP 或之后上,它就会使用 SetView() (对 LVM_SETVIEW 消息的一个封装)把列表视图的模式设置为平铺模式。然后再填充一个 LVTILEVIEWINFO 结构来设置一些控制如何平铺绘制的属性。 cLines 属性被设为了 2,表示要在图标旁边显示两行附加文本。 dwFlags 成员设置为了 LVTVIF_AUTOSIZE,这使得控件自身的大小改变时同时也改变平铺区域的大小。

设置平铺视图的图像列表

在平铺视图模式下,我们会使用特大系统图形列表(在缺省的显示设置下图标为 48×48 大小)。我们使用 SHGetImageList() API 获取此图像列表。 SHGetImageList() 不同于 SHGetFileInfo() 的是它返回一个基于图像列表对象的 COM 接口。视图窗口有两个成员变量用来管理此图像列表:

视图窗口在 InitImageLists() 中获取特大图像列表:

如果 SHGetImageList() 成功,我们可以把 IImageList* 接口转型为一个 HIMAGELIST,再像使用其它图像列表一样使用它。

使用平铺视图的图像列表

由于列表控件并不能为平铺视图模式持有单独的图像列表,所以我们需要在运行时,当用户选择大图标或者平铺视图模式时改变图像列表。视图类有一个 SetViewMode() 方法来处理改变图像列表和视图风格的事宜:

如果控件即将进入平铺视图模式,那我们就把控件的图像列表设置为 48×48 的那个,否则设置为 32×32 的那个。

设置附加的文本行

在初始化时,我们将平铺效果设置为要显示附加的两行文本。第一行总是项的文字,就像在大图标和小图标模式里一样。显示在两行附加的文本里的文字取自于子项,类似于详细信息模式下的列。我们可以为每个图标设置单独的子项。下面就是在 AddFile() 中视图是怎样设置文本的:

aCols 数组保存有子项,其文本将会被显示出来,在这儿我们显示子项 1 (文件类型)以及 2 (文件大小)。下面是在平铺视图模式下查看器的样子:

注意,附加行当你在详细信息模式下排序某列之后会改变。当使用 LVM_SETSELECTEDCOLUMN 设置了选中列时,该子项的文字总是最先显示,覆盖了我们传递到 LVTILEINFO 结构中的子项设置。

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.

修订历史

2006 年 6 月 16 日:首次发布

链接:上一部分

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注