Visual C++ MFC 扩展编程实例


VISUAL C++MFC 扩展 编程实例 目 录 第一部分 基础 第 1 章 概述 1 1.1 Windows 基础 1 1.1.1 窗口类结构 2 1.1.2 消息 2 1.1.3 客户区和非客户区 2 1.1.4 重叠窗口、弹出窗口和子窗口 2 1.1.5 父窗口和宿主窗口 3 1.2 Windows 消息 3 1.2.1 发送或寄送消息 4 1.2.2 消息类型 4 1.2.3 接收消息 4 1.2.4 窗口处理函数的子类化 5 1.3 窗口绘图 5 1.3.1 设备环境 5 1.3.2 绘图工具 6 1.3.3 映射模式 6 1.3.4 窗口视和视口视 6 1.3.5 逻辑单位和设备单位 7 1.3.6 绘图函数 7 1.3.7 抖动和非抖动颜色 7 1.3.8 设备无关位图 8 1.3.9 元文件 8 1.3.10 何时绘图 8 1.4 MFC 基础 8 1.5 Developer Studio 基础 9 1.6 Windows 和 MFC 总结 10 1.7 基本类 10 1.8 应用类 11 1.8.1 文档视 11 1.8.2 CWinApp(OC) 11 1.8.3 文档模板 12 1.8.4 线程 12 1.8.5 CFrameWnd(OCW) 12 1.8.6 CDocument(OC) 12 1.8.7 CView(OCW) 13 1.8.8 对话框应用程序 13 1.8.9 SDI 应用程序 13 1.8.10 MDI 应用程序 13 1.9 其余用户界面类 13 1.9.1 通用控件类 13 1.9.2 菜单类(O) 14 1.9.3 对话框类 15 1.9.4 通用对话框 MFC 类 15 1.9.5 控件条类 (OCW) 15 1.9.6 属性类 15 1.10 绘图类 16 1.11 其他 MFC 类 16 1.11.1 文件类 16 1.11.2 CArchive 和序列化 16 1.11.3 数据库类 17 1.11.4 ODBC 类 17 1.11.5 DAO 类 17 1.11.6 数据集合类 17 1.11.7 通信类 18 1.12 类的消息机制 18 1.12.1 MFC 如何接收一个寄送消息 18 1.12.2 MFC 如何处理接收的消息 18 1.12.3 UI 对象 20 1.13 小 结 20 第 2 章 控制条 21 2.1 通用控制条 21 2.2 用 API 创建控制条 22 2.3 用 MFC 创建控制条 24 2.3.1 CToolBarCtrl 和 CStatusBarCtrl 24 2.3.2 CToolBar 和 CStatusBar 24 2.3.3 CControlBar 26 2.4 停靠栏 27 2.4.1 设置停靠功能 28 2.4.2 自动改变大小和移动 30 2.4.3 停靠栏小结 30 2.5 浮动条 31 2.6 MFC 的高级控制条类小结 32 2.7 视和控制条如何共享客户区 32 2.7.1 CFrameWnd::RecalcLayout() 32 2.7.2 CWnd::RepositionBars() 33 2.7.3 CControlBar::OnSizeParent() 33 2.7.4 CalcDynamicLayout()和 CalcFixedLayout () 34 2.7.5 CToolBar::CalcFixedLayout()和 CTool Bar:: CalcDynamicLayout() 35 2.7.6 工具栏布局 35 2.7.7 CStatusBar::CalcFixedLayout() 36 2.7.8 CDockBar::CalcFixedLayout() 36 2.7.9 共享客户区小结 36 2.8 对话条 37 2.9 伸缩条 38 2.9.1 CReBar 和 CReBarCtrl 39 2.9.2 CReBar::CalcFixedLayout() 39 2.10 命令条 39 2.11 控制条窗口小部件风格 40 2.11.1 工具栏按钮风格 40 2.11.2 状态栏窗格风格 40 2.11.3 伸缩条段风格 40 2.12 设计自己的控制条 41 2.12.1 重载 CControlBar::CalcDynamic-Layout() 41 2.12.2 增加 WM_SIZEPARENT 消息处理器 41 2.12.3 重载 CMainFrame::RecalcLayout() 41 2.12.4 从 CDockBar 派生 42 2.13 实例 42 2.14 总结 42 第 3 章 通信 43 3.1 进程间通信 43 3.1.1 通信策略 43 3.1.2 同步和异步通信 44 3.2 窗口消息 44 3.2.1 打开和关闭 44 3.2.2 读与写 45 3.2.3 回顾 45 3.3 动态数据交换 46 3.3.1 客户/服务器 46 3.3.2 打开和关闭 46 3.3.3 读和写 47 3.3.4 其他 DDE 函数 48 3.3.5 MFC 支持 48 3.3.6 回顾 49 3.4 消息管道 49 3.4.1 打开和关闭 49 3.4.2 读和写 50 3.4.3 回顾 51 3.5 Windows 套接字 51 3.5.1 打开和关闭 52 3.5.2 读和写 52 3.5.2 通过 Windows 套接字序列化 53 3.5.3 数据流和数据报 53 3.5.4 回顾 54 3.6 串行/并行通信 54 3.6.1 打开和关闭 54 3.6.2 读和写 54 3.6.3 配置端口 55 3.6.4 回顾 55 3.7 Internet 通信 56 3.7.1 打开和关闭文件 56 3.7.2 读文件 56 3.7.3 打开和关闭连接 56 3.7.4 其他 Internet 类 57 3.8 通信方式小结 57 3.9 共享数据 58 3.10 共享内存文件 58 3.10.1 创建和销毁 58 3.10.2 读和写 58 3.10.3 回顾 59 3.11 文件映射 59 3.11.1 打开和关闭 59 3.11.2 读和写 60 3.11.3 数据同步 60 3.11.4 回顾 60 3.12 客户/服务器 61 3.12.1 传递调用参数 61 3.12.2 远程过程调用 62 3.13 小结 62 第二部分 用户界面实例 第 4 章 应用程序和环境 64 4.1 实例 1:在工具栏中添加静态标识符 64 4.2 实例 2:在工具栏中添加动态标识符 71 4.3 实例 3:只启动一个实例 75 4.4 实例 4:创建对话框/MDI 混合式应用程序 77 4.5 实例 5:在系统托盘中添加图标 79 4.6 实例 6: 主菜单状态栏中的标记 81 第 5 章 菜单、控件条和状态栏 85 5.1 实例 7:在菜单中添加图标 85 5.2 实例 8:调整命令条外观 97 5.3 实例 9:可编程工具栏 102 5.4 实例 10:在对话框中添加工具栏、菜单和状态栏 127 5.5 实例 11:在弹出菜单中增加位图标记 129 5.6 实例 12:工具栏上的下拉按钮 131 5.7 实例 13:在状态栏中添加图标 136 5.8 实例 14:使用伸缩条 141 第 6 章 视 143 6.1 实例 15:创建标签窗体视 143 6.2 实例 16:创建具有通用控件的视 150 6.3 实例 17 :打印报表 156 6.4 实例 18: 打印视 167 6.5 实例 19:绘制 MDI 客户视 174 6.6 实例 20:拖放文件到视 177 第 7 章 对话框和对话条 179 7.1 实例 21:动态改变对话框的尺寸 179 7.2 实例 22:自定义数据交换并验证 184 7.3 实例 23:重载通用文件对话框 187 7.4 实例 24:重载通用颜色对话框 190 7.5 实例 25:获得目录名 192 7.6 实例 26:子对话框 197 7.7 实例 27:子属性表 198 第 8 章 控件窗口 200 8.1 实例 28:自己绘制的控件 200 8.2 实例 29:在窗口标题中添加按钮 204 8.3 实例 30:添加热键控件 211 第 9 章 绘图 213 9.1 实例 31:使用非散射颜色 213 9.2 实例 32:伸展位图 227 9.3 实例 33:抓取屏幕 231 9.4 实例 34:输出 DIB 位图文件 236 第 10 章 帮助 243 10.1 实例 35:添加帮助菜单项 243 10.2 实例 36:添加上下文相关帮助 245 10.3 实例 37:添加气泡帮助 247 第 11 章 普通窗口 254 11.1 实例 38:创建普通窗口 254 11.2 实例 39:创建短调用形式窗口类 256 11.3 实例 40:创建长调用形式窗口类 258 第 12 章 特定的应用程序 261 12.1 实例 41:创建简单的文本编辑器 261 12.2 实例 42:生成简单的 RTF 编辑器 262 12.3 实例 43:创建资源管理器界面 265 12.4 实例 44:创建简单的 ODBC 数据库编辑器 284 12.5 实例 45:创建简单的 DAO 数据库编辑器 287 12.6 实例 46:创建简单的向导 289 第三部分 内部处理实例 第 13 章 消息和通信 295 13.1 实例 47:等待消息 296 13.2 实例 48:清除消息 297 13.3 实例 49:向其他应用程序发送消息 298 13.4 实例 50:与其他应用程序共享数据 300 13.5 实例 51:使用套接字与任意的应用程序通信 301 13.6 实例 52:使用串行或并行 I/O 321 第 14 章 多任务 331 14.1 实例 53:后台处理 331 14.2 实例 54:运行其他应用程序 332 14.3 实例 55:改变优先级 334 14.4 实例 56:应用程序内部的多任务工作者线程 336 14.5 实例 57:应用程序内部的多任务—用户界面线程 339 14.6 实例 58:向用户界面线程发送消息 342 14.7 实例 59:线程间的数据共享 343 第 15 章 其他 347 15.1 实例 60:创建定时器 347 15.2 实例 61:播放声音 349 15.3 实例 62:创建 VC++宏 350 15.4 实例 63:使用函数地址 351 15.5 实例 64:二进制字符串 352 15.6 实例 65:重新启动计算机 356 15.7 实例 66:获得可用磁盘空间 357 15.8 实例 67:闪烁窗口和文本 358 第四部分 附录 附录 A 消息和重载顺序 361 附录 B 绘图结构 385 下载下载 无论读者是否已经读过本系列的书籍,或者已经具备了多年的编程经验,我们仍将在这 一部分回顾一下所需要的基本知识,其目的就在于能够使读者更好地理解本书的实例。编写 程序常常是一种需要尝试不同方法以达到最终目的的工作。通常情况下,了解用 M F C来做什 么涉及到对4个基本概念的理解: Windows API怎样创建窗口;M F C如何封装并改进Wi n d o w s A P I;M F C如何与窗口通信以及 M F C是怎样控制绘图任务的。除了这些概念以外,本部分还 将讨论一下工具栏和状态栏。最后我们将讨论一下 M F C如何同非Wi n d o w s构件进行通信,如 串行口和I n t e r n e t站点。 本部分包括的章节介绍如下。 第1章 概述 本章概述M F C如何封装并改进Windows API。如果读者已经阅读过本系列书籍的前一本, 将会发现该章是对那些版本基础部分的一些回顾。本书包含这一章是为了保持本书对高层次 读者的独立性。 第2章 控件条 本章将讨论M F C支持的控件条。标准的控件条包括工具栏、状态栏和伸缩条 ( R e b a r )等。 M F C增加了对话条和停靠栏。该章还要探讨 M F C如何避免控件条之间以及它们和视之间互相 覆盖的技术内幕。 第3章 通信 本章讨论应用程序与外部世界的不同通信方式。其中最基础的窗口消息将在第一章中讨 论。本章还涉及其他一些通信途径,包括局域网、 I n t e r n e t通信、串行和并行端口、 D D E、 Wi n d o w s挂钩和管道等。 第 1 章 概 述 本章将回顾Wi n d o w s应用程序的基本知识,包括应用程序如何创建窗口、窗口之间如何 进行对话以及如何在窗口内绘图。然后将讨论微软基础类库 ( M F C )以及Developer Studio怎样 使创建窗口应用程序的工作变得容易起来。 1.1 Windows基础 当Wi n d o w s操作系统启动应用程序时,它首先创建一个程序线程,该线程一般只是一个 第一部分 基 础可执行内存的管理模块,而这些内存则与系统中其他应用程序分享执行时间。如果这个应用 程序要通过显示屏幕与用户交互,那么这个程序线程便需要负责创建显示在屏幕上的窗口。 程序线程通过调用操作系统的应用程序编程接口 ( A P I )来创建这些窗口。实际调用的函数 是: : C r e a t e Wi n d o w E x ( ),这个函数需要下列参数:屏幕位置、窗口大小以及即将创建的窗口的 风格。 1.1.1 窗口类结构 线程创建的多数窗口具有类似的风格 (例如按钮),这些类似的风格已经被集成为一个名为 窗口类( Windows Class)的结构。注意这是一个结构,而不是一个 C + +类。在创建窗口时必须 设定窗口类。也可以使用其他的窗口风格,并且分别设定各自的窗口类结构。 1.1.2 消息 如果用户单击了一个窗口,操作系统就会向这个窗口发送一个消息来把这一事件通知给 它。每个窗口用自己的窗口处理过程来处理窗口消息,举个例子,一个按钮的窗口处理过程 可能向它的主窗口发送一个消息告诉它需要做什么事情。 每个窗口的处理过程还负责在屏幕上绘制属于自己的窗口。操作系统在绘制窗口时会向 目标窗口发送W M _ PA I N T消息。 所有类似的窗口具备同样的窗口处理过程,例如,所有的按钮控件使用同样的窗口处理 过程,因此所有的按钮看起来外表都很类似,其行为也类似。这种情况下的窗口处理过程位 于操作系统内。它的地址在窗口的窗口类结构内指定。所有的按钮控件都由同样的窗口类创 建,这个窗口类结构的名字叫做 B U T TO N。 1.1.3 客户区和非客户区 窗口处理过程在屏幕上绘制一个窗口时实际上绘制了两个部分:客户区和非客户区。为 了绘制非客户区(nonclient area),窗口处理过程总是调用所有窗口过程都需要调用的相同的操 作系统处理过程。该过程接下来还需要绘制框架、菜单条以及标题栏等等多数窗口共同具有 的内容。过程所绘制的东西取决于窗口的风格。例如,由于按钮的风格被指定为不用绘制其 非客户区,所以按钮窗口上就不会看见框架和菜单。 窗口的客户区(client area)总是由窗口自己的窗口处理过程绘制,也可能由操作系统来完 成这件事,例如所有的按钮都由同样的处理过程来绘制,或者由创建者自己来绘制图像或列 表。 1.1.4 重叠窗口、弹出窗口和子窗口 除了窗口类以外,还有成百上千种窗口风格供用户指定窗口的绘制及其行为。其中有 3种 最重要的风格创建了对应3种最基本的窗口类型:重叠窗口、弹出窗口和子窗口。 ■ 重叠窗口(overlapped window),具有应用程序主窗口的全部特点。它的非客户区包括 一个可伸缩的框架、菜单条、标题栏和最小化、最大化按钮。 ■ 弹出窗口(popup window),具有消息框或者对话框的全部特点。它的非客户区包括一 个固定大小的框架和一个标题栏。 2第第第一部分第基 础 下载■ 子窗口(child window),具有类似按钮控件的全部特点。它没有非客户区,窗口的处理 过程负责绘制窗口的每个部分。 这些窗口在其行为上表现不同,这将在以后讨论。 1.1.5 父窗口和宿主窗口 由于用户界面可能会由好多个窗口组成,所以由程序线程控制它们将是很困难的,例如, 如果用户将一个应用程序最小化,那么程序线程应该对那些组成用户界面的所有最小化窗口 负责吗?实际上并没有采取直接控制的方式,应用程序创建的每个窗口都通过调 用: : C r e a t e Wi n d o w E x ( )分配了一个控制窗口。如果这个控制窗口被最小化,那么所有被其控制 的窗口都会由操作系统自动地最小化。如果控制窗口被销毁,那么每个被控制窗口也随之被 销毁。 每个被控窗口也可以作为其他窗口的控制窗口,结果最小化或者销毁某些窗口都只影响 用户界面的一部分。无论是什么窗口或者位于何处,程序员都可以在它内部创建另外的窗口。 子窗口的控制窗口叫做父窗口 (parent window)。父窗口剪切其子窗口,也就是子窗口不 能在其父窗口以外绘制。当用户与类似按钮的子窗口交互的时候,这些子窗口将自动地生成 消息并发送到其父窗口。这就使得控件可以在父窗口的窗口处理过程中被集中处理。 图1-1 构成一个Windows应用程序界面的窗口 弹出窗口和重叠窗口的控制窗口叫做宿主窗口 (owner window)。与父窗口不同,宿主窗口 并不限制属于它的窗口。然而,当最小化宿主窗口的时候,它的所属窗口也将被最小化。但 是,当宿主窗口隐藏的时候,它的所属窗口却仍然显示。请参见图 1 - 1以了解组成Wi n d o w s应 用程序的各种窗口。 1.2 Windows消息 上面提到,每个窗口由其自己的窗口处理过程响应来自操作系统或者其他窗口的消息。 例如,用户用鼠标单击了某个窗口,那么操作系统可能就会向这个窗口发送一个消息。如果 这是一个标识为 Load File的按钮窗口,那么它的窗口处理过程就可能向应用的主窗口发送一 个消息通知加载文件。主窗口处理过程将加载文件并在其客户区显示文件内容来作为响应。 接下来讨论消息是如何传送的。 第 1 章第概第第述第第3下载 一个重叠窗口。 宿主窗口是桌面, 也叫主窗口 子窗口。 文窗口是主窗口 弹出窗口。宿主 窗口是主窗口 子窗口。文窗口 是它所在的弹出 窗口1.2.1 发送或寄送消息 传送消息到窗口有两种方式:发送 ( s e n d )或者寄送( p o s t )。这两种方式之间的主要差别在 于被寄送的消息不必立即处理。被寄送的消息放置于一个先入先出的队列里等待应用程序空 闲的时候处理,而被发送的消息需要立即处理。实际上,发送消息到窗口处理过程和直接调 用窗口处理过程两者之间几乎没有任何不同。只是,你可以要求操作系统截获所有为达到某 个目的而在应用程序中被发送的消息,但不能截获对窗口处理过程的直接调用。 与用户输入相对应的消息 (如鼠标单击和按下一个按键 )通常都是以寄送的方式传送,以便 于这些用户输入可以由运行较缓慢的系统进行缓冲处理。而其余的所有消息都是被发送的。 在以上的例子中,系统寄送了鼠标单击消息,而按钮窗口则向其主窗口发送了 Load File消息。 1.2.2 消息类型 有3种类型的消息:窗口消息、命令消息和控件通知消息: ■ 窗口消息( Window message)是由操作系统和控制其他窗口的窗口所使用的消息。这一 类的消息有C r e a t e、D e s t r o y和M o v e等等。上例中的鼠标单击消息也是一种窗口消息。 ■ 命令消息(Command message)是一种特殊的窗口消息,它从一个窗口发送到另一个窗 口以处理来自用户的请求。在以上例子中,从按钮窗口发送到主窗口的消息就是命令消息。 ■ 控件通知消息(control notification) 是最后一种消息类型。它类似命令消息,当用户与 控件窗口交互时,这一类消息就从控件窗口发送到其主窗口。但是,这种消息的目的并不在 于处理用户命令,而是为了让主窗口能够改变控件,如加载并显示更多的数据。以上所述的 例子中并没有控件通知消息,但是,假如按钮发送了鼠标单击消息给它的主窗口,那么这个 消息也可以看作是一个控件通知消息。一个普通的鼠标单击消息可以由主窗口直接处理,然 后由控件窗口处理。 1.2.3 接收消息 窗口处理过程看起来与其他函数和方法没有任何不同。消息到来后,按照消息类型排序 进行处理。其中的参数则由调用函数提供以进一步区分消息。命令消息由 w P a r a m中的命令I D 分类。D e f Wi n d o w P r o c ( )函数则发送任何程序员都不会去处理的消息给操作系统。所有传送到 窗口的消息都将通过这个函数,甚至包括绘制窗口非客户区的消息— 尽管最终它们都将绕 过D e f Wi n d o w P r o c ( )函数。 一个主窗口的处理过程实例如下: MainWndProc( HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam ) { switch( message ) { case WM_CREAT E : : : : b r e a k ; case WM_PA I N T: : : : b r e a k ; case WM_COMMAND: 4第第第一部分第基 础 下载switch (id) { case IDC_LOAD_FILE: : : : b r e a k ; } b r e a k ; d e f a u l t : return( DefWindowProc( hWnd, message, wParam, lParam ) ); } return( NULL ); } 1.2.4 窗口处理函数的子类化 上面提到过,在窗口类中定义了窗口处理过程的地址。由窗口类所创建的窗口将把它们 的消息传递给窗口处理过程。如果程序员使用的是一个由系统提供的窗口类,并要增加自己 对窗口的特殊处理,就需要使用子类化 ( s w b o l a s s i n g )。 将窗口指向自己的窗口处理过程,便对窗口进行了子类化,这样所有的消息都可以由程序员 自己处理了。如果只想处理一个或 者两个消息,只需简单地将剩余消 息传递给初始的窗口处理过程即可。 我们注意到,采用子类化并没有修 改原先的窗口类,而是直接对窗口 对象进行了修改,这个对象保存了 一份窗口处理过程地址的拷贝。 与此相反,超类化( s u p e r c l a s s - i n g )则修改了原始的窗口类,然后 将其应用于创建窗口,但由于可 以更方便、安全地使用 M F C来获 得由超类化带来的好处,所以这 种方法就很少采用了。 请参见图1 - 2了解子类化概念。 1.3 窗口绘图 Windows API为窗口绘图提供了好几种调用函数。它们是点、弧、图形和圆绘图函数以及 图形填充和位图绘制等函数。正是因为采用了绘图 A P I,所以程序员必须负责传递坐标数值、 颜色、宽度和绘图位置,而 A P I函数则负责剩余的工作。为了简化对 Windows API的图像函数 调用,一些参数被固化到了一个名为设备环境的可重用对象中。 1.3.1 设备环境 图像设备环境(Device Context)是一个简单的对象,它包含了对绘图而言比较共同的属性, 第 1 章第概第第述第第5下载 窗口对象指向创 建它的窗口类的 窗口过程 通过设置自己的 窗口过程地址来 子类化该窗口 任何不希望自行 处理的消息可以交 由初始窗口过程负 责处理 窗口对象 初始窗 口过程 新窗口过程 图1-2 窗口处理函数的子类化例如绘图位置、线宽、填充模式的颜色等等。这个对象可以一次设置然后多次重复使用。实 际上并不需要自己创建设备环境,只要调用几个可能的 A P I函数,系统就可以返回已经被预先 准备好的设备环境值。例如,系统为屏幕创建的设备环境包含屏幕上将用于绘图的颜色以及 绘图工具的当前位置,程序员则可以在该位置进行绘图并设置颜色。 1.3.2 绘图工具 设备环境并没有包括绘图所需的全部特征。有几个特征存在于设备环境可以引用的附加图 像对象中。这些对象都各自代表了某一特定绘图工具的特征(如:画笔的色彩和宽度、画刷采用 的模式等)。这些工具包括绘制直线的画笔;用某种模式填充封闭区域的画刷;确定文本绘制效 果的字体以及确定使用何种颜色的调色板等等。另外的两种绘图工具:位图和区域则显得更抽 象一些。位图工具看起来像画刷,除了它只能用位图模式填充一个区域。而区域工具则类似于 画笔工具,但它起的是剪切作用,例如可以使用区域工具从一个位图中将“ S TO P”单词分割 出来。 1.3.3 映射模式 设备环境保持跟踪程序员采用的映射模式。设置映射模式可以指定调用参数中以英寸或 者厘米作为度量单位的坐标 x和y,每个绘图函数则可以自动确定绘制多少像素。可用的映射 模式如下表所示。 表1-1 可用的映射模式 模 式 使 用 说 明 M M _ T E X T 这是缺省的映射模式,坐标值 x和y精确地等于一个屏幕像素或者一个打印机的打印点, y的正方向沿屏幕或者打印页向下 M M _ H I E N G L I S H x和y值为屏幕或者打印页上一英寸的 1 / 1 0 0 0。窗口决定当前屏幕设备应该有多少个像 素等于1 / 1 0 0 0英寸。Y的方向则为沿屏幕或者打印页向上 M M _ L O E N G L I S H x和y值为设备上一英寸的1 / 1 0 0 0,y向上为正 M M _ H I M E T R I C x和y值为设备上一毫米的1 / 1 0 0,y向上为正 M M _ L O M E T R I C x和y值为设备上一毫米的1 / 1 0,y向上为正 M M _ T W I P S x和y值为设备上一英寸的1 / 1 4 4 0,y向上为正。这个模式通常应用于绘制文本,一 t w i p 等于一个字体点的1 / 2 0 M M _ A N I S O T R O P I C 程序员通过设置接下来将讨论的窗口和视口以决定 x和y代表多少像素 M M _ I S O T R O P I C 同上,但x和y代表的像素数必须相等 1.3.4 窗口视和视口视 M M _ A N I S O T R O P I C和M M _ I S O T R O P I C映射模式允许程序员自定义将坐标 x和y换算为像 素数的转换比率。这个工作由定义两种叫做视的矩形来完成。首先应该定义一个代表将要绘 制的整个区域 (例如:0,0,1 0 0 0,1 0 0 0 )的矩形,然后定义一个代表那些最终出现绘图结果 的屏幕或打印机上的对等坐标 (例如: 0,0,5 0 0,5 0 0 )的矩形。第一个矩形叫做窗口视 6第第第一部分第基 础 下载( Window Vi e w ),第二个矩形叫做视口视( Viewport Vi e w )。如果在设备环境中定义了这两个矩 形,并使用上述两种映射模式之一,那么使用窗口视坐标的图形将自动地被转换为使用视口 视的坐标。除了视口视的坐标之外,程序员可以缩小、放大以及颠倒自己的图形,而不需要 改变其他任何东西。 1.3.5 逻辑单位和设备单位 使用除M M _ T E X T以外的绘图模式绘图的时候,传递给绘图函数的坐标采用逻辑单位 (Logical Unit)。逻辑单位可以是英寸、厘米或者像素。绘图函数本身则用设备单位绘图。举 一个例子,直线绘图函数可能使用 2 5 4个像素代表一个1英寸的逻辑单位,这里的数字 1就是逻 辑单位数值,而数字 2 5 4则是设备单位(Device Unit)数值。这并不会成为一个问题,除非打算 让用户能够使用鼠标绘图。鼠标传回给应用程序的任何坐标都是以设备单位计量的数值,因 此必须使用一些Windows API将这些坐标值转换为逻辑单位数值。 1.3.6 绘图函数 Windows API具有多个绘图函数,举例如下: ■ 画点函数:如S e t P i x e l ( )。 ■ 画线函数:如L i n e To ( )、A r c ( )、P o l y l i n e ( )。 ■ 画图形函数:如R e c t a n g l e ( )、P o l y g o n ( )、E l l i p s e ( )。 ■ 填充和图形反转函数:如F i l l R e c t ( )、I n v e r t R e c t ( )、F i l l R g n ( )。 ■ 滚屏函数:S c r o l l D C ( )。 ■ 文本绘制函数: 如Te x t O u t ( )、D r a w Te x t ( )。 ■ 位图和图标绘制函数:如D r a w I c o n ( )、B i t B l t ( )。 1.3.7 抖动和非抖动颜色 所有可用的绘图函数,包括从画线到画图形和文本,都需要使用颜色。但是,除非系统 具备足够的显示内存,否则颜色将必须利用抖动 ( D i t h e r e d )方式。所谓抖动颜色实际上是一些 主要颜色的集合,在显示的时候通过几个颜色相互混合以获得某种理想的色彩。 对大多数情况,抖动颜色已经足够了。然而,由于抖动颜色显得比较模糊,对图像应用 程序而言通常不足以达到要求。图像应用程序不希望线条与其包围的图形颜色相互渗透混合。 对图像应用程序可以有以下几种选择方案: ■ 增加更多的显示内存。需要颜色抖动的理由在于,虽然屏幕上的每个像素都具有自 己的 R G B颜色。但一般的系统都没有足够的显示内存来为每个像素存储其 R G B值。例如, 设每个像素为 3 2位,那么一个 8 0 0×6 0 0像素的屏幕就需要 2 M字节的显示内存以容纳所有的 颜色值。 ■ 只用标准颜色绘图。 Wi n d o w s使用的显示卡保证了至少能定义 2 0种标准颜色,因为它 需要使用这些颜色来创建抖动效果。 ■ 配置自己的颜色。除了标准颜色以外,显示卡还有一些空间可定义 2 0 0多种颜色,可以 在设备环境调色板内定义这些颜色并只用这些颜色绘图。多数图像应用都采用了这种方法, 此方法将在实例3 1中得到详细描述。 第 1 章第概第第述第第7下载1.3.8 设备无关位图 每个像素都有自己的 R G B颜色,每种颜色的数值范围可以为 0~2 5 4。一排同样颜色(例如 黑色)的像素在屏幕上显示一条直线,一个具有不同颜色像素的矩形就可以创建任何图像。将 这些像素的色彩数值存到一个文件中就得到了一个位图文件。这个文件的头不仅要指示文件 包含了多少颜色值,而且还要指出多少颜色值组成一排。 如果位图文件中的每个颜色值都包含完整的 R G B数值,那么,由于这个颜色值完全在位 图中得到定义,这个文件就是一个设备无关位图。如果每个颜色值实际上都是对某个颜色表 的字节索引,那么,在它同时包含了这个颜色表的情况下,这个文件仍然是设备无关的。像 这样的颜色索引常用于压缩位图的大小。一个 8位索引只占用3 2位R G B值空间的四分之一。 设备无关位图(Device Independent Bitmaps)由对颜色表的索引组成,这个颜色表在系统的 显卡中被定义。这就是在上节的第 3步为显示非抖动颜色而配置的颜色表,如果某个位图指向 它的话,那么这个颜色表将不能独立于设备之外而存在。 1.3.9 元文件 除了在屏幕或者打印机上绘图,还可以在文件中绘图,这样的文件就是元文件 ( M e t a f i l e )。 元文件的优点在于:无论绘制的内容是什么,它们都将完整地重现。由于元文件可以伸展得 到更好的效果,而位图进行伸展时会扭曲变形,所以元文件优于用位图的形式存储图像。 1.3.10 何时绘图 看起来这个话题可能有点傻,但对一个多任务操作系统而言,应用程序之间不得不争夺 屏幕上有限的显示空间。哪怕只是在自己的窗口中绘图,也不可能想做什么就做什么。通常 只是在窗口接收到 W M _ PA I N T消息或者W M _ D R AW I T E M消息(针对所属的控件窗口 )的时候 才绘图。系统仅在窗口被部分遮盖以及由于关闭其他窗口而显现出来的时候才发送这些消息。 或者说,如果需要重画以显示新的信息,系统在这种情况之下将发送这些消息。 1.4 MFC基础 到目前为止,我们只讨论了应用程序可以从 Windows API中所获得的功能。但 A P I并不是 面向对象的。例如,使用A P I不可能在创建一个窗口实例之后再调用其成员函数作用于该窗口。 并且,不能从窗口类派生出一个可以加入自己所需功能的类,比方说,不可以增加自己的窗 口处理过程。 微软基础类库所要做的就是向应用程序提供可以访问 Windows API的一种模拟面向对象的 访问方式。在功能上,每个 M F C类都紧密结合一种 Wi n d o w s资源对象(如窗口),而A P I函数则 控制该资源。 举个例子,M F C的C W n d类创建并控制窗口。而操作系统一创建窗口就会开辟一块叫做 Wi n d o w对象的管理内存,然后返回该对象的指针,也就是窗口句柄。 MFC CWnd类则以成员 变量的形式存储这个句柄,并且由 C W n d的成员函数调用该变量来控制该窗口。例如, C W n d 的M o v e Wi n d o w ( )成员函数调用Windows API::MoveWi n d o w ( )来移动属于该窗口句柄的窗口。 因为M F C是用C + +写成的,所以,程序员可以从C W n d类派生自己的类并在其中增加所需功能。 8第第第一部分第基 础 下载但是,因为这只是面向对象设计的一个模拟,所以 M F C类对操作系统内部工作的控制能 力并不会比任何其他 A P I调用更多。例如,如果对窗口打开方式的修改是 A P I的内部功能,那 么即使使用M F C类来试图改变这一方式也是不可能的。 创建和销毁M F C类 创建M F C类是一个棘手的问题。对封装了类似窗口系统资源的 M F C类来说,程序员不仅 要创建自己的M F C类的实例,还要调用该类的成员函数来创建该系统资源。因此 M F C类的创 建几乎总是分成两步走:1 )创建一个类的示例;2 )创建系统资源。 注意 为什么M F C类不仅仅只是简单地在它们自己的构造函数内创建系统资源呢?这 是因为,创建系统资源是否成功是不可能预先知道的,而类的构造函数又难于访问 (它 甚至不返回错误状态),利用成员函数来完成这一工作则要容易得多。 销毁M F C类也同样棘手。如果类的实例首先被销毁,那么它将简单地在其析构函数内销 毁资源。然而,如果开始就没有资源,那么 Windows API也就没有办法知道有一个 MFC 类实 例必须被销毁。令人惊奇的是,这只是存在于 C W n d类和窗口资源之间的问题,其他类型的资 源并不由用户销毁。 但是用户可能通过单击窗口的关闭按钮来关闭窗口,在这种情况下,不知何故, C W n d对 象必须知道足够的信息来销毁自己以防止出现内存泄漏。幸运的是,当窗口资源被销毁时, 它会发送一个消息,而C W n d对象可以捕获这个消息以用于销毁自身。 因为M F C类对象和系统资源是两种不同实体,所以可以通过编程的方法将两者分开或者 重新组合在一起。例如,通过调用 A t t a c h ( )函数可以将一个 C W n d类对象附着于一个已经存在 的窗口对象上。所有控制系统资源的 M F C类都具有A t t a c h ( )函数和D e t a c h ( )函数。 1.5 Developer Studio基础 为了将 M F C类恰当地运用于应用程序, Developer Studio(开发平台 )提供了几种向导 ( Wi z a r d )工具和编辑器工具: ■ A p p Wizard 用于生成应用程序所需要的基本类文件。所产生的类都派生于 M F C类, 它们在编译后与M F C库链接以创建应用程序。 ■ C l a s s Wizard 用于创建应用程序额外的文件或者为已有的类增加新的成员函数。这些 被创建的类可以由M F C派生。 ■ Dialog Editor 用于创建对话框模板,方法是将控件窗口图标拖到一个空白的框架窗口 内。被创建的模板作为应用程序的资源存储,然后用于在运行时候创建对话框。 C l a s s Wi z a r d 可以直接从Dialog Editor调用以创建一个对话框类,该类则负责创建对话框。 ■ Toolbar Editor 用于创建工具栏和位图资源,而这些资源则又用于创建应用程序的工 具栏。 ■ C u r s o r、I c o n和Bitmap Editor 是简单的图像编辑器,用于创建应用所使用的光标、图 标和位图资源。 ■ Menu Editor 用于创建应用中的菜单条和弹出菜单资源。 ■ String Editor 用于创建字串资源,它可以将文本字符串从应用程序中分离出来,并且 可以很方便地从一种语言转变为另一种语言 (例如从英语转变为法语,而不是从 C + +到J AVA )。 第 1 章第概第第述第第9下载■ Text Editor 用于编辑类文件。 1.6 Windows和MFC总结 以上所述说明了Wi n d o w s、M F C和Developer Studio是如何一起协同工作的。 Wi n d o w s操 作系统创建并支持应用程序,其中包括窗口的创建。 M F C则在C + +类中封装了这一功能,而 Developer Studio则负责创建这些类。 现在来回顾一下M F C提供了哪些类。 1.7 基本类 多数M F C类由下列3种基本类派生:C O b j e c t、C C m d Ta rg e t和C W n d。如上所述,C W n d类 封装了创建和控制窗口的 Windows API,它还允许程序员向窗口处理函数中添加自己的消息 处理过程。C C m d Ta rg e t类则允许没有创建窗口的类也能处理消息,但只能是后面要讨论的所 谓命令消息。 C O b j e c t类为每个从它派生的类提供了许多基本功能,例如得到类对象的大小, 将对象存入一个磁盘文件等等。 1. CObject类 CObject 类本身并没有提供什么重要的功能。该类通过 6种相互配合的宏( m a c r o )完成实际 工作。这些宏使得类在运行时可以从 C o b j e c t类派生以获得类的名字和对象大小。创建一个这 样的类不必要知道类的名字,文档环境存储和接收这种类的实例也不必知道类的名字。 下列宏允许类的实例知道它自己的类名字和运行时的类大小: DECLARE_DYNAMIC( CYourClass ) // in the .h file IMPLEMENT_DYNAMIC( CYourClass, CYo u r B a s e C l a s s ) // in the .cpp file 使用C O b j e c t : : G e t R u n t i m e C l a s s ( )函数可以在运行时使用这些宏来获得与类有关的细节。 另外几个宏包括以上宏的功能,但同时允许类的实例在不知道它的类名字的情况下被创 建: D E C L A R E _ D Y N C R E ATE( CYourClass ) // in the .h file I M P L E M E N T _ D Y N C R E ATE( CYourClass, CYourBaseClass ) // in the .cpp file 使用C O b j e c t : : C r e a t e O b j e c t ( )函数可以利用这些宏创建一个类的实例而无需知道它的类名字。 另外几个宏包括以上所有宏的功能,但同时允许类实例在不知道它的类名字的情况下被 存贮: DECLARE_SERIAL ( CYourClass ) // in the .h file IMPLEMENT_SERIAL ( CYourClass , CYourBaseClass , schema ) // in the .cpp file 2. CCmdTa rg e t类 从C C m d Ta rg e t类派生的类可以接收并处理由应用程序的菜单或者工具栏发出的命令消息。 C C m d Ta rg e t类将在以后的消息机制部分详细讨论。 3. CWnd类 如上讨论,C W n d类的成员函数封装了负责创建和维护窗口的 Windows API。C W n d类派 生于C C m d Ta rg e t类,因此能够接收和处理命令消息。所有其他的控制窗口的 MFC 类都由该 类派生。 10第第第一部分第基 础 下载注意 本章所使用的下列字母用于指出MFC类从以上的何种基类派生: ■ O代表派生于C O b j e c t。 ■ O C代表派生于C O b j e c t和C C m d Ta rg e t。 ■ O C W代表派生于C O b j e c t、C C m d Ta rg e t和C W n d。 1.8 应用类 A p p Wi z a r d以下列4种M F C类为基础,为应用程序产生出一些派生类: 1) CWinApp 也就是应用程序的应用类 (Application Class),它负责初始化并运行应用程 序,这就是以上讨论的程序线程。 2) CFrameWnd 也就是应用程序的框架类 (Frame Class),它负责显示并跟踪用户命令以 及显示应用程序的主窗口。 3) CDocument 应用程序的文档类(Document Class),它负责加载和维护文档。文档可以 是从草稿到网络设置的任何东西。 4) CView 应用程序的视类( View Class),它负责为文档提供一个或者多个视。 注意 这里使用了应用类、框架类等术语,但本书所指的都是以上4种基类的派生类。 这4种类中的哪些类将包括在应用程序中,取决于所创建应用程序的类型。 ■ 对话框应用程序 (Dialog Application),只是简单地拥有作为用户界面的对话框而没有 框架、文档或者视类。对话框应用程序仅利用了应用类 C Wi n A p p的派生类。对话框则用 M F C 的C D i a l o g类创建,这个类将在以后讨论。 ■ 单文档界面应用程序(SDI:Single Document Interface Application),可以一次加载并编 辑一个文档,它使用以上提到的全部 4种基类。 ■ 多文档界面应用程序(MDI:Multiple Document Interface Application),可以一次加载并 编辑几个文档,它使用以上提到的全部 4种基类,此外还增加了两个 C F r a m e W n d的派生类: C M D I F r a m e W n d和C M D I C h i l d W n d。 1.8.1 文档视 C D o c u m e n t和C Vi e w类的派生类负责文档视。 M F C应用程序是面向文档的,这意味着应 用程序负责加载、观察、编辑并存储文档,而文档则可能是文本文件、图形图像或者二进制 配置文件。文档类的工作是将文档从磁盘加载到它的成员变量。然后创建一个或者多个视类 以显示这些成员变量。文档类仅需为文档类对象创建多个视类对象就可以拥有多个视。因为 文档类没有关联的窗口,所以它并不是从 C W n d类派生的,而是从 C C m d Ta rg e t派生的,因而 可以处理命令消息。 1.8.2 CWinApp(OC) 应用类是应用程序运行时创建的第一个对象,并在应用程序执行的过程中最后一个终止。 启动后,应用类就负责创建应用程序的其他对象。 ■ 针对对话框应用程序,应用类用 C D i a l o g创建一个对话框。 ■ 针对S D I应用程序,应用类创建一个或者多个文档模板 (参阅以后内容),然后使用该模 第 1 章第概第第述第第11下载板打开一个空文档。 ■ 针对M D I应用程序,应用类创建一个或者多个文档模板,然后使用该模板在主框架类 内打开一个空文档。 应用类派生于C Wi n A p p并从A p p Wi z a r d中得到类似C X x x A p p的类名字,X x x就是具体应用 的名字。 1.8.3 文档模板 文档模板定义了在应用程序打开一个文档时框架类、文档类和视类的创建结果。为了生 成一个文档模板,必须为SDI 应用程序创建一个C S i n g l e D o c Te m p l a t e类或者为M D I应用程序创 建一个C M u l t i D o c Te m p l a t e类,并用3个类指针对其初始化: p D o c Template = new CMultiDocTe m p l a t e ( I D R _ A P P T Y P E , RUNTIME_CLASS( CAppDoc ), // Your Document Class RUNTIME_CLASS( CChildFrame ), // Your Frame Class RUNTIME_CLASS( CAppView ) // Your View Class ) ; 其中的 R U N T I M E _ C L A S S ( )宏返回一个指向类的 C R u n t i m e C l a s s 结构的指针, D E C L A R E _ D Y N C R E AT E和I M P L E M E N T _ D Y N C R E ATE 宏则将该结构添加到类中。文档模 板通过创建以上3个类的实例并调用其C R u n t i m e C l a s s : : C r e a t e O b j e c t ( )函数打开文档。 1.8.4 线程 C Wi n A p p类本身从C w i n T h r e a d类派生。而C Wi n T h r e a d类则封装了创建和维护系统中具体 应用程序线程的 Windows API。实际上,可以通过创建 C Wi n T h r e a d类的另一个实例以在应用 程序中实现多任务。读者可以参考实例 5 6和实例5 7。C Wi n A p p类代表了应用程序中的执行主 线程。 1.8.5 CFrameWnd(OCW) 框架类是应用程序运行时创建的下一个对象,它负责显示并为应用程序引导用户命令。 对于S D I应用程序,框架类派生于 C F r a m e W n d类,A p p Wi z a r d将自动为它分配一个名字: C M a i n F r a m e。 对于 M D I应用程序,框架类则派生于 C M D I F r a m e W n d类, A p p Wi z a r d也为它分配 C M a i n F r a m e这个名字。同时 M D I应用程序还为每个打开的文档创建一个子框架类 ( C h i l d Frame Class)。每个子框架类都派生于C M D I C h i l d W n d,A p p Wi z a r d自动为子框架类分配名字: C C h i l d F r m。 对话框应用程序没有框架类,正如上面提到的,对话框应用程序由应用类和对话框类组成。 1.8.6 CDocument(OC) 文档类通常是应用程序创建的下一个类,应用程序或者打开一个新文档,或者打开一个 已经存在的文档。文档类负责将文档加载到其成员变量中,并允许视类编辑这些成员变量。 文档可以包括从图像文件到可编程控制器设置等任何内容。 12第第第一部分第基 础 下载文档类派生于C D o c u m e n t类,A p p Wi z a r d自动为其分配的名字为 C X x x D o c,其中X x x是应 用程序的名字。 1.8.7 CView(OCW) 文档类的实例创建之后,紧接着就会创建一个视类的实例。视类负责描述文档类的内容。 视类还允许用户编辑文档。分离窗口类 C S p l i t t e r W n d则允许文档同时具有一个以上的视,这些 视可以由好几个同样的或者不一样的视类创建。 A p p Wi z a r d 允许程序员从下列几个基类派生自己的视类,它们是: C Tr e e Vi e w 、 C E d i t Vi e w、C R i c h E d i t Vi e w和C L i s t Vi e w等等。每一种基类给予应用程序一组不同功能。所有 这些类都从 C Vi e w类派生。无论选择什么基类, A p p Wi z a r d自动为派生类分配的名字为 C X x x Vi e w,其中X x x是应用的名字。 如上所述,可以从这4种基类创建3种类型的M F C应用程序:对话框、S D I和M D I。下面详 细介绍一下这些应用程序类型。 1.8.8 对话框应用程序 对话框应用程序由一个应用类和对话框类组成,应用类是由 C Wi n A p p派生的,对话框则 由一个从对话框类派生的类创建。 1.8.9 SDI应用程序 S D I应用程序由一个派生于 C Wi n A p p的应用类、一个派生于 C F r a m e W n d的框架类、一个 派生于 C D o c u m e n t的文档类和一个或者一个以上的视类组成,这些视类由一种或几种以 C Vi e w类为基类的视类派生。 1.8.10 MDI应用程序 M D I应用程序由一个派生于 C Wi n A p p的应用类、一个派生于 C M D I F r a m e W n d的框架类、 一个或者一个以上派生于 C M D I C h i l d W n d的子框架类和多个文档类和视类组成。其中,每个 子框架类的对应文档派生于 C D o c u m e n t类,每个文档对应的一个或者多个视类则派生于 C Vi e w类。 1.9 其余用户界面类 除了框架类和视类,M F C还提供了几种其他的类来支持用户界面: ■ 通用控件类(Common Control Class),封装了诸如按钮一类的通用控件。 ■ 菜单类(Menu Class),是C W n d类为窗口提供的菜单。 ■ 对话框类(Dialog Class),封装了对话框和通用对话框。 ■ 控件条类(Control Bar Class),封装了控件条(工具栏、对话条和状态栏等 )。 ■ 属性类(Property Class),封装了属性表和属性页。 1.9.1 通用控件类 通用控件类封装了通用控件 (如按钮、列表框等 )的功能。这些类派生于 C W n d类并继承了 第 1 章第概第第述第第13下载其成员函数如S h o w Wi n d o w ( )和M o v e Wi n d o w ( )等等。当这些类创建一个窗口时,它们中的每 一种都将使用一种通用控件窗口类。例如,当 C B u t t o n通用控件类创建一个按钮时,它将使用 B U T TO N窗口类来创建实际的窗口: C r e a t ( _ T ( " B U T TON"), lpszCaption, dwStyle, rect, pParentwnd, nID); 下表列出了这些通用控件类、它们所创建的控件以及所使用的窗口类。 表1-2 MFC通用控件类和它们的窗口类 M F C类 通用控件 Wi n d o w s类 C A n i m a t e C t r l ( O C W ) A n i m a t i o n动画控件 S y s A n i m a t e 3 2 C B u t t o n ( O C W ) 按钮控件 B U T TO N C C o m b o B o x ( O C W ) 组合框控件 C O M B O B O X C E d i t ( O C W ) 编辑控件 E D I T C H e a d e r C t r l ( O C W ) 标头控件 S y s H e a d e r 3 2 C L i s t B o x ( O C W ) 列表框控件 L I S T B O X C L i s t C t r l ( O C W ) 列表控件 S y s L i s t Vi e w 3 2 C P r o g r e s s C t r l ( O C W ) 进度控件 m s c t l s _ p r o g r e s s 3 2 C S c r o l l B a r ( O C W ) 滚动条控件 S C R O L L B A R C S l i d e r C t r l ( O C W ) 滑块控件 m s c t l s _ t r a c k b a r 3 2 C S p i n B u t t o n C t r l ( O C W ) 上/下按钮控件 m s c t l s _ u p d o w n 3 2 C S t a t i c ( O C W ) 静态控件 S TAT I C C Tr e e C t r l ( O C W ) 树型控件 S y s Tr e e Vi e w 3 2 C Ta b C t r l ( O C W ) 标签控件 S y s Ta b C o n t r o l 3 2 C D a t e Ti m e C t r l ( O C W ) 日期/时间获取控件 S y s D a t e Ti m e P i c k 3 2 C M o n t h C a l C t r l ( O C W ) 日历控件 S y s M o n t h C a l 3 2 C H o t K e y C t r l ( O C W ) 热键控件 m s c t l s _ h o t k e y 3 2 C T o o l T i p C t r l ( O C W ) 工具提示控件 t o o l t i p s _ c l a s s 3 2 并不是所有的通用控件类都只是简单地封装一个通用控件窗口类。有 3种M F C类实际上 提供的功能在通用控件内就找不到。表 1 - 3显示了这些类、派生它们的 M F C类和它们所提供 的支持。 表1-3 MFC派生类和它们的基类 M F C类 M F C继承类 新增的功能 C B i t m a p B u t t o n C B u t t o n 对按钮上的位图提供了更好的支持 C C h e c k L i s t B o x C L i s t B o x 列表框中的复选框 C D r a g L i s t B o x C L i s t B o x 列表框中的用户可拖动项目 1.9.2 菜单类(O) C M e n u类封装了创建和维护菜单的 Windows API。C M e n u有两个成员函数: A t t a c h ( )和 D e t a c h ( )。这两个函数允许程序员采用类似 C W n d类对象包含一个已有窗口的方法包含一个已 14第第第一部分第基 础 下载有菜单。 1.9.3 对话框类 C D i a l o g类封装了创建对话框的 Windows API,对话框创建时是一种弹出窗口,可以向对 话框类增加在对话框模板中定义的控件窗口。 1.9.4 通用对话框MFC类 M F C库还有6种通用对话框类,它们封装了创建通用对话框的 Windows API。通用对话框 是预先加入了控件的对话框,它们以一些普通请求信息提示用户,如要装载和存储的文件名、 颜色、字体和打印参数等。它们简化了由程序员自己编写这些对话框的工作,而且向用户展 示了他们所熟悉的Wi n d o w s对话框。 表1 - 4显示了通用对话框的功能、提供的 Windows API以及封装的M F C通用对话框类。 表1-4 通用对话框类及其使用 通用对话框 Window API调用 M F C类 选择颜色 : : C h o o s e C o l o r ( ) C C o l o r D i a l o g 打开/保存文件 : : G e t O p e n F i l e N a m e ( ) C F i l e D i a l o g : : G e t S a v e F i l e N a m e ( ) “查找”或“替换文本” : : F i n d Te x t ( ) C F i n d R e p l a c e D i a l o g : : R e p l a c e Te x t ( ) 选择字体 : : C h o o s e F o n t ( ) C F o n t D i a l o g 打印页面设置 : : P a g e S e t u p D l g ( ) C P a g e S e t u p D i a l o g 打印 : : P r i n t D l g ( ) C P r i n t D i a l o g 使用通用对话框的实例可以参考实例 2 3和2 4。 1.9.5 控件条类 (OCW) 控件条类封装了向应用程序提供工具栏、状态栏、对话条和伸缩条的 Windows API。第2 章将更详细地讨论它们。 ■ C To o l B a r ( O C W )和C To o l B a r C t r l ( O C W )类创建并维护工具栏。 ■ C S t a t u s B a r ( O C W )和C S t a t u s B a r C t r l ( O C W )类创建并维护状态栏。 ■ C D i a l o g B a r ( O C W )类创建并维护对话条。 ■ C R e b a r ( O C W )和C R e b a r C t r l ( O C W )创建并维护伸缩条。 1.9.6 属性类 属性类封装了向应用程序提供属性页和属性表功能的 Windows API。属性表由一个或一个 以上的属性页组成,它可以创建 Wi n d o w s用户熟悉的标签视。该视一般用于选择程序选项。 ■ C P r o p e r t y S h e e t ( O C W )类创建属性表,C P r o p e r t y S h e e t类并不是从C D i a l o g类派生的,但 它们很相似。 ■ C P r o p e r t y P a g e ( O C W / C D i a l o g )类创建属性页,它是从C D i a l o g派生的。 第 1 章第概第第述第第15下载1.10 绘图类 C D C类封装了早先讨论的设备环境以及所有需要设备环境的绘图函数 (这个比例不小)。除 了C D C类以外,还有另外4种从C D C派生出来的M F C类提供了附加功能,包括: ■ C C l i e n t D C类,一般用来方便地创建和销毁一个设备环境。 C C l i e n t D C通常在堆栈上创 建。它的构造函数通过调用 C D C : : G e t D C ( )为窗口客户区创建一个设备环境。当子程序返回时, C C l i e n t D C的析构函数通过调用C D C : : R e l e a s e D C ( )函数销毁该设备环境。这样将不会存在忘记 释放的设备环境,从而也不会导致内存资源泄漏。 ■ C Wi n d o w D C类,与C C l i e n t D C类相似,但它的工作范围是窗口的非客户区。 ■ C P a i n t D C类,在构造并得到设备环境后将调用 C W n d : : B e g i n P a i n t ( )函数,这种情况下 的设备环境仅允许绘制无效化的窗口客户区而不是绘制整个窗口。销毁 C P a i n t D C类在时将调 用C W n d : : E n d P a i n t ( )函数。 ■ C M e t a F i l e D C类,用于创建元文件,如上所述,元文件是一种磁盘文件,它包含了所 有的绘图行为以及绘制图像所需要的绘图模式。可以通过打开一个元文件设备环境来创建元 文件,然后使用绘图工具来绘制它,就好像这个文件是计算机屏幕或者打印机设备。所产生 的文件可以在其他设备上重新读取以创建图像。 绘图工具和类 M F C使用类来封装绘图工具的特性: C P e n代表画笔,C B r u s h代表画刷,C F o n t代表字体, C P a l e t t e代表调色板,C B i t m a p s代表位图,C R e g i o n则代表区域。这些类中的每一种创建相关 联的图像对象,然后将其选入设备环境。 1.11 其他MFC类 并不是所有的M F C类都会影响到用户界面。几种其他的 M F C类还封装了控件文件、数据 库和Wi n d o w s套接字的A P I。还有一些类负责维护数据集合 (列表、数组和映射等)。 1.11.1 文件类 C F i l e ( O )类封装了创建并维护一个普通文件的 Windows API。有3种M F C类是从C F i l e类派 生并提供了附加功能: ■ C M e m F i l e类允许在内存而不是在磁盘上创建文件。在构造一个C M e m C l a s s对象的时候, 文件被立即打开,可以使用成员函数对该文件读写,就好像它是一个磁盘文件一样。 ■ 除了用于创建文件的内存位于全局堆上之外, CShared File类与C M e m F i l e完全类似。 这使得它可以通过剪贴板和 D D E共享。 ■ C S t d i o F i l e类允许读写以回车换行控制符结尾的文本字符串。 1.11.2 CArchive和序列化 C A r c h i v e类在序列化的过程中使用 C F i l e类将文档的类对象存入磁盘。采用序列化,类中 的成员变量和整个类对象可以按照某个顺序存入一个归档设备,以后则可以按照同样的顺序 恢复。 16第第第一部分第基 础 下载1.11.3 数据库类 M F C库有一些支持两种数据库的类: ■ ODBC(Open Database Connectivity)类封装了多数数据库开发商所支持的 ODBC API。 如果应用程序使用 M F C的O D B C类,它可以支持任何遵守 O D B C标准的数据库管理系统 (DBMS:Database Management System)。 ■ DAO(Data Access Object)类支持由Microsoft Jet数据库引擎所优化的更新数据库 A P I。 使用该引擎仍然可以访问O D B C兼容数据库或者其他数据库。 1.11.4 ODBC类 有3种O D B C类: 1) CDatabase(O)类使用ODBC API打开DBMS 数据库。在构造一个 C D a t a b a s e对象以后, 可以使用它的O p e n E x ( )函数与一个数据库建立连接。调用 C D a t a b a s e的C l o s e ( )函数则可以关闭 该连接。 2) CRecordset类用于通过数据库连接来存储和接收记录。 3) CDBVa r i a n t类代表了记录中的一列,它并不关心其数据类型。 1.11.5 DAO类 D A O类有3种类似O D B C的类,它们是: 1) CDaoDatabase(O)类打开D A O数据库。 2) CDaoRecordSet(O)拥有记录。 3) COleVa r i a n t表示记录列。 D A O类还包括以下3种类: ■ C D a o Wo r k S p a c e ( O )类,管理数据库会话,该会话允许执行事务 (存储到数据库 )或者 不执行。 ■ C D a o Q u e r y D e f ( O )类,表示一个查询定义。 ■ C D a o Ta b l e D e f ( O )类,表示一个表的定义,包括域和索引结构。 1.11.6 数据集合类 数据集合类维护并支持数组、列表和数据对象映射。 ■ C A r r a y类和它的派生类支持数据对象数组。一个数组由一个或者多个同样数据对象 (如:整型数、类 )所组成,它们在内存中相邻,因而可以用简单的索引来访问。 C A r r a y类可 以动态地增大或者减小其大小。 C A r r a y类有几种派生类 (如C B y t e A r r a y和C D Wo r d A r r a y )允许 创建某种安全类型的数组,当然,可以用 C A r r a y < t y p e,a rg _ t y p e >模板创建类创建任何安全 类型的数组。 ■ C L i s t类和它的派生类支持数据对象的链表。链表由一个或者一个以上的相同类型数据 对象(如:整型数、类)所组成,它们在内存上不连续,通过双向链接可以前后遍历链表。几种 C L i s t类的派生类 (如C P t r L i s t和C O b L i s t )允许创建安全类型链表。当然,可以用 C L i s t < t y p e, a rg _ t y p e >模板类创建任何安全类型的 C L i s t。 第 1 章第概第第述第第17下载■ C M a p类和它的派生类支持数据对象的字典。数据字典在二进制或者文本键值下存储一 个或者一个以上同样类型的数据对象 (如:整型数、类)。可以使用该键值接收数据。例如,因 为Wi n d o w s并不跟踪哪个M F C C W n d对象属于哪个窗口。应用程序就可以使用一个 C M a p对象 类将窗口句柄,与其对应的 C W n d对象相关联。 C M a p类有几种派生类 (如C M a p Wo r d To P t r, C O b To S t r i n g )允许创建保护类型的字典。当然,可以用 CMap模板类创建任何安全类型的 C M a p。 1.11.7 通信类 M F C库提供了一些类以使得应用程序通过网络或 I n t e r n e t进行通信。这些类将在第 3章详 细讨论。 1.12 类的消息机制 本章结束之前最后需要学习的内容是 M F C如何处理消息。如上所述,每个窗口都有一个 窗口处理过程处理任何发送到该窗口的消息。同时,窗口处理过程一般还写成使用 s w i t c h - c a s e语句的判断模块以对不同消息分类各自处理。实际上,这种判断模块可能会很大,而且 难以维护。M F C的解决方案是将这些消息重定向到 C W n d类的成员变量。这样就可以利用类 的安全性和方便性来处理消息。 M F C甚至还可以发送一些消息给其他非 C W n d派生类处理,这在技术上叫做命令路由,以 后章节将详细讨论它。 1.12.1 MFC如何接收一个寄送消息 消息既可以被发送也可以被寄送。发送的消息在本质上与直接调用窗口处理过程一样, 就好像它是另一个函数。寄送消息则进入一个在应用程序初始化时操作系统所建立的消息队 列中。鼠标和键盘单击一般产生寄送消息,然后应用程序一个接一个地将它们从消息队列中 删除,并将它们发送到被鼠标单击的窗口或者按键按下时接收输入的窗口。 Windows API提供了两个调用函数,G e t M e s s a g e ( )和P e e k M e s s a g e ( ),它们允许应用程序从 队列中删除消息。 M F C类C Wi n T h r e a d将这些函数调用封装到 R u n ( )函数中。R u n ( )是M F C应用 程序开始执行时最后被调用的函数。然后 R u n ( )函数就不断检查消息队列以等待用户的按键或 者鼠标单击等操作。R u n ( )函数还执行一些M F C类的后台维护工作并为程序员提供机会进行自 己的维护工作。 读者可以参考实例4 7和4 8以充分理解运用这一机制的有关内容。 一旦从消息队列中删除一条寄送消息,并将其发送到一个窗口,对它的处理就与接下来 所讨论的发送消息一样了。 1.12.2 MFC如何处理接收的消息 所有M F C被控窗口的基本窗口处理过程是静态函数 A f x W n d P r o c ( )。只要决定使用M F C将 一个窗口子类化,那么该函数的地址就由 M F C将其附着于适当的窗口对象。当一个消息到来 时,A f x W n d P r o c ( )就以某种方法调用成员函数来处理该消息并把其余的消息传递给原始窗口 18第第第一部分第基 础 下载处理过程。 但除了这种简单的消息处理过程之外, M F C还在消息机制中建立了两类与 A f x W n d P r o c ( ) 函数不同的附加特性:命令路由和消息反射。 ■ 命令路由(Command Routing),几乎所有由菜单和工具栏产生的命令消息都转发到主框 架类窗口进行处理。因为许多这样的消息在其他应用类中可能会得到更好的处理。例如视类 或者文档类,所以 A f x W n d P r o c ( )将自动地通过一种名叫命令路由的过程将这些消息发送给应 用程序。该过程还允许M F C不用控件窗口(例如,文档类)就能处理命令消息。 ■ 消息反射(Message Reflection),正如上面提到的,控件通知消息的发送目标是控件的 父窗口而不是控件自己。因此单独将控件窗口以 A f x W n d P r o c ( )子类化将不会允许在自己的控 件类中处理这些消息。因为在控件父窗口中增加控件特定代码是违反面向对象编程原则的, 所以父窗口类的A f x W n d P r o c ( )函数自动地将这些消息弹回控件类以允许其处理通知消息,这 个过程就叫作消息反射。 最后要考虑的就是 A f x W n d P r o c ( )的性能问题。如果使用标准的 C + +技术,那么将到来 的消息引导到成员函数的最简单方式就是为每个可能的消息重载基类中的哑元函数。如果 从这些类派生并重载了这类函数,这些消息就可以由自己来处理了。但是,任何涉及窗口 的活动一般都要产生一大堆窗口消息。如果每一个这样的消息都要通过这些哑元函数来过 滤的话,这样大的开销是不允许的。因此在基类中不会有哑元函数,作为一种替代方式, 从基类中派生的每个类将具有一个成员函数和它们处理的消息之间的嵌入消息映射。 A f x W n d P r o c ( )则直接访问该消息映射 (Message Map)。如果到来的消息没有对应的映射入口, 该消息就很快地被转发给原始窗口处理函数。为了提高这个速度, M F C甚至不惜使用汇编 程序来处理。 A f x w n d P r o c ( )函数使用6个辅助函数,并按照下列调用顺序来完成所有以上功能: 1) AfxWndProc()接收消息,找出消息所属的 C W n d对象然后调用A f x C a l l W n d P r o c ( )函数。 2) AfxCallWndProc()存储消息(消息I D和参数)供今后索引,然后调用 Wi n d o w P r o c ( )函数。 被存储的消息用于不处理该消息的情况。 3) Wi n d o w P r o c ( )运行期间发送消息给 O n w n d M s g ( )函数,如果不处理消息则传送给 D e f Wi n d o w P r o c ( )函数。 4) OnWndMsg()将消息按照其类型分类,对 W M _ C O M M A N D消息, O n W n d M s g ( )调用 O n C o m m a n d ( )函数。对W M _ N O T I F Y消息,O n W n d M s g ( )调用O n N o t i f y ( )函数。剩下的则是窗 口消息,O n W n d M s g ( )于是搜索消息映射找寻对应的消息处理器。如果没有找到,它就将消息 返回给Wi n d o w P r o c ( )函数,该函数则随后将消息发送给 D e f Wi n d o w P r o c ( )函数处理。 5) OnCommand()检查消息是否是一个控件通知消息 ( l P a r a m的值不是N U L L )。如果是, O n C o m m a n d ( )将把消息反射回发出通知消息的控件。如果不是控件通知消息,或者控件拒绝 被反射的消息,那么此时O n C o m m a n d ( )就调用O n C m d M s g ( )函数。 6) OnNotify()也将消息反射回控件,如果不成功则调用同样的 O n C m d M s g ( )函数。 7) OnCmdMsg()函数,取决于是什么类接收消息,它会暗中利用命令路由过程以发送命 令消息和控件通知消息,例如,如果拥有某个窗口的类是框架类,命令和通知消息就会被发 送到视类和文档类中以寻找该消息的消息处理器。 为回顾这一过程,请参见图 1 - 3。 第 1 章第概第第述第第19下载图1-3 MFC消息处理概述 1.12.3 UI对象 O n C m d M s g ( )不仅是处理命令消息的辅助函数,还用于自动地启用、禁用以及标识菜单项、 状态栏窗格和工具栏按钮等。换句话说,既然 O n C m d M s g ( )函数已经为将命令从菜单项传送到 类的成员提供了所有必要的底层支持,那么为什么不让类的成员来修改菜单项的外观呢? 只要用户拉下菜单,主框架类将通过调用 O n C m d M s g ( )函数遍历该菜单所有的命令 I D。 但并不是查找一个成员函数来处理这个命令, O n C m d M s g ( )函数只是查找一个可能在该菜单项 处增加标记或者改变其文本、将其禁用的成员函数。 只要应用程序空闲等待一个新的寄送消息,它就使用 O n C m d M s g ( )函数遍历所有的工具栏 按钮和状态栏窗格,并再次查找用户界面处理器以改变它们的外观。 1.13 小结 本章回顾了Wi n d o w s应用程序的基础知识,包括其窗口、消息机制和绘图机制,并讨论 了M F C如何封装Windows API以提供一种相当不错(但还不是完美)的C + +接口来创建Wi n d o w s 应用。此外还回顾了一些重要的 M F C类,以及M F C如何引导窗口消息到类的成员函数。 本章讨论了很多内容,但只是希望作为一个已有知识的回顾。如果要了解这方面的更多 知识,请参考《Visual C++ MFC编程实例》(已由机械工业出版社出版)。 第2章将深入讨论一个问题:控制条。 20第第第一部分第基 础 下载 执行完这些步骤后,消息可在类 的内部进行处理,控件通知将反 馈回相应的控件,命令消息也可 以在应用程序内进行传送 O n C o m m a n d ( )和O n N o t i f y ( ) 向控制类 (没有显示)发送回控 件通知。任何没有发送回的 消息将发送到 O n C m d M s g ( ) A f x W n d C a l l P r o c ,( )保存消息, 然后调用发现类 的Wi n d o w P r o c ( ), 以后所有的调用 函数均将被重载 M F C创建的发送到 所有窗口的消息将 由A f x W n d P r o c ( ) 处理 应用程 序消息 队列 消息泵 所有未由 O n W n d M s g ( )处理 的消息都被发送给 D e f Wi n d o w P r o c ( ) A f x W n d P r o c ( )查找到拥 有该窗口的类对象,并 调用A f x C a l l W n d P r o c ( ) 随后Wi n d o w P r o c ( ) 调用O n W n d M s g ( ) O n W n d M s g ( )向O n C o m m a n d ( ) 发送W M _ C O M M A N D S,向 O n N o t i f y ( )发送W M _ N O T I F Y, 其余的则在发现类的消息映射 中查找处理函数 O n C m d M s g ( )可在另 一个类中调用 O n C m d M s g ( )。否则 它将查找类的消息映 射以查找处理函数下载下载 第 2 章 控 制 条 从第1章中可以注意到,通用控件窗口是由 Windows API提供,用于创建按钮、列表框和 编辑框等控件的特定子窗口。虽然工具栏可能看起来像这些控件窗口的集合,但实际上它只 是一种较长的控件窗口,该控件窗口在本窗口中绘制按钮。状态栏也只是一个较长的窗口, 不过它可以在自己的边界以内绘制其窗格。工具栏和状态栏都代表了叫做通用控制条的一类 特定通用控件窗口。 本章仅探讨如何使用Windows API创建通用控制条。然后研究利用 M F C类通过A P I所提供 的许多功能来创建它们。最后则看看由这些控制条所绘制的按钮和窗格的可用风格。 2.1 通用控制条 通用控制条有3种:工具栏、状态栏和伸缩条。工具栏和状态栏在 Wi n d o w s的各个版本中 都可以见到。另一方面,伸缩条 ( R e b a r )只能在Windows 98或者以后版本中以及系统安装了 I E 4 . 0后才可以使用,本章最后将讨论伸缩条。工具栏是人们熟悉的控件窗口,它沿着应用主 窗口的顶端和四周布局并包含了多个按钮。状态栏通常位于主窗口的底部,它指示键盘状态 并显示帮助消息。 初看起来,用户可能会觉得工具栏上的按钮是一排使用通用控件类 B U T TO N创建的子窗 口,而状态栏则可能被认为是 S t a t i c控件的集合。实际上,这两者都是自己绘制并控制这些按 钮图像,请参见图2 - 1。 图2-1 工具栏和状态栏控制窗口 那么,为什么控制条要不厌其烦地重复创建这个功能而不使用已有的通用控件呢?答案 是控制条自己做这些工作可以节约许多开销。在创建窗口时, 5 0 %以上的时间花费在背景定 位和资源初始化上。具有 1 0个或者1 0个以上按钮的工具栏采用 1个绘图处理器比在1 0个处理器 中画得快得多。 虽然这些看起来像装载 了控件窗口的父窗口, 但它们中的每一个实际 上都是属于主框架窗口 的细而长的控件窗口 这些按钮 由工具条 控件窗口 绘制 这些窗格 由状态栏 控件窗口 绘制2.2 用API创建控制条 现在看看只采用A P I该如何创建并初始化控制条。因为这个工作的大部分可以由 M F C来完 成,这里的介绍就不拘泥于细节了。 创建工具栏或者状态栏的 Windows API与创建通用控件的A P I是一样的,只是其窗口类结 构不同。可以使用下列语句创建工具栏: HWND hWnd = :: CreateWindowEx(..., "To o l b a r Wi n d o w 3 2 " , . . . ) ; 工具栏控件可以由一个定义了一套按钮外观的位图对象来初始化。为了定义这个位图对 象,可以向这个控件发送一个如下所示的 T B _ A D D B I T M A P窗口消息: ::SendMessage (hWnd, TB_ADDBITMAP, nNumButtons,&tbab); 这里使用的 t b a b参数是一个 T B A D D B I T M A P结构,该结构包含了位图对象的句柄。 n N u m B u t t o n s参数则告诉工具栏该位图代表了多少个按钮表面。一旦该位图被定义,就可以发 送如下的T B _ A D D B U T TO N S消息以告诉工具栏绘制什么按钮以及在哪里绘制它们: ::SendMessage (hWnd, TB_ADDBUTTONS, nNumButtons, &tbbutton); 这里使用的t b b u t t o n参数代表一个定义了每个按钮的 T B B U T TO N结构数组。该结构指出 针对具体的按钮绘制什么样的按钮表面以及按钮被单击之后产生什么命令消息 I D。 可以使用下列语句创建状态栏: HWND hWnd = :: CreateWindowEx (..., "msctls_statusbar32" ,...); 状态栏由通常显示文本的窗格组成。可以使用 S B _ S E T PA RT S窗口消息创建这些窗格: ::SendMessage (hWnd, SB_SETPA RTS, nParts, Wi d t h s ) ; n P a r t s参数告诉控件要创建多少个窗格,而 Wi d t h s参数则是一个整型数数组,它定义了以 像素为单位计算的每个窗格的大小。可以使用 S B _ S E T T E X T消息在窗格上添加文本: ::SendMessage (hWnd, SB_SETTEXT, nPane, "pane text"); n P a n e告诉状态栏控件在哪个窗格上添加文本。 控制条风格 对所有通用控件,工具栏和状态栏都可以用标准的 Wi n d o w s 风格 ( W S _ C H I L D、 W S _ V I S I B L E等)创建,也可以使用它们自己的风格来创建。工具栏风格有一个 T B S T Y L E _前 缀,而状态栏风格则具有 S B A R S _前缀,但现在只用一种状态栏风格。这两种风格的实例可以 参见表 2 - 1。除了这些控件风格以外,这些控制条都还共享了一套名叫 Common Control S t y l e s (通用控件风格)的通用风格。这些风格具有前缀 C C S _并且倾向于包括所有控制窗口共有 的风格。然而,因为控件窗口的特征不同,这些风格多数仅被控件采用。通用控件风格可以 参见表2 - 2,了解这些风格如何影响控制条请参见图 2 - 2。 除了这些控件风格外,所有这些控件还有一个公共的风格集合,称为 common Control Styles (通用控件风格)。这些风格的前缀也为 C C S-,其目的是要包含所有控件窗口的所有公 共风格。但是,因为控件窗口的不同特性,这些风格大多由控件条使用。表 2 - 2中列出了这些 通用控件风格。要想了解这些风格对控件条的影响,请参见图 2 - 2。 22第第第一部分第基第第础 下载表2-1 一些工具栏和状态栏风格 风 格 使 用 说 明 T B S T Y L E _ F L AT 使工具栏上的按钮表面平坦,意味着按钮的边界不会被绘制出来,除非鼠标指针指 在上面 T B S T Y L E _ L I S T 按钮文本显示在按钮的左边 T B S T Y L E _ TO O LT I P S 如果用户将鼠标指针停留在按钮上较长时间就使按钮产生 T T N _ N E E D T E X T通知消 息。程序员需要负责为这个工具提示提供说明文本 T B S T Y L E _ W R A PA B L E 使控件将其按钮排成多行以适应当前的控件大小 SBARS_SIZEGRIP 唯一的状态栏风格,使状态栏在控件底部显示熟悉的把手条。因为控件正常情况下定 位于应用窗口的底部,所以该状态栏看起来影响了整个窗口 表2-2 通用控件风格 风 格 使 用 说 明 C C S _ N O R E S I Z E 被创建的控制条的大小由 : : C r e a t e Window() API调用来指定。该风格忽略以下 4种风 格:这些风格则忽略指定的大小 C C S _ TO P 使控制条一开始沿其父窗口框架的顶部 /底部对齐和延伸。高度则被设置为系统标 C C S _ B O T TO M 准。C r e a t e Windos() API调用中指定的大小则被忽略。注意:如果父窗口被重置大小, 则必须自己重置控制条大小 C C S _ L E F T 同上,但控制条必须沿父框架左 /右对齐并且其宽度设置为系统标准 C C S _ R I G H T C C S _ N O D I V I D E R 通用控制条自动地沿其顶部绘制一条直线:通常用来将工具栏和菜单分隔开 C C S _ A D J U S TA B L E 允许用户动态地配置其工具栏,这并不是 Developer Studio提供的良好方式,请参 看实例9以了解这一Developer Studio方法 C C S _ N O M O V E Y 这些风格或者是没有明显的效果或者是重复了以上所述的风格 C C S _ N O M O V E E X CCS_NOPARENTALIGN 图2-2 工具栏风格实例 第 2 章控控控制控条控控23下载 即使是左对齐,工具 条仍然垂直绘制 工具条的尺寸和位置 可在创建时指定 工具条中的按钮被设定为特定控 件尺寸,但 C C S_W R A PA B L E不 能与C C S _ L E F T一起使用如上表和上图所示,控制条可用窗口风格是相当少的,即便加上通用控件风格也是这样。 控制条在控件的顶部(而不是在其底部或是左边、右边 )自动地绘制一条直线,而且也没有垂直 放置通用控制条的风格(可以沿主窗口的左边或者右边对齐控制条,但按钮会消失在控件之外)。 此外,创建的控制条不能自动地与其他控制条共享主窗口的客户区。事实上,必须手工重置 其大小并移动它们以使视和控制条可见。通用工具栏的另一个难题是用户不能移动它们。现 在的多数应用程序中的工具栏可以移动到应用主窗口的任意一边或者浮动在其自己特定的窗 口框架上。然而,由 Windows API提供的控制条不支持以上任何功能。为了弥补这个功能上 的缺陷,M F C提供了几个类来封装并增强应用程序的控制条功能。 2.3 用MFC创建控制条 M F C提供的两种类封装了创建工具栏和状态栏的 Windows API(与在第1章中所述的C W n d 封装Windows API 创建窗口一样)。这些类是C To o l B a r C t r l和C S t a t u s B a r C t r l类。 2.3.1 CToolBarCtrl和CStatusBarCtrl 为了用M F C创建工具栏,使用以下代码: C ToolBarCtrl tb; tb.Create ( DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID ); 为了向该工具栏中增加位图,使用如下语句: tb.AddBitmap(int nNumButtons, CBitmap* pBitmap ); 最后,按照如下语句定义一个实际工具栏按钮: tb.AddButton(int nNumButtons, LPTBBUTTON lpButtons ); l p B u t t o n s参数与以上用A P I定义按钮的T B B U T TO N S数组是一样的。为了创建状态栏,使 用如下代码: CStatusBarCtrl sb; sb.Create( DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID ); 可以使用如下代码增加窗格和文本: sb.SetParts( int nParts, int* pWidths ); s b . S e t Text( LPCTSTR lpszText, int nPane, int nType ); p Wi d t h s和n P a n e参数在以上A P I版本中已经用过。 正如所看到的,这两个类立即带来的好处就是使得 C + +程序中与A P I对话变得容易多了。 然而,由于它们只是简单地封装了 A P I而没有增加附加的功能,所以由它们所创建的工具栏和 状态栏不会比A P I版本增加更多的功能。它们也不能移动或被用户停靠或者垂直对齐。它们仅 能用于在控制条内更方便地设置并控制按钮和窗格,所以,为了得到这些所希望的功能就必 须创建自己的控制条,这就要用到另外两个 M F C类: C To o l B a r和C S t a t u s B a r。 2.3.2 CToolBar和CStatusBar 为了使用C To o l B a r创建工具栏,需要再次使用下列熟悉的 C r e a t e ( )成员函数: 24第第第一部分第基第第础 下载C ToolBar tb; tb.Create( CWnd* pParentWnd, DWORD dwStyle, UINT nID ); 如上所示,创建工具栏已经是一件很容易的事,而且不需要考虑 r e c t参数。但最大的方便 还在于装载位图和定义按钮的时候: t b . L o a d ToolBar(UINT id); 此时不用编程定义位图和每个按钮,使用 Toolbar Editor创建一个将要引用的资源即可。 L o a d ToolBar 函数接下来负责将资源翻译为 A P I调用以设置该工具栏。 状态栏类也很容易使用,如下代码所示: CStatusBar sb; sb.Create( CWnd* pParentWnd, DWORD dwStyle, UINT nID ); sb.SetIndicators( const UINT* lpIDArray, int nIDCount ); 这时不用为状态栏定义文本和窗格的数目,可以使用 String Editor创建一组在I D A r r a y数 组中定义的字符串,该数组则被传送给 S e t I n d i c a t o r s ( )函数。 使用这些类不仅使装载和配置控制条变得更容易,而且还可以得到下列增强的功能: ■ 使用C To o l B a r类,工具栏中使用的位图的颜色自动地随当前按钮颜色而变化。淡灰色 是标准的Wi n d o w s按钮表面色,但是,如果用户确定为自己的桌面选定一种新的色彩方案 (例 如淡紫红色),那么如果工具栏仍保持淡灰色,按钮看起来就好像偏离了本来的位置。因此, C To o l B a r将它们转变成当前的按钮颜色,所有这些替代颜色如表 2 - 3所示 表2-3 CTo o l B a r位图颜色转换 工具栏位图颜色 系 统 颜 色 工具栏位图颜色 系 统 颜 色 黑 C O L O R _ B T N T E X T 亮灰 C O L O R _ B T N FA C E 暗灰 COLOR_BTNSHADOW 白色 COLOR_BTNHIGHLIGHT ■ 由C To o l B a r和C S t a t u s B a r提供的其他成员函数则建立在 Windows API的功能之上。例如, C To o l B a r : : S e t B u t t o n Te x t ( )函数允许直接设置工具栏按钮的颜色而不用关心所有做出这一改变 的A P I调用。 ■ 这些类还自动提供了启用或者禁用工具栏按钮和状态栏窗格或者对其设置复选状态的 能力。换句话说,除非工具栏是用 C To o l b a r创建的,否则 O n C m d U I ( )消息处理器不会启用或 者禁用工具栏按钮。 按照 C + +的继承思想,可以认为 C To o l B a r和C S t a u s B a r 类是从 C To o l B a r C t r l和 C S t a t u s B a r C t r l类派生的,因此可以得到这两者的功能。但是,微软决定从一种名叫 C C o n t r o l B a r的新类来派生C To o l B a r和C S t a u s B a r,这种新类包含两者共同的新功能。那是否意 味着微软必须在C To o l B a r中复制C To o l B a r C t r l类的功能呢?答案是“不”,这是因为C To o l B a r 和C To o l B a r C t r l都通过发送消息给一个控件的窗口句柄来同控件对话,这样,事实上可以同时 拥有一个C To o l B a r和C t o o l B a r C t r l类的实例来控制同样的工具栏并共享一个窗口句柄。实际上, C To o l B a r类的一个名叫G e t To o l B a r C t r l ( )的成员函数将自动地创建一个 C To o l B a r C t r l类的实例 并指向同一个工具栏。状态栏也提供了类似的函数 G e t S t a t u s B a r C t r l ( )。请参见图2 - 3了解它们 是如何协同工作的。 第 2 章控控控制控条控控25下载图2-3 控制条的类层次 2.3.3 CControlBar C To o l B a r和C S t a t u s B a r类都从 C C o n t r o l B a r类派生,这就可以允许微软将下列功能加入 C C o n t r o l B a r: ■ 如果状态栏或者工具栏不处理下列任何窗口消息, C C o n t r o l B a r就将它们发回给控制条 的宿主窗口处理。 W M _ N O T I F Y W M _ D E L E T E I T E M W M _ C O M M A N D W M _ C O M PA R E I T E M W M _ D R AW I T E M W M _ V K E Y TO I T E M W M _ M E A S U R E I T E M W M _ C H A RTO I T E M 通常情况下,窗口消息如果不由所关注的窗口处理则将被丢弃。这个功能允许工具栏将 按钮命令传回给框架窗口以便这些消息可以由与处理菜单命令相同的命令处理器来处理。 ■ 正如上面提到的, Windows API提供的控制条只能沿它们的顶部绘制边界。 C C o n t r o l B a r则可以在工具栏和状态栏的任何一边绘制边界。 ■ 同上所述,Windows API提供的控制条不能垂直确定工具栏。状态栏可以沿窗口的左 边或者右边对齐,但它们的按钮将会水平地消失在控件之外。即便控制条采用了 T B S T Y L E _ W R A PA B L E风格也不会作为单列按钮出现。 C C o n t r o l B a r类则可以创建一个按钮单 列。 注意 状态栏不能垂直排列,即使采用了C C o n t r o l B a r也是如此,不过,垂直的状态栏 也不怎么符合用户的审美习惯。 ■ 同上所述,以前用户必须自己保证 Windows API控制条不会互相覆盖或者覆盖窗口视。 而使用C C o n t r o l B a r类以及以上所述两种控制类,将会自动地保证控制条不会互相覆盖。对于 其他类以及它们是如何表现的,读者可以参考 2 . 7节“视和控制条如何共享客户区”。 ■ C C o n t r o l B a r类还为工具栏上的按钮和状态栏上的窗格提供工具提示和在线帮助。工具 提示( Tool tip)帮助,又称为气泡(bubble) 帮助,在鼠标指针停留在一个按钮或者窗格之上时将 出现一个小小的白色帮助窗口,这就是工具提示帮助。若在鼠标指针停留在菜单选项或者工 26第第第一部分第基第第础 下载 C To o l B a r创建工 具条并通过发送窗 口消息来控制它 因此C To o l B a r不需 要从C To o l B a r C t r l 派生以获得其功能 即使C To o l B a r C t r l 没有创建工具条仍 然可以通过发送窗 口消息来控制它具栏按钮之上时则会在状态栏上出现在线帮助。 使用C C o n t r o l B a r类可以具有表2 - 4所示的几种新的控制条风格,除了 C C S _ A D J U S TA B L E 以外,程序员应该对 C To o l B a r和C S t a t u s B a r控制条使用这些风格而放弃以前所述的 C C S _类别 风格。T B S T Y L E _和S B A R S _风格则可以继续使用。 表2-4 MFC控制条风格 风 格 使 用 说 明 C B R S _ A L I G N _ TO P 控制条沿父窗口框架的顶部 /底部对齐并延伸。如果控制条是可停靠的, C B R S _ A L I G N _ B O T TO M 则允许它停靠于父窗口的顶部 /底部 C B R S _ A L I G N _ L E F T 同上,但在父窗口的左边/右边 C B R S _ A L I G N _ R I G H T C B R S _ O R I E N T _ H O R Z 使用停靠控制条并允许它们停靠于父窗口的顶部或者底部 C B R S _ O R I E N T _ V E RT 同上,但在父窗口的左边或者右边 C B R S _ A L I G N _ A N Y 仅用于停靠控制条的情况,允许该条停靠于父窗口框架的任何一边 C B R S _ O R I E N T _ A N Y C B R S _ TO O LT I P S 在用户将鼠标指针停留在工具栏按钮或者状态栏窗格之上时使工具提示出现 C B R S _ F LY B Y 在用户将鼠标指针停留在工具栏按钮或者状态栏窗格之上时使在线帮助出现 C B R S _ B O R D E R _ TO P M F C控制条类内部使用 C B R S _ B O R D E R _ L E F T C B R S _ B O R D E R _ R I G H T C B R S _ B O R D E R _ B O T TO M C B R S _ B O R D E R _ A N Y C B R S _ B O R D E R _ 3 D C B R S _ H I D E _ I N P L A C E C B R S _ N O A L I G N CBRS_FLOATING C C o n t r o l B a r类能够保证工具栏和状态栏免于互相覆盖并且不会遮盖窗口视。但是,它所 采用的方法是成套的,没有两个工具栏可以共享同一行或者列。这样,用户还是不能移动它 们或者将其浮动。为了得到这些附加的功能就需要使用另外两个 M F C类: C D o c k B a r和 C D o c k C o n t e x t。 2.4 停靠栏 为了让用户能够使用其鼠标移动工具栏, M F C应用程序沿其主窗口创建了 4种统一的控制 条,即停靠栏。用户可能没有注意过它们,但正是它们绘制了包围可移动工具栏的空间。更 常见的是,停靠栏往往沿着应用程序主窗口缩小到不可见,以等待以后用户在其所处位置停 靠工具栏。一旦工具栏被停靠,它们就展开并包围工具栏。 C F r a m e W n d类提供了一个叫E n a b l e D o c k i n g ( )的成员函数,它可以沿窗口框架外沿创建多 达4个停靠栏。C F r a m e W n d的E n a b l e D o c k i n g ( )函数使用C D o c k B a r类创建停靠栏。C D o c k B a r本 身派生于C C o n t r o l B a r类,就如同工具栏和状态栏一样可以使用其共享功能,包括 C C o n t r o l B a r 绘制控制条周边的功能。 请参见图2 - 4观察停靠栏。 注意 状态栏即使是从CControlBar派生而来并受其控制,也并不支持停靠功能。 第 2 章控控控制控条控控27下载图2-4 停靠栏 2.4.1 设置停靠功能 停靠功能并不是自动地被 M F C应用程序启用的,为了启用停靠功能必须在使用 A p p Wi z a r d创建工程时选择 Docking toolbars选项,或者由自己动手将同样的 3行代码增加到 C M a i n F r a m e类的O n C r e a t e ( )函数之中,如下所示: ■ CFrameWnd::EnableDocking() 该函数创建停靠栏。 ■ CControlBar::EnableDocking() 该函数向控制条中增加停靠和移动功能。 ■ C F r a m e W n d : : D o c k C o n t r o l B a r ( ) 该函数将控制条停靠到停靠栏。 下面就讨论这些函数所做的工作。 1. CFrameWnd::EnableDocking() 为了使主框架或者子框架窗口具备停靠功能,使用如下所示代码: EnableDocking( DWORD dwStyle // which side to create // a docking bar on: // BRS_ALIGN_TO P // for the top // CBRS_ALIGN_BOTTO M // for the bottom // CBRS_ALIGN_LEFT // for the left side // CBRS_ALIGN_RIGHT // for the right side // CBRS_ALIGN_ANY // for all four sides ) ; 可以将这些风格用 O R联合起来以提供附加效果。例如,使用 CBRS_ALIGN_LEFT | C B R S _ A L I G N _ TO P可以防止用户停靠在其框架窗口的底部或者右边,这是因为该位置没有停 靠栏可接受它们。 C F r a m e W n d : : E n a b l e D o c k i n g ( )函数可以同时创建4个停靠栏,每个停靠栏都采用 C D o c k B a r 类来创建。C D o c k B a r类与其他M F C控件类一样,为了创建该控件,必须首先创建一个该类的 实例然后再调用 C r e a t e ( )成员函数来创建其窗口。为了创建停靠窗口, C D o c k B a r使用了 A f x W n d C o n t r o l B a r窗口类。 C F r a m e W n d类在一个名为m _ l i s t C o n t r o l B a r s的指针链表里为它所创建的每个 C D o c k B a r实 例存储了一个指针。该链表是 C P t r L i s t类型的非文档公共成员变量。 如果要从C D o c k B a r派生出自己的停靠栏则必须重写 C F r a m e W n d : : E n a b l e D o c k i n g ( )函数, 因为很难在E n a b l e D o c k i n g ( )函数中编写代码C D o c k B a r,读者可以参考该主题下的“控制条趣 28第第第一部分第基第第础 下载 固定且不可移 动的工具条 停靠条已收缩 到主框架窗口 的一边 已停靠且可移 动的工具条 停靠条在此可见味编程”一节。 2. CControlBar::EnableDocking() 为了向控制条内增加必要的功能以允许其停靠,应该进行如下调用: m _ w n d To o l B a r.EnableDocking( dwStyle // which side can this bar // be docked to: // CBRS_ALIGN_LEFT // for left side only // CBRS_ALIGN_RIGHT // for right side only // CBRS_ALIGN_TO P // for top only // CBRS_ALIGN_BOTTO M // for bottom only // CBRS_ALIGN_ANY // for any side ) ; 这些风格可以再次使用 O R联合以获得预期的效果,例如 CBRS_ALIGN_LEFT |CBRS_ A L I G N _ TO P将告诉工具栏它只能停靠于主窗口的顶部或者左边,哪怕在窗口四周都具有停靠 栏。 E n a b l e D o c k i n g ( )函数增加到控制条内的停靠功能被包含在另一个 M F C类实例 C D o c k C o n t e x t中。只要用户双击或者拖动工具栏, C D o c k C o n t e x t内的函数就会起作用。当用 户释放一个被拖动的工具栏时, C D o c k C o n t e x t将确定哪一个停靠栏起停靠作用以及停靠于什 么位置。 3. CFrameWnd::DockControlBar() 为了确实将控制条停靠于停靠栏内,可使用如下代码调用: D o c k C o n t r o l B a r ( ( CWnd * )&m_wndTo o l B a r // a pointer to the CControlBar object LPRECT lpRect // location to dock the bar (optional) ) ; 其中C F r a m e W n d的D o c k C o n t r o l B a r ( )函数确定使用什么停靠栏并将控制条停靠在该停靠 栏。如何选取一个停靠栏取决于由 l p R e c t参数确定的最接近位置,同时还取决于如何启用控制 条。例如,如果在启用控制条的时候仅指定了 C B R S _ A L I G N _ L E F T风格,那么C F r a m e W n d的 DockControlBar ()函数将把控制条停靠在应用窗口的左边。没有指定位置的 C B R S _ A L I G N _ A N Y风格则使得C F r a m e W n d的D o c k C o n t r o l B a r ( )函数自动选择首先存在的停靠栏,一般是顶 部停靠栏。 C F r a m e W n d的D o c k C o n t r o l B a r ( )函数通过调用C D o c k B a r的D o c k C o n t r o l B a r ( )函数停靠控制 条。虽然它们名字一样,但是只有 C D o c k B a r的这一函数才接受控制条停靠。它将指向控制条 的一个指针存放在一个内部数组中。此外它还完成一些在以后章节将要讨论的工作。 1) CDockBar::DockControlBar() 对C D o c k B a r的D o c k C o n t r o l B a r ( )函数调用如下所示: DockControlBar(CWnd *pBar, LPRECT lpRect); 用户并不需要自己调用该函数, C F r a m e W n d的D o c k C o n t r o l B a r ( )函数便可完成这些工作。 调用C F r a m e W n d的D o c k C o n t r o l B a r ( )函数所需要的控制条指针和位置矩形被传送给该函数以 后,将被存储在C d o c k B a r中的一个叫做m _ a r r B a r s的成员数组中。只要控制条开始停靠于其停 靠栏上,该数组就起作用。该数组内的一个零指针告诉 C D o c k B a r开始一个新行。如果在调用 D o c k C o n t r o l B a r ( )函数的时候不精确指定位置,它只是将其添加到该数组末尾并创建一个新行 第 2 章控控控制控条控控29下载(在末尾添零)。如果指定了一个位置, D o c k C o n t r o l B a r ( )将识别是哪一行,然后保存它并将其 插入。 2) CDockContext::DockControlBar() C D o c k C o n t e x t类在需要停靠控制条的时候也调用该函数。它首先确定停靠到哪个停靠栏 (类似C F r a m e W n d的D o c k C o n t r o l B a r函数),然后调用该控制条的 C D o c k B a r : : D o c k c o n t r o l B a r ( ) 函数传递用户当前的鼠标位置。 2.4.2 自动改变大小和移动 用户不仅能在停靠栏内移动工具条,而且当用户重置应用程序大小的时候,如果一个或 多个控制条消失在窗口框架以外,停靠栏本身也能够自动地移动其控制条,而部分被遮盖的 控制条并不这样。但是,如果一个控制条完全不可见,它将会被移动到一个新行 (或新列— 如果该条是垂直对齐方式 )。可见,将控制条停靠到特定位置是很快捷的。停靠栏区域则可以 是固定的。 本主题的更多内容可参见2 . 7节“视和控制条如何共享客户区”。 请参见图2 - 5回顾停靠控制条的过程。 图2-5 停靠栏回顾 2.4.3 停靠栏小结 停靠控制条采用了两种M F C类以及4个函数: 1) CDockBar类沿应用程序的主窗口四周,创建至多 4个停靠栏以控制和包含其他控制条。 如果停靠栏没有包含其他控制条,它就将自身缩小到看起来仿佛已经从应用程序中消失,然 后等待新的控制条放置在自己上面。 2) CDockContext类处理用户鼠标单击并拖动控制条于停靠栏上这一事件,该类通过 C C o n t r o l B a r : : E n a b l e D o c k i n g ( )函数为控制条创建。 3) CFrameWnd的E n a b l e D o c k i n g ( )函数为窗口的每一边创建C D o c k B a r的实例。 4) CControlBar的E n a b l e D o c k i n g ( )函数创建一个C D o c k C o n t e x t实例以允许控制条处理用户 鼠标输入移动或者停靠其控制条等事件。 5) CFrameWnd的D o c k C o n t r o l B a r ( )函数确定将控制条停靠到哪个停靠栏。 6) CDockBar的D o c k C o n t r o l B a r ( )函数用于停靠栏控制其上的控制条。 30第第第一部分第基第第础 下载 使用C F r a m e Wr d : : E n a b l e D o c k i n g ( ) 函数将停靠条添加 到主窗口的各边 使用C C o n t r o l B a r : : E n a b l e D o c k i n g ( )函 数为每一个工具条 加入可移动的功能 使用C F r a n e W n d : : D o c k C o n t r o l B a r ( ) 函数对工具条进行 初始停靠 C F r a n e W n d : : D o c k C o n t r o l B a r ( )函数根据邻近程度选 择停靠条并调用它的 C C o n t r o l B a r : : D o c k C o n t r o l B a r ( )函数以上便是用户移动并停靠其工具栏的过程。 如果用户熟悉M F C工具栏,可能会注意到在拖动工具栏离开主窗口后,它会突然被自己 的 框 架 包 围 , 换 句 话 说 就 是 浮 动 。 对 这 个 功 能 , M F C 提 供 了 一 个 最 新 的 叫 做 C D o c k M i n i F r a m e W n d的类。 2.5 浮动条 C D o c k M i n i F r a m e W n d类本身是从C F r a m e W n d派生的(请看注解)。基本上,它是一种简单 的框架窗口,该窗口仅包含了一个停靠栏。因为该框架窗口没有视,所以这种情况下不需要 4 个停靠栏。当C D o c k C o n t e x t类实现了用户拖放其工具栏到空白区域的任务时,它将相应创建 一个C D o c k M i n i F r a m e W n d实例并将工具栏停靠于该框架内部自动创建的停靠栏上。请参见图 2 - 6的浮动条实例。 图2-6 浮动工具栏 注解 C D o c k M i n i F r a m e W n d实际上派生于C M i n i F r a m e W n d,而C M i n i F r a m e W n d又派生 于C F r a m e W n d。C M i n i F r a m e W n d因而具备C F r a m e W n d的全部功能但比起主框架或者子 框架来又更容易适应定制的应用程序。它可以有自己的菜单和工具栏,在效果上类似 于一个正好处于当前应用程序中间的微型S D I应用程序。然而,不能使用A p p Wi z a r d来 创建该微型 S D I应用程序,而且在同一工程内混合视类和文档类可能引起麻烦。因此, 最好创建一个单独的S D I应用程序而不是在自己的应用程序中创建。C M i n i F r a m e W n d所 做的最好工作可能就仅仅是支持CDockMiniFrameWnd以及浮动工具栏。 浮动工具栏具有两个停靠工具栏所不具备的特点:第一,用户仅需要拖动微型框架窗口 一角就能够改变工具栏的大小;第二,用户还可以在同一个微型框架窗口内停靠一个或者多 个附加工具栏。读者请参见图 2 - 7以了解这些特点。 图2-7 改变大小和微型浮动工具栏 第 2 章控控控制控条控控31下载 一个由 C D o c k M i n i F r a m e W n d创建的停靠 微型框架窗口 其中一个已停靠 的工具条 该停靠条完全被 工具条所遮盖 一个已调整大小的工具条 注意:一个可调整大小的工具条不能和另一个位于微型框架窗口的工具条一起停靠 两个在同一微型框架 窗口已停靠的工具条 这些线条由 C C o n t r o l B a r 绘制 停靠条在此 将可见启用或者关闭浮动条功能需要两种额外的控制条风格,如表 2 - 5所示。 表2-5 浮动控制条的M F C风格 风 格 使 用 说 明 C B R S _ S I Z E _ F I X E D 防止用户或者系统重置控制条的大小 C B R S _ S I Z E _ D Y N A M I C 允许用户或者系统重置控制条大小,虽然用户可能只是改变浮动地工具栏大小 CBRS_FLOAT_MULTI 允许多个控制条被停靠于同一微型框架窗口内 可以通过调用C F r a m e W n d的F l o a t C o n t r o l B a r ( )函数将工具栏浮动。 2.6 MFC的高级控制条类小结 现在我们已经了解, C To o l B a r、C S t a t u s B a r类允许控制条和视类共享客户区以及许多其他 功能。C D o c k B a r和C D o c k C o n t e x t类允许用户移动其工具栏。而 C D o c k M i n i F r a m e W n d类则可 以使用户浮动它们的工具栏。 接下来讨论这些类型的控制条如何与主窗口共享同一客户区。 2.7 视和控制条如何共享客户区 应用程序的客户区允许两种类型的子窗口争夺空间:视窗口和控制条窗口。在 M D I应用 程序的情况下,视窗口就是 M D I C l i e n t区域。在S D I应用程序的情况下,视窗口则由 C Vi e w类 创建。通过一个循环过程可以确定每个窗口占据了多少屏幕实际空间。首先调用主窗口的 G e t C l i e n t R e c t ( )函数得到整个可用空间的大小。然后这个空间大小作为一个矩形参数被传送给 主窗口的每个子窗口。每个窗口则为自己划分出一块空间并在该处定位,然后向下一个窗口 传送一个减小的矩形空间,这样直到所有的窗口都找到空间。剩下的空间则留给视窗口。 并不是所有的子窗口都参与了这一过程,只有那些由 C C o n t r o l B a r的派生类所创建的窗口 才这样做。这就意味着像 C To o l B a r C t r l这样的类或者直接通过 A P I调用而创建的窗口不会得到 一块空间。由于视窗口并不是由 C C o n t r o l B a r类创建的,它也不能挖出一块场地来,但它确实 得到了所有剩余的空间。由于有的时候没有什么空间留下,于是视就只好消失了。 这整个过程开始于应用程序的主窗口发生变化的时候— 要么是添加或者删除了工具栏, 要么就是移动或者改变大小。对浮动的工具栏,用户可以改变它的形状或者增加一个新的控 制条。这些功能中的任何一个通常都会导致调用 C F r a m e W n d的R e c a l c L a y o u t ( )。举个例子, C F r a m e W n d中的O n S i z e ( )消息处理器将直接调用该函数。可以按以下方式编程调用该函数。 2.7.1 CFrameWnd::RecalcLayout() 该函数的语法表达是: CFrameWnd::RecalcLayout(BOOL bNotify); 如果框架包括O L E框架并且要让框架重新计算其布局,那么这里的 b N o t i f y参数就应该设 为T R U E。 该函数除了调用基类函数 C W n d : : R e p o s i t i o n B a r s ( )以外几乎什么也没有做,这事实上就是 循环开始。R e c a l c L a y o u t ( )实际完成的一件工作是为 R e p o s i t i o n B a r s ( )识别视窗口。如果没有视 窗口,或者没有浮动的工具栏,那么 R e c a l c L a y o u t ( )就使用R e p o s i t i o n B a r s ( )函数来关闭包围其 32第第第一部分第基第第础 下载内部任何控制条的框架。 2.7.2 CWnd::RepositionBars() 该函数的语法表达如下: C W n d : : R e p o s i t i o n B a r s ( UINT nIDFirst, UINT nIDLast, // child windows to poll UINT nIDLeftOver, // id of leftover window UINT nFlags, // can be set to simply inquire // how much space the // control bars need LPRECT lpRectParam, // can return inquired size LPCRECT lpRectClient, // substitute client area BOOL bStretch // cause bars to stretch to fit // client area ) ; R e p o s i t i o n B a r s ( )首先确定其窗口的客户区,换句话说,它必须计算出有多少空间来放置 视和控制条。为了得到这个尺寸,它只调用 C W n d : : G e t C l i e n t R e c t ( )函数。然后R e p o s i t i o n B a r s () 向它的任何子窗口发送一个 W M _ S I Z E PA R E N T窗口消息(特别为M F C创建的),而这些子窗 口则落在由n I D F i r s t和n I D L a s t参数限定的范围之内 (是nIDLast 而不是n I D L e f t O v e r结尾:并不 一定仅仅是剩余的窗口 )。传递给这些窗口的参数只有两个:客户区矩形以及每个控制条是否 应当延伸以填充客户区。每一个窗口的 O n S i z e P a r e n t ( )消息处理器负责确定它需要多少客户区, 并将该区域从客户区矩形中减去,使减去的空间对下一个窗口不可用。 当全部子窗口(包括控制条)都划分出自己的区域以后, R e p o s i t i o n B a r s ( )使用剩余的空间设 置剩余窗口的大小和位置。在这种情况下即为视窗口。 虽然有好几类控制条,但 W M _ S I Z E PA R E N T窗口消息则只由C C o n t r o l B a r中的一个消息处 理器处理。这就是为什么只有用 C C o n t r o l B a r创建的控制条才可以划出区域的原因。 接下来讨 论C C o n t r o l B a r的O n S i z e P a r e n t ( )消息处理器如何确定划分出多少空间。 2.7.3 CControlBar::OnSizeParent() 该消息处理器的语法表达如下: L R E S U LT CControlBar::OnSizeParent(WPARAM, LPARAM lParam); 其中l P a r a m包含了客户区矩形。 O n S i z e P a r e n t ( )消息处理器接收传递给每个窗口的矩形参数,确定它自己的控制条需要多 少空间,从矩形中减去这一片空间然后将该参数传递给下一个窗口。为确定自己的控制条需 要多少空间,O n S i z e P a r e n t ( )函数调用了C a l c D y n a m i c L a y o u t ( )函数。 C a l c D y n a m i c L a y o u t ( )函数本身被每个由 C C o n t r o l B a r派生的控制条类 (如C To o l B a r、 C S t a t u s B a r等)重载以确定相应类型控制条所需要的空间尺寸大小。对于可停靠工具栏, C a l c D y n a m i c L a y o u t ( )在计算出其大小之前可能改变工具栏的形状。 C a l c D y n a m i c L a y o u t ( )返回给O n S i z e P a r e n t ( )函数的尺寸与其说是需求不如说是请求。例如, 延伸的水平工具栏将请求一个 32 767像素的宽度。但显然没有多少监视器处理这样的分辨率, 实际上可以说没有。但C a l c D y n a m i c L a y o u t ( )真正要O n S i z e P a r e n t ( )去做的是使该宽度达到当前 第 2 章控控控制控条控控33下载可用空间的最大尺寸。或者说,使控制条延伸跨越整个主窗口的客户区。 因此,O n S i z e P a r e n t ( )最后判断它的控制条将拥有多少客户区,它总是尽力调整其控制条, 使它们沿窗口框架四周占据最小的区域。一旦该函数做出了决定,控制条就移动到指定位置, 其占用的大小也如上所述从客户区中扣除。 下面讨论C a l c D y n a m i c L a y o u t ( )如何计算出控制条大小和改变其形状。 2.7.4 CalcDynamicLayout()和CalcFixedLayout() 实际上有两个函数可用于帮助 O n S i z e P a r e n t ( )计算控制条的大小: C a l c D y n a m i c L a y o u t ( )— 用于对具有C B R S _ S I Z E _ D Y N A M I C风格的工具栏布局并计算 其大小。 C a l c F i x e d L a y o u t ( )— 计算其余控制条的大小。 O n S i z e P a r e n t ( )函数并不直接调用 C a l c F i x e d L a y o u t ( ),它调用的是C a l c D y n a m i c L a y o u t ( )。 该函数确定是否是动态工具栏,如果不是, C a l c D y n a m i c L a y o u t ( )则调用C a l c F i x e d L a y o u t ( )函 数。 C a l c D y n a m i c L a y o u t ( )的语法表达如下: virtual CSize CCalDynamicLayout( int nLength, DWORD dwMode); 这里的n L e n g t h参数指定控制条请求所需的长度,而 d w M o d e则是表2 - 6所示的布局模式的 联合。这两种参数都仅能用于具有 C B R S _ D Y N A M I C _ S I Z E风格的工具栏。 注意 如果调用CalcDynamicLayout()时具有有效的长度参数,控制条必须被停靠并且不 能使用LM_MRUWIDTH或者LM_COMMIT布局模式。 表2-6 CalcDynamicLayout()布局模式 模 式 使 用 说 明 L M _ L E N G T H Y 如果不是宽度而是一个指示长度的长度参数则需要设置该模式 L M _ C O M M I T 告诉函数将其计算的尺寸存储于 Most Recently Used变量里,该变量可以用下一个 模式得到。它还锁定工具栏的方向和形状,请参考以下的讨论 L M _ M R U W I D T H 当L M _ C O M M I T模式被使用时,告诉函数返回其计算的最后尺寸 L M _ S T R E T C H 告诉函数返回被延伸控制条的大小。如果使用了 L M _ H O R Z,控制条将水平延伸, 否则将垂直延伸。控制条被延伸到填充控制条和主窗口之间的空间。由于停靠栏填充 任何空白的区域,因此停靠和浮动条不能被延伸 L M _ H O R Z 指示控制条水平或者垂直布局 L M _ H O R Z D O C K 告诉函数为已经停靠的工具栏返回一个尺寸 LM_VERTDOCK 停靠工具栏只有一行或者一列按钮 C a l c F i x e d L a y o u t ( )的语法表达如下: CSize CalcFixedLayout(BOOL bStretch, BOOL bHorz ); 如果需要一个延伸的尺寸而不是控制条真正的尺寸,便需要在此设置 b S t r e t c h参数。而 b H o r z参数则仅适用于垂直延伸的工具栏。 C C o n t r o l B a r中C a l c D y n a m i c L a y o u t ( )函数的缺省行为只是调用 C a l c F i x e d L a y o u t ( ),而 C C o n t r o l B a r中C a l c F i x e d L a y o u t ( )的缺省行为只是为水平延伸控制条返回尺寸数 3 2 7 6 7,0,为垂 直延伸控制条返回 0,3 2 7 6 7或者为不延伸的控制条返回 0,0。显而易见,不从 C C o n t r o l B a r派生 34第第第一部分第基第第础 下载的类总是不可见的。 对每种控制条(工具栏、停靠栏等),C a l c F i x e d L a y o u t ( )和C a l c D y n a m i c L a y o u t ( )的重载版本 将在下面更详细讨论。 2.7.5 CToolBar::CalcFixedLayout()和CToolBar::CalcDynamicLayout() C To o l B a r的C a l c F i x e d L a y o u t ( )和C a l c D y n a m i c L a y o u t ( )都调用同样的辅助函数 C a l c L a y o u t ( ) 函数来确定工具栏大小。 C a l c L a y o u t ( )函数询问工具栏有多少按钮以及配置如何。如果工具栏 具有C B R S _ S I Z E _ F I X E D风格,C a l c L a y o u t ( )将只返回基于按钮风格的工具栏的当前大小,以 及这个控制条是一行还是多行。 但是,如果工具栏具有 C B R S _ S I Z E _ D Y N A M I C风格,C a l c L a y o u t ( )就可能在计算其大小 之前改变工具栏的形状。它放置工具栏的方法取决于下列顺序中的布局模式: ■ 如果工具栏处于浮动状态或者使用了 L M _ M R U W I D T H模式,工具栏将返回到最近使 用的大小并返回该尺寸。一般这种模式常用于恢复浮动工具栏的大小 (在其停靠的情况下)。 ■ 如果工具栏垂直或者水平停靠, C a l c D y n a m i c L a y o u t ( )将改变工具栏形状为单行或者单 列按钮并请求客户区内可用的最大宽度或高度。 ■ 如果长度不是- 1,那么,工具栏在停靠并且没有采用 L M _ C O M M I T模式的情况下就置 为该长度。如果设置了L M _ L E N G T H Y布局模式,那么L e n g t h指的就是宽度。 为了确认布局,必须使用 L M _ C O M M I T布局模式。否则,由 C a l c D y n a m i c L a y o u t ( )返回的 大小会对被请求的布局有效,但是在操作它之前工具栏本身又要保持其形状。如果使用了 L M _ M R U W I D T H模式,L M _ C O M M I T还将导致C a l c D y n a m i c L a y o u t ( )存储它所计算的尺寸以 供今后使用。 2.7.6 工具栏布局 为了改变工具栏的形状 (使它垂直或者行数加倍以创建特定大小 ),C a l c D y n a m i c L a y o u t ( ) 在开始新的一行之前对工具栏按钮应用 T B S TAT E _ W R A P按钮状态。正如所看到的, 这是一种按钮状态而不是工具栏状态。没 有可靠的方式来告诉工具栏按照特定按钮 封装。可以将工具栏设置为 T B S T Y L E _ W R A PA B L E风格以便它在宽度太小的情 况下自动地封装成多行。但是,这不可能 获得直接使用T B S TAT E _ W R A P所能达到的 控制程度。事实是,为了使工具栏垂直仅 需 要 为 工 具 栏 上 的 每 个 按 钮 应 用 T B S TAT E _ W R A P状态即可。请参见图2 - 8了解 T B S TAT E _ W R A P状态是如何用于创建多行的。 注意 为什么C a l c D y n a m i c L a y o u t ( )函数会愚蠢到为了确定其工具栏形状而在其他操作 正在进行的过程中来获得其大小呢?为什么工具栏不事先建立自己的外形呢?这是因 为,在工具栏的邻居们都在同时执行操作的这一过程中,根本没有更合适的时间来重 新塑造工具栏的外观。仅仅改变一个控制条的外观将会导致在其原位置出现一个漏洞, 第 2 章控控控制控条控控35下载 图2-8 封装工具栏各行 T B S TAT E _ W R A P工具条 状态应用于该 单个工具条控 件窗口中的两 个按钮在其新位置上却会引起覆盖操作。因而,只要工具栏需要被重新塑造外观,系统只改 变其风格然后调用RecalcLayout()函数来执行实际改变外观的工作。 2.7.7 CStatusBar::CalcFixedLayout() 由于状态栏不能重置大小或者改变其对齐方向,所以其 C a l c D y n a m i c L a y o u t ( )函数仅仅调 用C a l c F i x e d L a y o u t ( )函数即可。C a l c F i x e d L a y o u t ( )函数的状态栏版本随后只是返回该状态栏在 其边界大小和在所用字体尺寸的基础上所需的高度。该宽度总是返回 32 767像素值,以使状 态栏延伸到窗口框架的两边。 2.7.8 CDockBar::CalcFixedLayout() 由于停靠栏也从不改变大小或者对齐方向,其 C a l c F i x e d L a y o u t ( )函数也仅仅调用 C a l c F i x e d L a y o u t ( )函数。C a l c F i x e d L a y o u t ( )函数的停靠栏版本确定它需要多大空间来包围所有 的被停靠控制条。但在进行这个计算之前, C a l c F i x e d L a y o u t ( )函数必须为自己进行一些布局 方面的工作。 C a l c F i x e d L a y o u t ( )所做的工作就是遍历所有被停靠于其上的控制条 (记住m _ a r r B a r s包含了 该列表),并调用它们每一个的C a l c D y n a m i c L a y o u t ( )函数。这与先前学习的C To o l B a r的布局函 数是完全一样的。其返回的请求尺寸随后用于计算添加控制条的停靠栏所需的空间。如果控 制条离开了父窗口的左边或者底部, C a l c F i x e d L a y o u t ( )则将使其移回原处。如果控制条离开 父框架很远则不可见, C a l c F i x e d L a y o u t ( )就会创建新行(如果是垂直布局则创建新列 )并把控制 条移动到新行去。一旦 C a l c F i x e d L a y o u t ( )函数为停靠栏中的每一个控制条完成了这项工作, 它就会计算自己在父框架内需要多大空间并将这个数值返回给 O n S i z e P a r e n t ( ),该函数则按规 矩从可用空间中扣除掉这一块。 注意 位于停靠栏上的控制条从不接收W M _ S I Z E PA R E N T消息。因为它们是停靠栏的 子窗口,以上讨论的R e p o s i t i o n B a r ( )函数“看不见”它们,只有它们的父窗口,也就是 停靠栏才能看见。停靠栏的C a l c F i x e d L a y o u t ( )函数完成一些“冒充”的工作,为这些控 制条代理调用OnSizeParent()函数以保证它们之间不相互覆盖。 2.7.9 共享客户区小结 下面介绍应用程序主窗口中的控制条如何互相共享客户区: 1) 当窗口上的内容改变时,C F r a m e W n d的R e c a l c L a y o u t ( )函数即被调用。 2) RecalcLayout()随后调用C W n d的R e p o s i t i o n B a r s ( )函数并告诉它允许窗口中所有的控制 条占用其所需空间,然后将剩余空间留给客户区。 3) RepositionBars()遍历窗口中的所有子窗口并向它们发送 W M _ S I Z E PA R E N T消息来完成 这项工作。它给框架中每个还可见的控制条传递给一个逐渐缩小的矩形区域。 4) CControlBar的O n S i z e P a r e n t ( )消息处理器为所有的控制条类型处理 W M _ S I Z E PA R E N T 消息,它采取的方式是询问控制条它们需要多大的空间并随后从可用空间中将它们所需的空 间减去。 5) OnSizeParent()通过调用C a l c D y n a m i c L a y o u t ( )函数计算出其控制条所需的空间,这个函 36第第第一部分第基第第础 下载数被每个基于不同类型的控制条重载以确定这个所需的尺寸。 6) 停靠栏在返回请求尺寸之前为自己完成一点布局工作。 以上所述就是本节主题的全部过程,请参见图 2 - 9以深入学习该过程。 图2-9 共享客户区小结 2.8 对话条 对话条是工具栏和无模式对话框相结合的产物。如同 C To o l B a r类创建了工具栏并控制其 子窗口一样,C D i a l o g B a r类则创建对话条并控制其无模式的子对话框。 C D i a l o g B a r类本身派 生于C C o n t r o l B a r类并再次使用了其绘制边界、停靠和浮动等通用的功能。 对话条由一个具有子风格、无边界的对话框模板产生。出于风格统一的考虑,它应该具 有工具栏或者状态栏的一般尺度。例如,它应该长而窄或者高而苗条等等。外观较粗大的对 话条可能使应用程序的外观显得不清晰明快。 虽然对话条可以浮动或者停靠,但它们并不能由用户自动改变其大小。但是程序员可以 重载其C a l c F i x e d L a y o u t ( )函数并控制其大小。 C D i a l o g B a r : : C a l c F i x e d L a y o u t ( ) 如同C To o l B a r和C D o c k B a r,C D i a l o g B a r也有自己的C a l c F i x e d L a y o u t ( )派生函数以占用一 块主窗口区域。在这种情况下,该函数仅仅返回一些修改的 m _ s i z e D e f a u l t数值,该值是创建 对话条的对话框模板所定义的原始尺寸。例如,假设对话条已停靠并延伸到填充应用程序窗 口的顶部,那么C a l c F i x e d L a y o u t ( )将返回3 2 7 6 7,也就是m _ s i z e D e a f a u l t . c y值。 如果打算控制该函数,仅需重载 C D i a l o g B a r派生类的C a l c F i x e d L a y o u t ( )函数并提供自己的 定义值即可。 第 2 章控控控制控条控控37下载 主框架窗口中发生一些事 件,C F r a m e W n d : : R e c a l c - L a y o u t ( )函数被调用 每一个 C C o n t r o l B a r类通过使 用C a l c D y n a m i c L a y o u t ( )来获 取各自工具条的尺寸,然后在 框架中寻找一个区域 可调整大小的工具条中的 C a l c D y n a m i c L a y o u t ( )甚至可 能首先改变工具条的布局 R e p o s i t i o n B a r s ( )将剩 余的空间留给视窗口 R e c a l c L a y o u t ( )获得视察窗口的 I D号并调用W n d : : R e p o s i t i o n - Bars () ,在这种情况下,视是一 个M D I C l i e n t窗口 R e p o s i t i o n B a r s ( )以原始状态启动 并发送给该框架窗口中的每一个 子窗口W M _ S I Z E PA R E N T消息2.9 伸缩条 应用程序最后一种可用的控制条就是伸缩条 ( R e b a r ),虽然它也被称为 Cool Bar,术语 r e b a r可能是Resize Bar的缩写。 伸缩条具有它自己的 Windows API对等函数,可以以如下方式创建,虽然它仅仅在 Windows 98版本或者安装了Internet Explorer 4.01及其以上版本以后才可用,但在介绍其内容 时不必拘泥于这些细节: HWND hwnd=::CreateWindowEx( ..., "ReBarWi n d o w 3 2 " , . . ) ; 实际上伸缩条看起来像是一种 windows API,这种A P I试图提供一些以前仅在 M F C停靠栏 中才可用的功能,以便使非 M F C应用程序也可以移动它们的工具栏。 但是,当它终于能够移动工具栏的时候,伸缩条的功能已经远远超过了简单的停靠栏。 它不仅可以移动工具栏而且还可以改变 其大小。不只工具栏可以得到这样的支 持,任何控件窗口都可以得到其支持, 例如编辑框、按钮、组合框、列表框和 动画控件等等。 每种控件窗口包含在伸缩条的段 ( b a n d )中,每段一个控件,但是每个伸 缩条可能有无限的段。一个段增加一个 提手条(grriper bar),程序员自己可以拥 有其标题并用自己的位图绘制其背景。 请参见图2 - 1 0了解伸缩条的布局。 正如工具栏和状态栏一样,可以使用 3种风格来影响伸缩条的外观和行为:标准窗口风格 ( W S _ V I S I B L E , . . . . )、通用控件风格( C C S _ V E RT, . . . )以及一种针对伸缩条的特有风格等等,该特 定风格具有R B S _前缀。伸缩条风格如表2 - 7所示。 表2-7 伸缩控制条风格 风 格 使 用 说 明 R B S _ B A N D B O R D E R S 伸缩条控件绘制相邻段之间的线条 R B S _ V E RT I C A L G R I P P E R 提手条在垂直伸缩条中垂直显示 ( Wi n 9 8和I E 4 . 0 1中才有) RBS_AUTOSIZE 当控制条的大小改变时,伸缩条改变段的布局(Win98和IE4.01才有) 伸缩条与其停靠栏不能一起使用,虽然可以在应用程序中创建许多个伸缩条而且可以将 它们垂直竖起来,但就是不能使它们停靠。所以还得创建停靠栏以停靠应用中的其他工具栏。 为了给伸缩条增加新的控制窗口,可使用如下代码: ::SendMessage(hWnd, RB_INSERTBAND, uIndex, (LPA R A M ) p r b b i ) ; 这里u I n d e x是插入段的位置,而p r b b i则是指R E B A R B A N D I N F O结构,该结构定义了放进 伸缩条段中的子窗口。 M F C近来只在其6 . 0版本中才提供了对伸缩条的支持,但这项支持与对工具栏和状态栏的 支持是一样的:使用一种类简单地封装 A P I,一种类则提供更强大的功能。 38第第第一部分第基第第础 下载 图2-10 伸缩条控件窗口 一个跨越主框架窗口顶 部的伸缩条控件窗口 一个在其 自己伸缩 条区域的 工具条 一个在其 自己伸缩 条区域的 对话条2.9.1 CReBar和CReBarCtrl C R e B a r C t r l类封装用于创建伸缩条的 Windows API的方式与C To o l B a r C t r l封装工具栏的创 建类似 。为了创建伸缩条并使用C R e B a r C t r l类增加一个新的段,可以使用如下代码: CReBarCtrl rb; rb.Create( DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID ); rb.InsertBand( UINT uIndex, REBARBANDINFO* prbbi ); 这里u I n d e x和p r b b i参数的含义与使用API 插入一个段时的含义是一样的。 当然,不仅仅只是封装 API ,C R e B a r类还具有一些更先进的特点。例如,由于 C R e B a r加 入了在其他的M F C类中(如C To o l B a r、C S t a t u s B a r和C D i a l o g B a r )才能发现的功能。程序员可以 向由C R e B a r创建的伸缩条中直接增加由这些类所创建的控制条,如下所示: CReBar rb; rb.Create( CWnd* pParentWnd, DWORD dwCtrlStyle, DWORD dwStyle, UINT nID ); C ToolBar tb; tb.Create( CWnd *pParentWnd ); t b . L o a d ToolBar( UINT nID ); rb.AddBar( &tb, LPCTSTR lpszText, CBitmap* pbmp, DWORD dwStyle ); A d d B a r ( )的l p s z Te x t和p b m参数是可选段的文本和位图。 如果打算在段中包括其他的窗口类型,例如动画控件,仅需将该控件放进对话框模板中 并创建对话条,然后利用C R e B a r : : A d d B a r ( )将其加入该条中。 这种直接装载其他C C o n t r o l B a r的功能使得C R e B a r类特别有用,但是该类没有 C R e B a r C t r l 类的附加功能。 2.9.2 CReBar::CalcFixedLayout() 如同C To o l B a r和C d i a l o g B a r一样,C r e B a r具有自己的C a l c F i x e d L a y o u t ( )派生函数,该函数 返回一个请求大小值给 O n S i z e P a r e n t ( )。然而,在这种情况下, C R e B a r的C a l c F i x e d L a y o u t ( )函 数仅仅将其全部段的大小加在一起组成一个整体传递。因为伸缩条不能被停靠或者浮动,用 户当然就不能改变其大小,所以 C R e B a r并没有C a l c D y n a m i c L a y o u t ( )函数。 2.10 命令条 命令条(Command Bar)是本章最后要进行深入讨论的控制条,这是因为命令条在好几种微 软应用程序中都得到了使用,其中包括 Developer Studio。命令条实际上不过是一种具有扁平 外观按钮的工具栏,它可以使用 T B S T Y L E _ F L AT工具栏风格来创建。命令条的前面还有一个 提手条,以帮助用户抓住该条。否则从扁平的按钮周围找到工具栏的边界再拖动将比较困难。 请参考实例8以进一步了解对命令条外观进行初始化的方法。 Developer Studio内的菜单也是命令条。换句话说, Developer Studio内的菜单实际上是一 个带有按钮文本的扁平工具栏,而不是按钮图标。当这些按钮之一被按下时,在该按钮之下 就会直接出现一个弹出菜单,该类按钮则表现出标准下拉菜单的外观。 以这种方式使用工具栏虽然并不会为应用程序增加任何附加功能,而且实际上需要花费 第 2 章控控控制控条控控39下载额外的工作来实现这种方式,但这样将使应用程序看起来与众不同。 2.11 控制条窗口小部件风格 每种类型的控制条都具有各自的风格,这些风格使得控件具有不同的外观。 2.11.1 工具栏按钮风格 一些更有趣的工具栏按钮风格包括: ■ T B S T Y L E _ S E P,一般用于创建工具栏按钮之间的间隙 ( g a p ),以便建立按钮分组。指 向按钮位图表面的索引成为以像素计算的间隙大小。 ■ T B S T Y L E _ A U TO S I Z E,仅用于Windows 98或者安装了IE 4.01以上版本的应用程序。 该风格中每个按钮的大小基于其中的文本计算,它经常用于模仿菜单条的工具栏。 ■ T B S T Y L E _ D R O P D O W N,使工具栏产生T B N _ D R O P D O W N控件通知消息而不是产生 来自按钮的命令消息。它具有 T B S T Y L E _ A U TO S I Z E类似的使用局限性,也用于实现菜单工 具栏。 ■ T B S T Y L E _ E X _ D R AW D D A R R O W S,与T B S T Y L E _ D R O P D O W N联合用于创建一种按 钮,这种按钮中嵌入了一个下拉箭头。该风格应用时必须使用 TB_ SETEXTENDEDSTYLE窗 口消息。 用户可以为自己的工具栏按钮应用好几种风格以改变它们的外观。但是除了上面讨论的 T B S TAT E _ W R A P状态之外,其他的多数状态已经通过 O n C m d U I消息处理器由M F C处理,其 中包括禁用按钮或者为按钮设置复选标记。 2.11.2 状态栏窗格风格 状态栏窗格的一些更有趣的风格如下所示: ■ S B P S _ N O B O R D E R S,导致窗格凹进控制条而不出现。该风格自动应用于 M F C出现帮 助消息的应用程序状态栏的第一个窗格。如果要让该窗格也凹进去,那就需要为窗格 0使用接 下来的两种风格。 ■ S B P S _ N O R M A L,导致窗格出现下凹并且不延伸。 ■ S B P S _ S T R E T C H,导致窗格延伸并填充延伸状态栏的空白区域。该风格只能使用于一 个窗格,通常是第一个,用于提供在那里出现的帮助信息。 ■ S B P S _ P O P O U T,导致窗格呈现突起。使用用于更新用户界面的 C C m d U I的成员函数 S e t C h e c k ( T R U E )可以得到同样的效果。 ■ S B P S _ O W N E R D R AW,允许程序员绘制单独的窗格,为了做到这一点,必须从 C S t a t u s B a r派生自己的类并重载D r a w I t e m ( )成员函数。可以使用该风格在状态窗格中显示图标 或者使用颜色。 2.11.3 伸缩条段风格 一些更有趣的伸缩条段的风格如下: ■ R B B S _ B R E A K,导致控制条开始新行/列。 ■ R B B S _ C H I L D E D G E,导致段在其子窗口的顶部和底部绘制边界。 40第第第一部分第基第第础 下载■ R B B S _ F I X E D S I Z E,防止段被改变大小,因此不显示提手条。 ■ R B B S _ N O V E RT,在伸缩条垂直对齐的情况下隐藏该段。 ■ R B B S _ G R I P P E R A LWAY S和R B B S _ N O G R I P P E R,导致提手条总是 (或是从不)显示。 仅在Windows 98和IE 4.01及其以上系统中才可用。 2.12 设计自己的控制条 虽然M F C控制条类已经减轻了程序员使用控制条的许多麻烦和头痛,但是它们是已经实 现了的,这就使得有时,如果不依赖一些其他未列出的特点,是不可能修改它们的一些缺省 行为表现的。所以在某些情况下甚至需要重写 M F C功能,虽然这种情况不多见。 以下是一些修改和增强控制条功能的方法。 2.12.1 重载CControlBar::CalcDynamicLayout() 这也许是增强控制条功能最普通的方法。重载 C a l c D y n a m i c L a y o u t ( )函数可以确定控制条 的大小。因为当前 M F C并不支持重置对话条大小,所以首先可以自己增加该功能,以向用户 提供一种请求新尺寸的机制。然后将新的尺寸参数返回给 C a l c D y n a m i c L a y o u t ( )函数。无法自 动定义像Dialog Editor那样具有多个列的工具栏,替代的办法就是在自己的工具栏派生类中重 载C a l c D y n a m i c L a y o u t ( )函数。在这个类中必须对适当的按钮使用 T B S TAT E _ W R A P风格然后 返回合适的尺寸。你甚至可以在基于工具栏所停靠的停靠栏的基础之上重新对齐工具栏。 2.12.2 增加WM_SIZEPARENT消息处理器 正如前面所提到的,应用程序中的主窗口的所有子窗口都接收 W M _ S I Z E PA R E N T窗口消 息,但目前只有C C o n t r o l B a r类具有针对该消息的处理器函数,它允许 C C o n t r o l B a r窗口在客户 区划分出一块小区域。因此,对这个过程增加一个非控制条窗口仅仅意味着为自己的窗口类 增加了一个W M _ S I Z E PA R E N T消息处理器。然而,既然添加到这一处理过程的其他类型的窗 口可能位于对话条内,而对话条可以自然地获得 W M _ S I Z E PA R E N T消息处理器,所以程序员 也许从来不会需要自己来创建该处理器。 2.12.3 重载CMainFrame::RecalcLayout() 控 制 条 在 应 用 程 序 主 窗 口 中 的 布 局 如 何 得 到 控 制 呢 ? 这 可 以 通 过 重 载 C M a i n F r a m e : : R e c a l c L a y o u t ( )函数或者C C h i l d F r a m e : : R e c a l c L a y o u t ( )函数来完成。通过手工进 行布局,可以: ■ 确定哪个条首先得到分配区域。 ■ 总是在特定的位置放置特定的控制条。 然而 M F C已经使这种过程变得不可能,除非重写函数 C F r a m e W n d : : R e c a l c L a y o u t ( )和 C W n d : : R e p o s i t i o n B a r s ( )。因为这些函数的功能将来会由微软增强,而这些函数现在又不得不 由程序员自己来实现,所以,如果对重写 M F C函数感到棘手,那么大可放心:这都是些内容 不大可能改变的小函数。 如果修改这些函数可以带来很大的好处,那么程序员完全可以通过从 M F C的\ s r c目录下拷 贝R e c a l c L a y o u t ( )和C W n d : : R e p o s i t i o n B a r s ( )函数版本来开始这项工作。这些函数包含在 第 2 章控控控制控条控控41下载w i n c o r e . c p p和w i n f r m . c p p中,而且它们的内容通俗易懂,这些函数如何修改就留给读者自己 领会了。 2.12.4 从CDockBar派生 为了修改并增强自己的停靠栏,需要再次重写 M F C函数。但是该函数规模较小,而且在 未来M F C版本中也不可能会改变。停靠栏一般由 C F r a m e W n d的E n a b l e D o c k i n g ( )函数内部的 C D o c k B a r类创建。但是当 E n a b l e D o c k i n g ( )函数创建这个类的时候,使用了固定代码名字 C D o c k B a r,这就不可能替换自己的 C D o c k B a r派生类。因此,如果真想要建立自己的 C D o c k B a r版本,就需要在 C M a i n F r a m e内部重新创建E n a b l e D o c k i n g ( )的功能。同样的,也可 以从拷贝M F C的\ s r c目录下w i n f r m . c p p内的原始代码开始着手这项工作。 2.13 实例 本书与控制条相关的例子包括: ■ 实例1— 为工具栏增加标记( l o g o )。 ■ 实例2— 为工具栏增加动画标记。 ■ 实例3— 创建命令条。 ■ 实例4— 可编程工具栏。 2.14 总结 本章讨论了工具栏和状态栏,它们都是长而窄的控件窗口,这类控件窗口在自身内部绘 制按钮和窗格。本章还讨论了如何利用 Windows API和M F C类创建控制条,而 M F C类方法则 使得这项工作简单化。然后讨论了控制条如何停靠以及它们之间以及与视窗口之间如何共享 主窗口的客户区。最后讨论了修改并增强控制条的几种可能的方式。 接下来将要讨论M F C应用程序如何与其所处的环境通信。 42第第第一部分第基第第础 下载下载下载 第 3 章 通 信 在第1章中我们讨论了怎样使用 S e n d M e s s a g e ( )和P o s t M e s s a g e ( )函数与应用程序的窗口通 信。如果要与其他的应用程序或者另一个系统中的应用程序通信又该怎么去做呢? 本章将概述6种应用程序与外界通信的方法,其中包括与其他操作系统、 I n t e r n e t以及串行 线之间的通信。另外,还要探讨这些通信连接方式如何允许程序员控制其他的应用程序甚至 使用它们。此外还要介绍用于与其他应用程序共享大量数据的方法。 3.1 进程间通信 应用程序之间的通信,不管是在同一系统上或是通过网络进行,都叫做进程间通信 ( I P C : Interprocess Communication)。M F C应用程序为进程间通信准备了下列 6种可用的途径: ■ 窗口消息( Windows message),允许与其他应用程序的窗口通信。这与先前用于与自己 的应用程序窗口通信所采用的窗口消息是一个概念。 ■ 动态数据交换(DDE: Dynamic Data Exchange),通过维护全局分配内存使得应用程序 间传递大量数据成为可能。其方式是在一块全局分配内存中手工放置大量的数据,然后使 用窗口消息传送该内存指针。 D D E提供了一种标准使得任何遵守该标准的应用程序都可以 采用它。 ■ 消息管道(Message Pipe),用于设置应用程序之间的一条永久通信通道,通过该通道 可以像自己的应用程序访问一个平面文件一样读写数据。 D D E的数据传送速度使人失望, 但是采用消息管道就可以无缝地发送数据到其他应用程序,而这些应用程序可能在其他系 统上。 ■ Wi n d o w s套接字( Windows Socket),它具备消息管道的所有功能,但遵守一套通信标准 使得不同操作系统之上的应用程序之间可以互相通信。这些操作系统可以是非 Wi n d o w s系统, 例如U N I X系统。实际上在开发新的应用程序时,使用 Wi n d o w s套接字比消息管道和D D E都更 为有利。 ■ I n t e r n e t通信,它让应用程序从I n t e r n e t地址上载或者下载文件。 ■ 串行/并行通信(Serial/Parallel Communication),它允许应用程序通过串行或者并行端口 与其他应用程序通信。 3.1.1 通信策略 虽然以上的每一种通信方式都是用不同的 Windows API或者M F C类调用的,但是使用它 们的过程却大同小异: 1) 用Windows API或者M F C类打开与其他应用程序之间的通信连接。 2) 读取或者写入对方应用程序。对某些方式而言可能意味着发送及接收消息。对另一些 方式,该过程则与读写普通文件没有太大的不同。 3) 关闭连接。3.1.2 同步和异步通信 每一种通信方式都可以是同步或者异步的。同步 ( s y n c h r o n o u s )通信使应用程序暂停,直 到它完成对其他应用程序的读或者写操作。异步 ( a s y n c h r o n o u s )通信则允许应用程序继续运行, 当系统完成读写操作时就会用一个事件标记或者调用指定的函数通知用户任务已经完成。 虽然异步通信听起来更为适当,特别是在应用程序要一次同其他几个应用程序会话的情 况下更是如此,但这种方式并不是面向对象的。程序员不是需要跳出对象提供静态回调函数 就是需要费心处理事件标记。同时,设置异步连接也更为复杂,更容易产生程序错误 ( b u g )。 解决方案是采用同步通信,但要将每个读写操作放进它自己的线程中,这样这些操作就 不会再阻止应用程序之间的通信。当操作完成后,线程可以自动地将读取的数据存回应用程 序。当然,它也可以设置事件标记,但这需要在更为友好的 M F C类的环境下才可以。实例 5 1 和实例5 2说明了怎么做到这一点。 注意 应用程序创建自己线程的能力只有Windows 95和Windows NT操作系统才具备。 如果是在Windows 3.1系统平台上开发,就需要使用异步通信。对异步通信来说,操作 系统本身将读写操作放进了自己的特定线程。 注意 用Wi n d o w s套接字的术语来说:同步通信的应用程序被认为是阻塞 ( b l o c k i n g )类 型的。 下面研究以上的每一种通信方式,其中包括如何打开和关闭通信连接以及如何利用连接 进行读写操作。 3.2 窗口消息 先前所述的窗口消息系统允许应用程序控制其窗口并处理来自用户的命令消息,这套消 息系统同样可以用于控制并处理来自其他应用程序的命令。正是由于全局分配每个窗口对象 才使得这一切成为了可能,这样任何应用程序都能够发送消息到其他应用程序窗口。关键在 于必须知道要同哪个窗口对话。 3.2.1 打开和关闭 打开与其他应用程序之间的通信连接,涉及到确定应用程序要发送消息到某个窗口所对 应的窗口句柄。麻烦在于,因为每个句柄不是在编译或者链接应用程序时创建而是在窗口被 创建的时候才生成,所以每次运行时都有可能不同。这样就不能知道该句柄。而且,也不可 能询问其他窗口其句柄是什么。因为正是需要那些窗口的句柄才可能发出这一请求!这里有 3 种技术可以得到其他窗口句柄: ■ 当一个父应用程序创建其子应用程序的时候,它将属于自己的一个窗口的句柄作为参 数传送。子应用程序然后可以将该句柄发送给自己的一个窗口句柄,这样通信就可以进行了。 一般情况下,这些应用程序之间交换的句柄都是普通句柄,但总是被创建为只进行通信的窗 口所隐藏。关闭连接也就只是关闭这些窗口。 ■ 但是,如果目标应用程序已经在运行了,则可以调用 Windows API函数: F i n d Wi n d o w ( ) 以找到适当的窗口。该函数将返回一个句柄,该句柄指向任何匹配特定的名字或者使用了特 定的窗口类的窗口。为了帮助 F i n d Wi n d o w ( )函数发现正确的窗口,可以用唯一的名字或者 44第第第一部分第基第第础 下载Windows 类创建如上所述的普通消息窗口。 ■ 得到正确窗口句柄的最后一种方式是采用 Windows API函数B r o a d c a s t S y s t e m M e s s a g e ( ),该函数将发送一个消息给每一个当前正在运行的应用程序。使用 B r o a d c a s t S y s t e m M e s s a g e ( )函数可以广播包含自己应用程序窗口句柄的特定消息,处理该消息的应用程序则随后以其句 柄作为答复,从而完成这个循环。请参考实例 4 9来了解该方法。 3.2.2 读与写 到现在为止,与其他应用程序通信已经成为一件并不复杂的事情了,所要做的仅仅是将 窗口句柄插入: : S e n d M e s s a g e ( )或者: : P o s t M e s s a g e ( )之中。还可以用 A t t a c h ( )函数将窗口句柄封 装到C W n d类中,然后利用 C W n d的其他方法来发送和寄送消息。发送消息向应用程序提供了 同步通信,寄送消息则提供异步通信。 虽然一般情况下需要向目标窗口发送标准的窗口消息 (如W M _ C L O S E,W M _ M O V E等), 但有时也要创建自己的窗口消息来完成某些特定任务。 可以在W M _ U S E R以上范围内定义消息来创建自己的窗口消息 I D。如下所示: #define WM_MYMESSAGE WM_USER + 1 但是,创建新窗口消息I D的更可靠的方法是向系统注册自己的消息,如下所示: #define IDString "MyMessage" MsgID = ::RegisterWindowMessage( IDString ); 这里I D S t r i n g是每个应用程序都关注的唯一文本字符串,M s g I D是要创建的消息I D。因此, 这种方式比为自己的消息 I D分配数值而可能引起应用程序内部的混乱效果要好。另外还可以 用描述字符串如O p e n F i l e和: : R e g i s t e r Wi n d o w M e s s a g e ( )函数来创建自己的消息I D。如果某应用 程序抢先注册了 O p e n F i l e ,接下来使用 O p e n F i l e 的应用程序就可以调用 : : R e g i s t e r WindowMessage ()返回同样的消息I D,因此不会有混淆的危险。 请参考实例4 9了解如何正确使用这些函数。 3.2.3 回顾 利用窗口消息向其他应用程序发送消息实际上是先得到其他应用程序句柄,然后向其发 送消息。请参见图3 - 1回顾全过程。 图3-1 窗口消息发送 第 3 章第通第第信第第45下载 应用程序A通过使 用: : F i n d Wi n d o w ( )来搜 索或者直接从应用程 序B获得窗口1的句柄 应用程序A使用 S e n d M e s s a g e ( )或 P o s t M e s s a g e ( )将消 息发送给该窗口句柄 应用程序 A 窗口 1 应用程序 B 窗口1可以是应用程序 B的 主窗口,也可以是仅用于 通信的隐藏窗口 窗口1可以通过在应用程 序B中保存数据或控制应 用程序 B来处理该消息3.3 动态数据交换 S e n d M e s s a g e ( )和P o s t M e s s a g e ( )函数允许同时向其他应用程序发送两个整型数值。因此传 送1 0 0 0个字节就需要2 5 0个消息。为了一次发送更多消息,可以将数据填满一块全局分配内存 并将其句柄作为参数之一传送。这种方式仅允许同自己设计的其他应用程序交换数据。为了 与非自己设计的应用程序交换数据就要采用 DDE(Dynamic Data Exchange)的标准,该方法已 经在动态数据交换库( D D E M L )中实现。 3.3.1 客户/服务器 D D E的采用引进了客户应用程序和服务器应用程序 ( C l i e n t / S e r v e r )的概念。在D D E的情况 下,几乎所有的数据都驻留在服务器上供客户应用程序来访问。此时的通信方式可以形容为: 服务器打出一块牌子,上面写着“对外办公”,客户则从服务器读写数据。 客户/服务器通讯方式将在以后章节详细讨论。 3.3.2 打开和关闭 为了打开通信连接,服务器应用程序通过广播自己的方式开始启动,程序如下所示: // initialize DDE services UINT DdeInitialize( LPDWORD pidInst, // a pointer to the instance PFNCALLBACK pfnCallback, // a callback function (see below) DWORD afCmd, // command and filter flags DWORD ulRes // reserved ) ; // create a string handle to the name of the service we are creating HSZ DdeCreateStringHandle( DWORD idInst, // returned by DdeInitialize() above LPTSTR psz, // pointer to service name string int iCodePage // CP_WINANSI or CP_WINUNICODE for Unicode ) ; // register the service H D D E D ATA DdeNameService( DWORD idInst, // returned by DdeInitialize() above HSZ hsz1, // string handle to service name 0 L , // reserved UINT afCmd // service name flags ) ; 然后客户应用程序如下连接到服务器: UINT DdeInitialize( ... ); // as seen above HCONV DdeConnect( DWORD idInst, // returned by DdeInitialize() above HSZ hszService, // handle to service name string HSZ hszTo p i c , // handle to topic name string (optional) PCONVCONTEXT pCC // pointer to structure with context data ) ; 为了在服务器应用程序中处理该连接,服务器的回调函数必须处理一个 X T Y P _ 46第第第一部分第基第第础 下载C O N N E C T消息: H D D E D ATA CALLBACK DdeCallback( type ... ) { switch( type ) case XTYP_CONNECT: return TRUE; // return FALSE to reject connection } 客户随后关闭该连接: BOOL DdeDisconnect( HCONV hConv // handle returned by DdeConnect() above ) ; 3.3.3 读和写 客户可以使用如下方式从服务器请求数据: H D D E D ATA DdeClientTr a n s a c t i o n ( NULL, 0, HCONV hConv, // handle returned by DdeConnect() above HSZ hszItem, // string handle of name of data to read // and return UINT wFmt, // format of data based on clipboard formats X T Y P _ R E Q U E S T, // <<<<<<<<<<<<<> data; // and any other data 写数据到应用程序则使用: CSocketFile sockFile( &sock ); CArchive ar( &sockFile,CArchive::store ); ar << data; 3.5.4 数据流和数据报 Wi n d o w s套接字可以是两种方式之一:数据流 ( S t r e a m )或者数据报( D a t a g r a m )。本书提供 的所有实例采用的都是数据流模式。只有一些应用程序需要数据报。数据报开销不大但是很 不可靠。使用数据报的一个例子是:同步网络上所有系统的时钟。因为这种类型的应用程序 持续地发出消息,也许一个小时一次,丢失一两个报文也就无所谓了。 第 3 章第通第第信第第53下载3.5.5 回顾 Wi n d o w s套接字允许两个或者两个以上的应用程序相互通信,其通信平台可以在 Wi n d o w s 非系统之上。应用程序首先创建特殊的侦听套接字以侦听来自其他应用程序的会话请求。该 侦听应用程序随后创建另一个套接字以准备对话,请参见图 3 - 4。 图3-4 Windows套接字 3.6 串行/并行通信 串行和并行通信通常通过系统背后的端口和嵌入设备对话,例如网络节点、打印机或者 电话交换机。与这些设备的通信与访问磁盘普通文件没有太大不同。实际上使用了同样的 MFC CFile类。与一般通信的唯一不同是:这种通信方式在一个线程内完成同步读写并且不会 主动读取更多可用字节,请参考实例 5 2。 3.6.1 打开和关闭 为通信打开一个串行或者并行端口,需使用: CFile file; CFileException e; f i l e . O p e n ( p o r t N a m e , // examples "COM1", "COM2", "LPT2" C F i l e : : m o d e R e a d Wr i t e , & e ) ; 3.6.2 读和写 从该端口进行读写操作,需使用: UINT nBytes = // actual number of bytes read R e a d ( void* lpBuf, // buffer to store bytes 54第第第一部分第基第第础 下载 应用程序 A创建一个 “监听”套接字然后使 用C S o c k e t : : L i s t e n ( )查 找其他想与它对话的 应用程序请示 应用程序 B创建一个 套接字然后使用 C S o c k e t : C o n n e c t ( )试 图与应用程序 A连接 这样两个应用程序 之间使用C S o c k e t : : R e c e i v e ( )和C S o c k e t : : S e n d ( )便可以通信应用程序A通过创建一个新的套接字并将它 传送到 CSocket::Accept(), 以此在 C S o c k e t : : O n A c c e p t ( )中接受应用程序 B的通信请求 应用程序 应用程序 监听套 接字 通信套 接字 通信套 接字UINT nCount // number of bytes to read ) ; f i l e . Wr i t e ( void* lpBuf, // buffer to write UINT nCount // number of bytes to write ) ; 3.6.3 配置端口 所有并行端口创建时都是一样的,但是必须设置串行端口以匹配与其对话的设备。这就 意味着需要相同的波特率、奇偶位和停止位等。虽然可以通过操作系统设置这些参数,但也 可以用Windows API中的S e t C o m m S t a t e ( )函数来设置它们。一般地,可用如下程序来设置当前 的配置: DCB dcb; ::GetCommState( file.m_hFile, &dcb ); dcb.BaudRate = 1200, ...; dcb.ByteSize = 7 or 8; dcb.StopBits = 0,1,2 = 1, 1.5, 2; dcb.Parity = 0-4 = no,odd,even,mark,space; ::SetCommState( file.m_hFile, &dcb ); 串行口和并行口可以超时通信,这有时会不合乎要求,例如在线程内执行一个同步读写 就会导致消息在到来之前一直读下去。为了关闭该项超时功能,可以使用 : : S e t C o m m Ti m e o u t ( )函数,如下所示: COMMTIMEOUTS cto; : : G e t C o m m Timeouts( file.m_hFile, &cto ); c t o . R e a d I n t e r v a l Timeout = 0; c t o . Wr i t e To t a l TimeoutMultiplier = 0; c t o . Wr i t e To t a l TimeoutConstant = 0; : : S e t C o m m Timeouts( file.m_hFile, &sto ); 3.6.4 回顾 串行和并行通信可以像访问普通文件那样通过 API 来实现,系统的虚拟驱动程序负责执 行特定的工作。只有串行通信需要额外考虑与对方设备的参数匹配问题 (例如波特率等),请参 见图3 - 5。 图3-5 串行通信和并行通信 第 3 章第通第第信第第55下载 应用程序使用相同的C F i l e : : O p e n ( )打开一个端口,该函数同样 可打开一张盘文件。用于串行口 的文件名为“ C O M 1”、“C O M 2” 等等,用于并行口的文件名则为 “L P T 1”、“L P T 2”等等 串行口可以使 用: : S e t C o m m S t a t e ( ) 和: : S e t C o m m Ti m e o u t ( ) 应用程序可使用 C F i l e : : R e a d ( )和 C F i l e : : Wr i t e ( )来 存取设备中的数据 应用程序3.7 Internet通信 有几个M F C类封装了 Windows API的Internet Extension(Internet扩展功能、Wi n I n e t ),它 允许C + +利用下述4种协议之一访问I n t e r n e t:文件传送协议(FTP:File Transfer Protocol)、超文 本传送协议 (HTTP:Hypertext Transfer Protocol)、G o p h e r或者文件形式。可以用 M F C的 C I n t e r n e t S e s s i o n类连接到一个I n t e n e t的w e b地址。也可以直接用4种M F C类之一直接访问文件: C S t d i o F i l e、C H t t p F i l e、C G o p h e r F i l e或者C I n t e r n e t F i l e等。或者用下列3种连接类控制一个w e b 站点: C F t p C o n n e c t i o n、C h t t p C o n n e c t i o n、C G o p h e r C o n n e c t i o n等。这些文件类和连接类都是由 C I n t e r n e t S e s s i o n类创建的。 3.7.1 打开和关闭文件 为了打开一个I n t e r n e t文件,首先要打开一个I n t e r n e t会话: C I n t e r n e t S e s s i o n ( ) ; // there are several arguments but you // will typically use the defaults 然后用C I n t e r n e t S e e s i o n类的O p e n U R I ( )成员函数打开一特定类型的文件: CStdioFile*pFile = // see Table 3.1 for returned object type s e s s i o n . O p e n U R L ( LPCTSTR pstrURL // see Table 3.1 for sample values ) ; // There are several other arguments, however you will typically // use the default values. 打开的U R I类型取决于C I n t e r n e t S e e s i o n创建的类对象,如表3 - 1所示: 表3-1 OpenURI()参数说明 U R L类型 返回的I n t e r n e t类指针 U R L类型 返回的I n t e r n e t类指针 f i l e : / / C S t d i o F i l e * g o p h e r : / / C G o p h e r F i l e * h t t p : / / C H t t p F i l e * ftp:// C I n t e r n e t F i l e * 由该方法创建的文件对象具有只读属性。为了写文件以控制 w e b站点,参考以下的打开和 关闭连接的内容介绍。 3.7.2 读文件 读取I n t e r n e t文件类型和读取普通文件完全一样。实际上, C H t t p F i l e、C G o p h e r F i l e以及 C I n t e r n e t F i l e都是从C S t d i o F i l e派生的,这样就可以按照字节或者字符串数目读取文件; UINT pFile->Read( void* lpBuf, UINT nCount ); pFile->ReadString( CString str ); C H t t p F i l e允许通过对象和动词( v e r b )访问H T T P文件。 3.7.3 打开和关闭连接 C I n t e r n e t S e s s i o n类有3个成员函数允许打开 w e b站点,使用连接不仅可以读写文件还可以 56第第第一部分第基第第础 下载控制站点。例如,F T P连接就允许采用编程的方法在 f t p站点之上执行f t p命令。 1. FTP连接 为打开f t p连接,可以使用: CFtpConnection* ftp = GetFtpConnection(); CInternetFile* pFile = f t p . O p e n F i l e ( LPCTSTR pstrFileName, DWORD dwAccess = GENERIC_READ, // and/or // GENERIC_WRITE DWORD dwFlags = FTP_TRANSFER_TYPE_BINARY, // use default DWORD dwContext = 1 // use default ) ; 要打开该站点的文件,可以使用: CFtpFile *ftpFile = ftp.OpenFile( // same as CStdioFile open ) ; 2. Gopher连接 要打开G o p h e r连接,可以使用: CGopherconnection* gopher = GetGopherConnection(); 要打开该站点的文件,则可以使用: CGopherFile *gopherFile = gopher. O p e n F i l e ( // same as CStdioFile Open() ) ; 3. HTTP连接 要打开H t t p连接,可以使用: CHttpConnection* http = GetHttpConnection(); 要打开该站点的文件,则可以使用; CHttpFile* http.OpenRequest( // please refer to your MFC documentation for these arguments ) ; 3.7.4 其他Internet类 另外3种令人感兴趣的I n t e n e t客户类是C F t p F i l e F i n d、C G o p h e r F i l e F i n d和C G o p h e r L o c a t o r。 C F t p F i l e F i n d和C G o p h e r F i l e F i n d由C F i l e F i n d类派生,可以用于在 I n t e r n e t上寻找文件的位置。 C G o p h e r L o c a t o r类从 g o p h e r服务器得到一个 g o p h e r“定位器 ( l o c a t o r )”,并使之对 C G o p h e r F i l e f i n d可用。 3.8 通信方式小结 采用什么通信方式取决于应用程序。例如,有限的通信方式应该采用窗口消息。而对持 续或者高级通信则应该使用 Wi n d o w s套接字。如果要与嵌入设备,如打印机等通信则要使用 串行/并行通信,其余则使用I n t e r n e t通信。 第 3 章第通第第信第第57下载在新的应用程序开发过程中建议不要采用 D D E或者消息管道方式。 Wi n d o w s套接字对进 程间通信而言是一种相对灵活得多的解决方案,而且具备支持基础类的相关类。 尽管D D E比Windows Socket的数据通信速度要快,但在一次只通过电缆发送一个字节时 就无法替代后者了。 3.9 共享数据 到目前为止,本书已经覆盖了应用程序间主动通信 (active communication)的内容,主动 通信就是应用程序双方都要参加的通信。应用程序还有两种被动通信方式允许其他应用程序 访问其数据,如下所示: ■ 共享内存文件(Shared Memory File),就是利用前面所讨论的D D E方式使用共同的全局 分配内存,因而不能从其他计算机访问。 ■ 文件映射(File Mapping),允许应用程序之间共享文件和内存,甚至通过网络和其他 Wi n d o w s计算机之间也可以这样做。 注意 如果要与非Windows平台(如UNIX)共享数据,文件映射对此还不能适用。在这种 情况下,最好还是使用Wi n d o w s套接字。但不要担心,文件映射并不比Wi n d o w s套接字 能向你的应用程序提供更快的数据访问速度,它仍然只是通过网络一次一个字节来共 享数据。 注意 应用程序也可以通过剪贴板共享数据,但是剪贴板通常用于用户和应用程序交互。 3.10 共享内存文件 如果并不需要遵守 D D E的标准并且也不需要通过网络共享数据,那么可以使用自己的全 局分配内存来与其他应用程序共享数据。实际上, M F C的C S h a r e F i l e类使得创建和访问全局 内存方便得犹如创建普通文件。 3.10.1 创建和销毁 创建和销毁C S h a r e F i l e类的实例将会创建和销毁全局内存: CSharefile file; 3.10.2 读和写 读写共享内存文件和读写普通文件是完全一样的。因此,作为完整的数据传送例子,可 如下编程: CSharedFile file; f i l e . Write( buff e r, nBytes ); // extract global memory handle from CFile object to send another application HGLOBAL hgbl = file.Detach(); SendMessage( hWnd, WM_MYMESSAGE, ( WPARAM ) hgbl,0 ); 在接收程序中读取数据则使用: H R E S U LT OnMyMessage( WPARAM wParam, LPARAM lParam ) { 58第第第一部分第基第第础 下载CSharedFile file; // encapsulate global memory handle in this CFile object file.SetHandle( ( HGLOBAL ) wParam ); file.Read( buff e r,nBytes ); } 3.10.3 回顾 可以通过全局内存共享数据,这是用 C S h a r e F i l e类来实现的,所采用的函数如同访问普通 文件一样,请参见图3 - 6。 图3-6 共享内存文件 3.11 文件映射 如果要在应用程序之间 (包括其他Wi n d o w s平台上的应用程序 )共享数据,可以使用文件 映射。与全局分配内存不能一次由一个以上应用程序访问的特点所不同的是,文件映射允许 两个或者两个以上的应用程序同时访问共享内存。举个例子,可以在两个应用程序中共享一 个数组B O b [ 1 0 0 ],在应用程序 A中的B o b [ 2 3 ]里存储3 4,将在应用程序 B的B o b [ 2 3 ]中同样出 现3 4。 这种共享方式实际上发生在文件中,所以被命名为文件映射。在一个系统中共享数据的 时候,一般只是使用缺省的文件,该文件实际上是用于向系统提供虚拟内存的交换文件。如 果要通过网络共享则需要自己打开文件并提供其句柄。 3.11.1 打开和关闭 要打开一段交换文件以共享内存,需使用: m_hMap = ::CreateFileMapping( ( HANDLE ) 0xffffffff , // or can be an open file handle 0 , // security PA G E _ R E A D W R I T E , // or PA G E _ R E A D O N LY or PA G E _ W R I T E C O P Y 0 , // size - - high order // (required if no file handle) 0 x 1 0 0 0 , // size - - low order // (required if no file handle) 第 3 章第通第第信第第59下载 应用程序A使用C S h a r e d F i l e 打开一个共享内存文件,并 使用C S h a r e d F i l e : : Wr i t e ( )向 其中装载数据 应用程序A随后从 C S h a r e d F i l e 中抽取出全局内存句柄,并将 它发送给应用程序 B 应用程序 B将该内存句 柄加入到一个 C S h a r e d F i l e对象并使 用CSharedFile:: Read() 来读取该内存 应用程序 应用程序 全局内存 S e n d M e s s a g e (句柄)M A P _ I D // unique id - - required if no file handle ) ; 返回的映射句柄可被转变为内存指针,如下所示: m_pSharedData = ::MapViewOfFile( m_hMap, F I L E _ M A P _ W R I T E , // or FILE_MAP_READ, FILE_MAP_COPY // (FILE_MAP_WRITE is read/write) 0 , // offset - - high order 0 , // offset - - low order 0 // number of bytes (zero maps entire file) ) ; // When using swap file, offset must be zero(0) 要关闭共享内存,需使用: : : U n m a p Vi e w O f F i l e ( m _ p S h a r e d D a t a ) ; : : C l o s e H a n d l e ( m _ h M a p ) ; 3.11.2 读和写 假设正如上面提到的,应用程序之间正在共享一个名叫 B o b的数组,可以在应用程序中使 用以下代码: int *Bob = ( int * ) pSharedData; Bob[23] = 34; 3.11.3 数据同步 在同一系统上对映射内存的所有访问都是同步的。换句话说,两个应用程序不能同时访 问同一内存。这就阻止了一个应用程序在其他应用程序正对内存区进行写入操作的同时向其 中读取数据。 通过网络映射文件也可能出现麻烦。如果正在共享的文件在另一个系统之中,则应用程 序实际上是在写入网络缓存,而该缓存超过一定时间即被发送到其他系统。与此同时,其他 系统上的应用程序也可能正在写该文件,在这种情况之下需要保证应用程序之间不互相竞争。 3.11.4 回顾 内存的共享可以用文件的方式来实现。对两个应用程序而言则没有其他办法共享同一地 址空间,请参见图3 - 7。 图3-7 文件映射 60第第第一部分第基第第础 下载 应用程序A使用 : : C r e a t e F i l e M a p p i n g ( ) 和: : M a p Vi e w O f F i l e ( ) 映射到文件 该文件可以是本系统 中的交换文件或者是 其他系统上用于在网 络上共享数据的文件 应用程序B使用 相同的 A P I调用 和参数来映射到 相同的文件 现在两者都可以 使用内存指针来 访问该文件,这 意味着读写其中 的数据就和访问 所有数据内存一 样简单 文件应用程序 应用程序3.12 客户/服务器 程序员不仅能够向其他应用程序发送消息或者与其共享数据,甚至还能通过间接调用其 成员函数以访问其功能。按照这种方式共享其功能的应用程序叫服务器,就是提供服务的一 方。而访问这些功能的应用程序则叫做客户,也就是接受服务的客户。 允许应用程序与其他应用程序分享其功能,可以通过以上提到的任何通信方式来完成, 基本上采用同样的步骤: 1) 创建新的命令消息代表想要访问的函数 (例如:I D C _ C A L L _ H O M E是对应H o m e ( )方法 的)。 2) 向提供其功能的应用程序 (服务器)发送该消息并在同一消息中传递任何所需函数的必 要参数: wParam = x; lParam = y; SendMessage( hWnd,IDC_CALL_HOME,wParam,lParam ); 3) 服务器然后将消息和参数转变为对所需要功能的实际函数调用: case IDC_CALL_HOME: x = wParam; y = lParam; res = Home( x,y ); : : : 4) 由函数返回的任何值连同该消息都被返回给客户: return res; 或者与新的消息一道: wParm = y; SendMessage( IDC_CALL_HOME_REPLY, wParam, lParam) ; 5) 客户然后通过把这些值放进本地内存而响应一个应答消息: case IDC_CALL_HOME_REPLY: y = wParam; 请参见图3 - 8概览该技术: 图3-8 间接调用外部函数 3.12.1 传递调用参数 如何传递参数取决于使用什么通信方法。正如先前看到的,窗口消息机制允许通过消息 传递两个参数。为了传递额外的参数,就需要将它们填充到某种数据结构中,并通过 D D E、 第 3 章第通第第信第第61下载 应用程序 应用程序 应用程序A将函数调用 转变为窗口消息和相应 的参数 应用程序B将窗口消 息转变为带参数的 函数调用文件映射或者全局分配内存等方式传递该结构。 不能仅仅把该结构的指针传递给其他应用程序,这是因为该结构存在于不同于其他应用 程序所处的地址空间(指向一个应用程序的指针不会指向其他应用程序中的同样数据 )。这也就 意味着不能向消息结构中加入指针,但整个数组内容和参数数据却必须包含在该消息结构里 以供其他应用程序访问。 当服务器应用程序在其他系统上时,不能使用 D D E或者全局内存来传递参数—因为没有 办法让其他应用程序来访问它。还可以使用文件映射来包含该参数或者通过消息本身传递。 如果通过通信过程传递参数,任何返回值都必须被返回客户应用程序。那就意味着服务器可 能修改数组,整个数组必须被返回给客户。文件映射方式相同,但后台实现不同。总之,让 大量参数来回传递可能会很慢,因此必须计划周密。 3.12.2 远程过程调用 参数经常以通信的方式在系统之间传递。实际上,一种名为远程过程调用 ( R e m o t e Procedure Calling,R P C )的Windows API可用于处理许多通过网络访问服务器应用程序这样的 问题,该A P I负责传递消息中的参数。 R P C经常是作为微软的组件对象模型 ( C O M )的一个组件 来使用的,有关内容已经超出本书的介绍范围。 3.13 小结 本章讨论了6种使应用程序与其所处环境通信的可用方式: ■ 窗口消息作为简单的进程间通信。 ■ D D E传送大量数据。 ■ 消息管道通过网络通信。 ■ Wi n d o w s套接字通过网络与非Wi n d o w s平台通信。 ■ I n t e r n e t类通过I n t e r n e t通信。 ■ 串行/并行通信与连接到系统串行口或者并行口的设备对话。 从本章还了解到Wi n d o w s套接字因为其网络分布运算的能力而取代了 D D E和消息管道。 同时还学习了如何在应用程序之间共享数据: ■ 共享内存文件,这种方式由 M F C的C S h a r e F i l e类创建以封装全局分配内存供 C + +方便地 访问。 ■ 文件映射,这种方式允许数据共享,甚至还可以通过网络进行,但是不适用于非 Wi n d o w s平台。 本章包含了本书的纯文本部分,在其他部分将换一个角度来观察 MFC 和Visual C++。并 且提出一些可增加到应用程序中的实际特色,采用循序渐进的方式指导读者来实现它们。由 于可能包括更多解释如何实现这些特色的注解,因此这些实例可能被扩充或者增强。 62第第第一部分第基第第础 下载下载下载 本部分的实例将把重点放在应用程序用户界面的各个方面,这些界面可以由 D e v e l o p e r S t u d i o、M F C和Visual C++来创建。用户界面开发工具是本部分密切关注的内容,所以该部分 内容包括了本书的大部分实例。本部分的各个章节是: 第4章 应用程序和环境 本章的实例覆盖了应用程序与其所处环境交互的各个方面,内容包括应用程序如何运行 以及应用程序的外观如何显现。实例的范围包括寻找空间添加标识以及阻止同时运行同一个 应用程序等。 第5章 菜单和控件条 这一章的内容涉及应用程序的菜单和控制条,这也许是用户同应用程序之间进行交互的 主要方式。A p p Wi z a r d自动地为应用程序添加基本的菜单、工具栏和状态栏,但是这些菜单和 控制条是很简单的,连 Developer Studio的都比不上。自己动手费点力可以为自己的应用程序 增加与Developer Studio类似的外观。 第6章 视 如果创建S D I或者M D I应用程序,则此时应用程序的视将是用户与应用程序,特别是与正 在编辑中的属于应用程序的文档之间进行交互的主要方式。本章所有的实例都与视相关,从 创建一个属性表、打印视到拖放文件到视中等等。 第7章 对话框和对话条 对话框和对话条用于提示用户使用不同类型的控件,比如按钮和列表框等等。 M F C和 Developer Studio自动创建对话框和对话框类。本章将探讨手工修改对话框的各种方式,使之 更加符合用户的要求。 第8章 控件窗口 对话框上添加了按钮和编辑框等控件窗口(操作系统提供的子窗口 )。不仅可以在对话框中 采用它们,还可以把它们放在视、控制条和任何窗口之内。程序员甚至可以自己绘制它们。 第9章 绘图 位图和图标可以为自己的应用程序增加颜色和风格。因为所有的窗口界面都类似,所以 标识符和s p l a s h屏幕才是真正区别不同程序外观的唯一方式。显而易见,绘图对创建自己的控 件以及在C A D应用程序中显示图表也是很重要的。 第二部分 用户界面实例第10章 帮助 比起阅读手册里的各种命令,在线帮助有助于大量减少学习应用程序所需要的时间和困 难,用户可以直接查询控件,本章实例描述了如何实现 3种类型的在线帮助。 第11章 普通窗口 视、对话框和控制条、控件窗口和工具栏等组成了 M F C应用程序的界面,但它们都是建 立在低级窗口的基础之上。为什么在其他元素都很复杂的情况下却采用这样类型的窗口呢? 这是因为,在这个级别上编写程序可以达到高级窗口才能达到的效果。 第12章 特定的应用程序 本章用一些专门的 M F C应用程序来概述全部分。本章的实例包括一对简单的文本编辑器 和两个数据库编辑器。其中一个实例提供了创建自己的浏览器风格应用程序的全部内容。最 后,本章还包括了如何创建简单的 Wi z a r d的内容。 第 4 章 应用程序和环境 本章探讨应用程序和环境之间在各方面如何交互。本章的内容包括所有应用程序的运行 方式以及它们向用户呈现出的外观表现。这里的实例有:寻找新地方来添加标识符,在环境 中添加图标以及防止同时运行两个相同的应用程序。这些实例具体包括: 实例1 在工具栏中添加一个静态标识符,即在工具栏中添加一个程序附属的图片,使之 一直可见。 实例2 在工具栏中添加一个动态标识符 ,即上例中静态标识符的动态版本。 实例3 只启动一个实例,本例将防止同时运行应用程序中的多个实例。 实例4 创建一个对话框/ M D I混合式应用程序,本例将创建这两类标准的应用程序类型。 实例5 在系统托盘(System Tr a y )中添加图标,本例将单击系统托盘中用户自己的图标(系 统托盘是一个位于桌面上的图标集,与时间显示相邻 )。 实例6 在主窗口标题栏上显示标识符,本例利用应用程序的标题栏来显示标识符或其他 位图。 4.1 实例1:在工具栏中添加静态标识符 1. 目标 如图4 - 1所示,在应用程序工具栏的右上角处显示一个标识符。 2. 策略 工具栏是单控件窗口,可以在其客户区内绘制各类按钮。当用户单击某个按钮时,工具 栏就会根据该按钮的图像来触发相应的命令。这里只是简单地给应用程序的工具栏添加一个 子窗口,而且该子窗口远离其他按钮。所添加的子窗口是一个普通窗口,它可以显示一幅位 图,并且随着工具栏尺寸的改变而不断移动。 3. 步骤 64第第第二部分第用户界面实例 下载1) 创建一个用来绘制标识符的普通窗口 用Class Wizard 创建一个来自 generic CWnd的通用窗口。 在该通用窗口中,用光盘中为本 例提供的位图类嵌入一个位图变量, 如下所示: CWzdBitmap m_bitmap; 注意 在本系列的第一本书中, 创建位图类时不仅装入了一个位 图资源,而且也装入了位图的调 色板,其目的是用来准确地修改位图的颜色。 在该类中添加一个C r e a t e B X ( )成员函数,该函数将装入位图标识符,并同时在工具栏中创 建一个实际的窗口: void CWzdLogo::CreateBX( CWnd *pWnd, UINT nBitmapID, UINT nChildID ) { m_bitmap.LoadBitmapEx( nBitmapID,TRUE ); CRect rect( 0,0,0,0 ); // will be resizing constantly anyways Create( NULL,_T( " " ),WS_CHILD|WS_VISIBLE, rect, pWnd, nChildID ); } 添加另一个成员函数,该函数使得工具栏可以移动窗口: void CWzdLogo::MoveLogo( int nWidth, int nHeight ) { M o v e Window( nWi d t h - m _ b i t m a p . m _ Wi d t h , 0 , m _ b i t m a p . m _ Width,nHeight ); } 应用Class Wi z a r d添加一个在窗口中绘制位图的 W M _ PA I N T消息处理函数句柄,并以此来 结束该类: void CWzdLogo::OnPaint() { CPaintDC dc( this ); // device context for painting // get bitmap colors CPalette *pOldPal = dc.SelectPalette( m_bitmap.GetPalette(), FALSE ); d c . R e a l i z e P a l e t t e ( ) ; // get device context to select bitmap into CDC dcComp; dcComp.CreateCompatibleDC( &dc ); dcComp.SelectObject( &m_bitmap ); // draw bitmap dc.BitBlt( 0,0,m_bitmap.m_Width, m_bitmap.m_Height, &dcComp, 0,0,SRCCOPY ); // reselect old palette 可以在此处放置任 意类型的位图图片 图4-1 在工具栏中添加一个静态标识符 第 4 章第应用程序和环境第第65下载dc.SelectPalette( pOldPal, FALSE ); } 要查看该普通窗口类的总体情况,请参考本实例结尾处的程序清单— 标识符类。 2) 创建一个包含普通窗口类的自定义工具栏 使用Class Wizard 从C To o l B a r C t r l中创建一个新类。编辑所有生成的 . h和. c p p文件,以修 改所有从C ToolBarCtrl 到C To o l B a r的引用( C l a s s Wi z a r d不允许从C To o l B a r类派生一个类)。 在新的工具栏中嵌入新的普通窗口类,如下所示: C W z d L o g o m _ L o g o ; 使用Class Wi z a r d给工具栏添加一个 W M _ C R E AT消息处理函数句柄。用该句柄调用普通 窗口类的C r e a t B X ( )函数: int CWzdToolbar::OnCreate( LPCREATESTRUCT lpCreateStruct ) { if ( CToolBar::OnCreate( lpCreateStruct ) = -1 ) return -1; m_Logo.CreateBX( this,IDB_LOGO,-1 ); return 0; } 再一次使用Class Wi z a r d来添加一个W M _ S I Z E消息处理函数,这里要调用普通窗口类的 M o v e L o g o ( )函数: void CWzdToolbar::OnSize( UINT nType, int cx, int cy ) { C ToolBar::OnSize( nType, cx, cy ); m_Logo.MoveLogo( cx,cy ); } 要看该工具栏类的总体,请参考本实例结尾处的程序清单— 工具栏类。 3) 在应用程序中使用新的工具栏类 现在,在M a i n F r m . h文件中用新的工具栏类替换 C To o l B a r : // change CToolBar to CWzdTo o l B a r C W z d Toolbar m_wndTo o l B a r ; 不幸的是,本实例对可停靠的工具栏就不起作用了。这是因为可停靠的工具栏仅仅与它 所包括的最后一个按钮长度相同。而该实例却依赖于具有大量额外空间的工具栏,以便在结 尾处粘贴普通窗口。因此必须在注释工具栏处将其设为非停靠式 ( u n - d o c k a b l e ),或者在 C m a i n F r a m e : : O n C r e a t ( )中删除以下各列: int CMainFrame::OnCreate( LPCREATESTRUCT lpCreateStruct ) { : : : // comment out or delete these lines // m_wndTo o l B a r.EnableDocking( CBRS_ALIGN_ANY ); // EnableDocking( CBRS_ALIGN_ANY ); // DockControlBar( &m_wndToolBar ); 66第第第二部分第用户界面实例 下载return 0; } 4. 注意 ■ 实际上,可以利用本实例在父窗口的任何空白区粘贴标识符。只要在父窗口的类中嵌 入这个窗口,并使用Class Wizard 添加一个W M _ C R E AT消息处理函数句柄就可以创建标识符 窗口。 ■ 如果想在可停靠的工具栏中粘贴一个标识符,请参考第 2章。在第2章中,你会发现: 通过在C ToolBar 类中重载C a l C D y n a m i c L a y o u t ( )可以扩大工具栏的尺寸使之包含你的标识符。 无论如何都要确保提供该标识符的水平和垂直两个版本。 5. 使用光盘时注意 运行随书附带光盘上的工程时,会注意到右手方向的工具栏上写有 Your Company的字 样。 6. 程序清单— 标识符类 #if !defined WZDLOGO_H #define WZDLOGO_H // WzdLogo.h : header file / / #include "WzdBitmap.h" / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdLogo window class CWzdLogo : public CWnd { // Construction p u b l i c : C W z d L o g o ( ) ; // Attributes p u b l i c : // Operations p u b l i c : void CreateBX( CWnd *pWnd, UINT nBitmapID, UINT nChildID ); void MoveLogo( int nWidth, int nHeight ); // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CWzdLogo ) // }}AFX_VIRT U A L // Implementation p u b l i c : virtual ~CWzdLogo(); // Generated message map functions 第 4 章第应用程序和环境第第67下载p r o t e c t e d : // {{AFX_MSG(CWzdLogo) afx_msg void OnPaint(); // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : CWzdBitmap m_bitmap; } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / # e n d i f // WzdLogo.cpp : implementation file / / #include "stdafx.h" #include "WzdLogo.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdLogo C W z d L o g o : : C W z d L o g o ( ) { } C W z d L o g o : : ~ C W z d L o g o ( ) { } BEGIN_MESSAGE_MAP( CWzdLogo, CWnd ) // {{AFX_MSG_MAP( CWzdLogo ) O N _ W M _ PA I N T ( ) // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdLogo message handlers void CWzdLogo::OnPaint() { CPaintDC dc( this ); // device context for painting 68第第第二部分第用户界面实例 下载// get bitmap colors CPalette *pOldPal = dc.SelectPalette( m_bitmap.GetPalette(),FALSE ); d c . R e a l i z e P a l e t t e ( ) ; // get device context to select bitmap into CDC dcComp; dcComp.CreateCompatibleDC( &dc ); dcComp.SelectObject( &m_bitmap ); // draw bitmap dc.BitBlt( 0, 0, m_bitmap.m_Width,m_bitmap.m_Height, &dcComp, 0, 0, SRCCOPY ); // reselect old palette d c . S e l e c t P a l e t t e ( p O l d P a l , FA L S E ) ; } void CWzdLogo::CreateBX( CWnd *pWnd, UINT nBitmapID, UINT nChildID ) { m_bitmap.LoadBitmapEx( nBitmapID,TRUE ); CRect rect( 0,0,0,0 ); Create( NULL,_T( " " ),WS_CHILD|WS_VISIBLE,rect,pWnd,nChildID ); } void CWzdLogo::MoveLogo( int nWidth, int nHeight ) { M o v e Window( nWi d t h - m _ b i t m a p . m _ Wi d t h , 0 , m _ b i t m a p . m _ Width,nHeight ); } 7. 程序清单— 工具栏类 #if !defined WZDTO O L B A R _ H #define WZDTO O L B A R _ H // WzdTo o l b a r.h : header file / / #include "WzdLogo.h" / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdToolbar window class CWzdToolbar : public CTo o l B a r { // Construction p u b l i c : C W z d To o l b a r ( ) ; // Attributes p u b l i c : // Operations p u b l i c : 第 4 章第应用程序和环境第第69下载// Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRT U A L ( C W z d To o l b a r ) // }}AFX_VIRT U A L // Implementation p u b l i c : virtual ~CWzdTo o l b a r ( ) ; // Generated message map functions p r o t e c t e d : // {{AFX_MSG(CWzdTo o l b a r ) afx_msg void OnSize( UINT nType, int cx, int cy ); afx_msg int OnCreate( LPCREATESTRUCT lpCreateStruct ); // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : CWzdLogo m_Logo; } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / # e n d i f // WzdTo o l b a r.cpp : implementation file / / #include "stdafx.h" #include "wzd.h" #include "WzdTo o l b a r. h " #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTo o l b a r C W z d To o l b a r : : C W z d To o l b a r ( ) { } C W z d To o l b a r : : ~ C W z d To o l b a r ( ) { } BEGIN_MESSAGE_MAP( CWzdTo o l b a r, CToolBar ) 70第第第二部分第用户界面实例 下载// {{AFX_MSG_MAP(CWzdTo o l b a r ) O N _ W M _ S I Z E ( ) O N _ W M _ C R E AT E ( ) // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdToolbar message handlers int CWzdToolbar::OnCreate( LPCREATESTRUCT lpCreateStruct ) { if ( CToolBar::OnCreate(lpCreateStruct) = -1 ) return -1; m_Logo.CreateBX( this,IDB_LOGO,-1 ); return 0; } void CWzdToolbar::OnSize( UINT nType, int cx, int cy ) { C ToolBar::OnSize( nType, cx, cy ); m_Logo.MoveLogo( cx,cy ); } 4.2 实例2:在工具栏中添加动态标识符 1. 目标 如图4 - 2所示,要在应用程序工具栏的右上角处播放一个 AV I文件 (动态位图): 2. 策略 创建自己的工具栏,并在其较 远的一角粘贴一个动画控件。从 M F C的C Tool Bar类中产生工具栏, 并用M F C的CAnitmateCtrl 类创建动 画控件。 3. 步骤 1) 给应用程序资源添加一个 AV I文件 单击Developer Studio的I n s e r t 和R e s o u r c e s菜单命令,将会产生 Insert Resources对话框。单击I m p o r t并定位要找的AV I文件。 当提示资源类型时,输入 AV I。该命令会把指定的 AV I文件拷贝到项目的 \ r e s文件,并同时在 项目资源中添加一个AV I文件夹。使用鼠标右键单击资源 I D号,给它起一个恰当的名字。本实 例将它命名为:I D R _ M F C 2。 2) 创建一个自定义工具栏类 第 4 章第应用程序和环境第第71下载 用工具条中该命 令播放 AV I文件 图4-2 在工具栏添加一个动态标识符使用Class Wi z a r d创建一个来自C To o l B a r C t r l的新类。用文本编辑器修改所有 C To o l B a r C t r l 对C To o l B a r的引用(注意:Class Wi z a r d通常不允许从C To o l B a r中派生类)。 在新的工具栏类中嵌入一个 C A n i m a t e C t r l类: . h : CAnimateCtrl m_AnimateCtrl; 使用Class Wi z a r d给新的工具栏类中添加一个 W M _ C R E A RT消息处理函数。使用该句柄创 建上一步中要嵌入的动画控件窗口。同时装载前面创建的 AV I资源,并将其初始化为播放三 次: int CWzdToolbar::OnCreate( LPCREATESTRUCT lpCreateStruct ) { if ( CToolBar::OnCreate(lpCreateStruct) = -1 ) return -1; m_AnimateCtrl.Create( WS_CHILD| WS_VISIBLE| ACS_CENTER, CRect( 0,0,0,0 ), this, IDC_ANIMATE_CTRL ); m_AnimateCtrl.Open( IDR_MFC2 ); m_AnimateCtrl.Play( 0,-1,3 ); // play three times initially return 0; } 注意在初始时创建的动画控件窗口是没有尺寸的 ( C R e c t ( 0 , 0 , 0 , 0 ) )。这是因为程序员无论如 何都会修改它的尺寸,因此在这里尺寸就不重要了。 使用Class Wi z a r d给工具栏类添加一个W M _ S I Z E消息处理函数。在W M _ S I Z E消息处理函 数中将确定工具栏的当前尺寸,并将动画控件移动到左边。程序代码如下: void CWzdToolbar::OnSize( UINT nType, int cx, int cy ) { C ToolBar::OnSize( nType, cx, cy ); CRect rect; G e t WindowRect( &rect ); S c r e e n ToClient( &rect ); rect.left = rect.right-32; m _ A n i m a t e C t r l . M o v e Window( rect ); } 下面还要给工具栏添加两个辅助函数,以便允许其他类来播放这个 AV I文件: void CWzdTo o l b a r : : P l a y L o g o ( ) { m_AnimateCtrl.Play( 0,-1,-1 ); } void CWzdTo o l b a r : : S t o p L o g o ( ) { m _ A n i m a t e C t r l . S t o p ( ) ; } 要看该工具栏类的总体,请参考本实例结尾处的程序清单— 工具栏类。 72第第第二部分第用户界面实例 下载3) 在C M a i n F r a m e中使用新的工具栏类 用新的工具栏类替换当前由 M a i n F r a m e使用的类: // use CWzdToolbar for toolbar in Mainfrm.h C W z d Toolbar m_wndTo o l B a r ; 同样必须通过注释M a i n F r m . c p p中的以下程序行将工具栏的浮动和停靠性能置为禁用。否 则,工具栏将会自动改变其尺寸,使得它仅能包含它自己的按钮: // disable toolbar docking in Mainfrm.cpp // TODO: Delete these three lines if you don't want the toolbar to // be dockable // m_wndTo o l B a r. E n a b l e D o c k i n g ( C B R S _ A L I G N _ A N Y ) ; // EnableDocking(CBRS_ALIGN_ANY); // DockControlBar(&m_wndTo o l B a r ) ; 4. 注意 ■ 动画控件不能扩展到工具栏控件实际的边界。这是因为工具栏控件在其周围绘制了一 个空白边界。如果想将动画控件扩展到工具栏控件的实际边界,请参考站点 w w w. w d j . c o m上 的WDJ 5/99中的文章“使M F C停靠栏很酷”。 ■ AV I文件是由两个或多个已转换成为一个 . a v i文件框架的位图文件创建的。进行此转换 的应用程序可以在I n t e r n e t的共享软件包中找到。 5. 使用光盘时注意 运行随书附带光盘上的工程时,会注意到右手方向的工具栏上正在播放一个 AV I文件。 6. 程序清单— 工具栏类 #if !defined WZDTO O L B A R _ H #define WZDTO O L B A R _ H // WzdTo o l b a r.h : header file / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdToolbar window class CWzdToolbar : public CTo o l B a r { // Construction p u b l i c : C W z d To o l b a r ( ) ; // Attributes p u b l i c : // Operations p u b l i c : void PlayLogo(); void StopLogo(); // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CWzdToolbar ) 第 4 章第应用程序和环境第第73下载// }}AFX_VIRT U A L // Implementation p u b l i c : virtual ~CWzdTo o l b a r ( ) ; // Generated message map functions p r o t e c t e d : // {{AFX_MSG( CWzdToolbar ) afx_msg void OnSize( UINT nType, int cx, int cy ); afx_msg int OnCreate( LPCREATESTRUCT lpCreateStruct ); // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : CAnimateCtrl m_AnimateCtrl; } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / # e n d i f // WzdTo o l b a r.cpp : implementation file / / #include "stdafx.h" #include "wzd.h" #include "WzdTo o l b a r. h " #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTo o l b a r C W z d To o l b a r : : C W z d To o l b a r ( ) { } C W z d To o l b a r : : ~ C W z d To o l b a r ( ) { } BEGIN_MESSAGE_MAP( CWzdTo o l b a r, CToolBar ) // {{AFX_MSG_MAP(CWzdTo o l b a r ) O N _ W M _ S I Z E ( ) O N _ W M _ C R E AT E ( ) // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) 74第第第二部分第用户界面实例 下载/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdToolbar message handlers int CWzdToolbar::OnCreate( LPCREATESTRUCT lpCreateStruct ) { if ( CToolBar::OnCreate( lpCreateStruct ) == -1 ) return -1; m _ A n i m a t e C t r l . C r e a t e ( W S _ C H I L D | W S _ V I S I B L E | W S _ D L G F R A M E | W S _ E X _ C L I E N T E D G E | A C S _ C E N T E R , CRect( 0,0,0,0 ), this, IDC_ANIMATE_CTRL ); m_AnimateCtrl.Open( IDR_MFC2 ); m_AnimateCtrl.Play( 0,-1,3 ); return 0; } void CWzdToolbar::OnSize( UINT nType, int cx, int cy ) { C ToolBar::OnSize( nType, cx, cy ); CRect rect; G e t WindowRect( &rect ); S c r e e n ToClient( &rect ); rect.left = rect.right-32; m _ A n i m a t e C t r l . M o v e Window( rect ); } void CWzdTo o l b a r : : P l a y L o g o ( ) { m _ A n i m a t e C t r l . P l a y ( 0 , - 1 , - 1 ) ; } void CWzdTo o l b a r : : S t o p L o g o ( ) { m _ A n i m a t e C t r l . S t o p ( ) ; } 4.3 实例3:只启动一个实例 1. 目标 防止系统在任何时刻运行一个应用程序的多个实例。 2. 策略 M F C和Windows API通常不支持该功能,因此只能自己“动手”来实现。微软推荐的方 法是为应用程序在系统中创建一些唯一的东西,这些唯一的东西在运行之前可以检查出来。 例如,在本例中将要创建一个名为互斥体 ( m u t e x )的唯一资源。互斥体通常用来同步两个或多 个正在使用同一数据区的进程。在实例 5 9中使用的是封装互斥体的 M F C类,然而,由于必须 第 4 章第应用程序和环境第第75下载设置互斥体的确切名称,因此本例中将直接使用 Windows API。 3. 步骤 1) 设置应用程序 在应用程序类中使用 # d e f i n e定义一个唯一的名字。确保该名字唯一的办法是使用为 C O M 接口提供唯一 I D号的 G U I D程序发生器,用户可以在 V C + +的\ B I N目录下找到一个名为 G U I D G E N . E X E的程序,它就是G U I D程序发生器。使用 G U I D G E N . E X E定义唯一的名字的一 个实例如下: #define UNIQE_NAME "{F5EFF561-ECB3-11 d 1 - A 1 8 D - D C B 3 C 8 5 E B D 3 4 } " 2) 在I n i t I n s t a n c e ( )中创建互斥体 使用上面在应用程序类的 I n i t I n s t a n c e ( )函数的开始处定义的名字来创建一个互斥体,并保 存该互斥体的句柄— 在关闭互斥体时还要用到该句柄。如果应用程序的另一个实例已经存 在,: : C r e a t M u t e x ( )函数就会给由其他实例创建的互斥体返回一个句柄,而不是再创建一个新 的句柄。为了确定该句柄是否为应用程序已经运行的实例的互斥体的句柄,可以调用 G e t L a s t E r r o r ( )函数,如果是,则该函数将返回一个 E R R O R _ A L R E A D Y _ E X I S T S错误。可以 通过退出I n i t I n s t a n c e ( )函数返回一个FA L S E值来终止应用程序的运行。以下代码说明了上述过 程是如何完成的: BOOL CWzdApp::InitInstance() { m_hOneInstance = ::CreateMutex( NULL,FALSE,UNIQUE_NAME ); if ( GetLastError() = ERROR_ALREADY_EXISTS ) { AfxMessageBox( "Application already running!" ); return FA L S E ; } : : : } 3) 关闭互斥体 使用Class Wizard 重载应用程序类的E x i t I n s t a n c e ( )函数,同时关闭该互斥体的句柄: int CWzdApp::ExitInstance() { CloseHandle( m_hOneInstance ); return CWi n A p p : : E x i t I n s t a n c e ( ) ; } 4. 注意 ■ 不仅可以显示应用程序的实例已经运行的错误信息,而且还可以向其他的实例广播消 息,告诉它们将其窗口放置在前台。怎样向其他应用程序广播消息,可以参考实例 4 9。 ■ 关闭句柄是十分有必要的。当程序运行结束时, Wi n d o w s会自动清除它所创建的所有 互斥体。这使得这种方法更具有可靠性— 如果应用程序非正常终止,则它就不会有机会调 用C l o s e H a n d l e ( )函数,也就不会留下阻止应用程序再次运行的互斥体。 ■ 另一个确定应用程序是否有其他实例正在运行的方法是使用 C W n d : : F i n d Wi n d o w ( )函 数。F i n d Wi n d o w ( )用一个特殊的窗口标题和窗口类名来查找上一层的所有窗口。用户可以创 76第第第二部分第用户界面实例 下载建一个唯一但隐藏的普通窗口 (参见实例3 8 ),并使该窗口可以被以后的应用程序实例查找;或 者也可以查找应用程序的主窗口。如果主窗口没有唯一且不变的标题,就可以给它起一个唯 一的窗口类名以便查找。可以在 CmainFrame 的P r e C r e a t Wi n d o w ( )函数中创建这个唯一的窗口 类名。这种方法的缺点是在创建实例和创建主窗口之间存在着一定的时间延迟— 这样应用 程序的第二个实例可能会逃过检查而被启动,因此微软不推荐这种方法。 5. 使用光盘时注意 运行光盘上的项目两次。在试着运行第二次时,就会出现应用程序已经运行的消息。 4.4 实例4:创建对话框/MDI 混合式应用程序 1. 目标 创建一个由命令行标志所决定的对话框或 M D I应用程序。 2. 策略 使用A p p Wi z a r d创建一个MDI 应用程序。该应用程序会检查 I n i t I n s t a n c e ( )函数中/ d标志, 如果该标志置位,则创建的将是一个有模式对话框,而不是 M D I框架窗口。一旦该对话框关 闭,则使I n i t I n s t a n c e ( )函数返回一个引起应用程序结束的 FA L S E值。 3. 步骤 1) 设置应用程序 使用A p p Wi z a r d创建一个M D I应用程序。 使用对话框编辑器(Dialog Editor)和C l a s s Wi z a r d创建对话框的模板和类。 使用C l a s s Wi z a r d创建自己的C C o m m a n d L i n e I n f o类版本。修改该类以检查/ d标志。 2) 修改I n i t I n s t a n c e ( )来创建上述两种应用程序中的一种 用新的CCommandLineInfo 类代替原有的应用程序类: CWzdCommandLineInfo cmdInfo; ParseCommandLine( cmdInfo ); 如果命令行中出现 / d,将使用新的对话框类创建有模式对话框。在对话框关闭时,将从 I n i t I n s t a n c e ( )返回一个引起应用程序结束的 FA L S E值。具体情况请参见以下代码: // if user started with /d flag, start dialog app instead if ( cmdInfo.m_bDialogFg ) { CWzdDialog dlg; d l g . D o M o d a l ( ) ; return FA L S E ; } 4. 注意 本实例的另一类型是创建一个基于命令行选项的 M D I或S D I应用程序。开始时可以用 A p p Wi z a r d创建一个M D I应用程序,接着再根据某个特定标志的设置与否来定义 S D I或M D I文 档模板。特别地,可使用 A p p Wi z a r d创建S D I应用程序,并将它的 I n i t I n s t a n c e ( )结合到M D I应 用程序的I n i t I n s t a n c e ( )中。 5. 使用光盘时注意 当运行光盘上没有/ d选项的项目时,就会出现一个 M D I应用程序。使用/ d选项,则会出现 第 4 章第应用程序和环境第第77下载一个对话框应用程序。 6. 程序清单— C C o m m a n d L i n e I n f o类 #if !defined WZDCOMMANDLINEINFO_H #define WZDCOMMANDLINEINFO_H // WzdCommandLineInfo.h : header file / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdCommandLineInfo window class CWzdCommandLineInfo : public CCommandLineInfo { // Construction p u b l i c : C W z d C o m m a n d L i n e I n f o ( ) ; // Attributes p u b l i c : BOOL m_bDialogFg; // Operations p u b l i c : void ParseParam( const TCHAR* pszParam,BOOL bFlag,BOOL bLast ); // Overrides // Implementation p u b l i c : virtual ~CWzdCommandLineInfo(); } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / # e n d i f // WzdCommandLineInfo.cpp : implementation file / / #include "stdafx.h" #include "wzd.h" #include "WzdCommandLineInfo.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdCommandLineInfo C W z d C o m m a n d L i n e I n f o : : C W z d C o m m a n d L i n e I n f o ( ) 78第第第二部分第用户界面实例 下载{ m_bDialogFg = FA L S E ; } C W z d C o m m a n d L i n e I n f o : : ~ C W z d C o m m a n d L i n e I n f o ( ) { } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / void CWzdCommandLineInfo::ParseParam( const TCHAR* pszParam,BOOL bFlag, BOOL bLast ) { CString sArg( pszParam ); if ( bFlag ) { m_bDialogFg = !sArg.CompareNoCase( "d" ); } CCommandLineInfo::ParseParam( pszParam,bFlag,bLast ); } 4.5 实例5:在系统托盘中添加图标 1. 目标 在系统托盘(system tray)中添加图标(如图4 - 3所示,系统托盘是一个出现在桌面右下方的 图标集合)。 2. 策略 使用: : S h e l l _ N o t i f y I c o n ( ) Window API 调用给系统托盘添加一个图标。为了在 用户单击图标时能够接收到从该图标返 回的消息,需要创建自己的窗口消息, 并手工添加自己的消息处理函数。 3. 步骤 1) 设置应用程序 系统托盘图标通过向应用程序窗口发送窗口消息来向应用程序报告返回。在 MDI 或S D I 应用程序中,系统托盘图标是一种典型的主窗口。因此,需要给 C M a i n F r a m e类添加自己的系 统托盘逻辑。在对话框应用程序中,使用自己的 C D i a l o g类。 如下所示在CMainFrame 或C D i a l o g类中的包含文件中定义一个新的窗口消息。当用户单 击图标时,这个消息就会发送到该类所在的窗口: // define new window message to indicate user has clicked on icon // in system tray #define WM_SYSTEMTRAY WM_USER+1 2) 在系统托盘中创建图标 在CMainFrame 或C D i a l o g派生类中添加以下代码以创建图标。其中的 m _ h W n d变量是属 于该类的窗口句柄。使用 I D编辑器将I D _ S Y S T E M T R AY加入I D。如果应用程序显示的图标有 第 4 章第应用程序和环境第第79下载 图4-3 系统托盘中的图标 放在桌面右下角的系统托 盘中的用户自己的图标好几个,这里就会用不同的图标来表示。参见下一步了解如何添加 W M _ S Y S T E M T R AY消息 用于当用户单击图标时向应用程序发送该消息: // put icon in system tray N O T I F Y I C O N D ATA nid; nid.cbSize = sizeof( NOTIFYICONDATA ); nid.hWnd = m_hWnd; // handle of window that will receive // messages from icon nid.uID = ID_SYSTEMTRAY; // id for this icon nid.uFlags = NIF_MESSAGE|NIF_ICON|NIF_TIP; // the next three parameters are valid nid.uCallbackMessage = WM_SYSTEMTRAY; // message that icon sends when clicked nid.hIcon = AfxGetApp()->LoadIcon( IDI_SYSTEMTRAY_ICON ); // icon strcpy( nid.szTip, "System Tray Tip" ); // bubble help message for icon ::Shell_NotifyIcon( NIM_ADD,&nid ); 3) 从图标获取消息 在CMainFrame 或C D i a l o g派生类中手工添加一个新的消息处理函数。由于是手工添加, 因此必须确保将消息映射宏放在 { { } }括号之外,使Class Wi z a r d不能删除它: BEGIN_MESSAGE_MAP( CMainFrame, CMDIFrameWnd ) // {{AFX_MSG_MAP( CMainFrame ) : : : // }}AFX_MSG_MAP ON_MESSAGE( WM_SYSTEMTRAY, O n S y s t e m Tray ) 如下所示的代码用于处理来自图标的消息。其中 1 P a r a m参数包含实际的鼠标消息内容: // handle system tray message L R E S U LT CMainFrame::OnSystemTray( WPARAM wParam,LPARAM lParam) { // wParam = the nid.uID defined above // (useful if you have more then one icon in tray) // lParam = mouse message if ( wParam = ID_SYSTEMTRAY ) { switch( lParam ) { case WM_LBUTTO N D O W N : b r e a k ; case WM_RBUTTO N D O W N : b r e a k ; case WM_LBUTTO N D B L C L K : b r e a k ; } } return 1; } 80第第第二部分第用户界面实例 下载4) 从系统托盘中删除图标 在应用程序结束之前,确保使用以下代码从系统托盘中删除自己的图标。否则该图标将 一直存在,直到用户重新启动计算机时才消失。删除图标的理想位置是在创建图标的窗口的 W M _ C L O S E消息处理函数中,代码如下所示: // delete icon from system tray N O T I F Y I C O N D ATA nid; nid.cbSize = sizeof( NOTIFYICONDATA ); nid.hWnd = m_hWnd; nid.uID = ID_SYSTEMTRAY; nid.uFlags = 0; ::Shell_NotifyIcon( NIM_DELETE,&nid ); 注意 如果是在调试器中终止应用程序,那就没有机会删除系统托盘中的图标。但是, 如果拖动鼠标光标到这个“流浪的”图标上,系统将会迅速判定该图标不再需要并将 其自动删除。 4. 注意 系统托盘的典型用途是配置一个应用程序并注册自己的图标,然后使应用程序隐藏。为 了创建这种应用程序,可以使用 A p p Wi z a r d来创建一个对话框应用程序。如果乐意的话,还可 以将对话框转换为具有标签的属性表— 如实例4 6所示,但不要设置 Wi z a r d的模式。一旦应 用程序在系统托盘中注册好一个图标,它将使用 S h o w _ Wi n d o w ( S W _ H I D E )来使自己隐藏。如 果用户单击系统托盘中的相应图标,就会出现一个弹出式菜单或用 Show Wi n d o w ( S W _ H I D E ) 产生的应用程序对话框。 注意 隐藏应用程序的主窗口将同时使主窗口从任务栏中消失。 5. 使用光盘时注意 当运行随书附带光盘上的工程时,在系统托盘中就会出现一个新的图标。可以在 M a i n F r a m e . c p p中的O n S y s t e m Tr a y ( )中设置断点,并观察用鼠标单击图标的动作给应用程序所 报告的消息。 4.6 实例6: 主菜单状态栏中的标记 1. 目标 如图4 - 4所示,在主窗口的标题栏中显示一个标记或 其他位图。 2. 策略 标题栏通常是在窗口收到 W M _ N C PA I N T消息时由 系统绘制的。然而,标题栏也可以用来反映某个窗口的 激活与否— 通过窗口背景颜色由灰到亮的变化来判断。 因 此 , 必 须 截 获 发 送 给 主 窗 口 的 3 个 窗 口 消 息 : W M _ N C PA I N T、W M _ A C T I VAT E和W M _ N C A C T I VAT E, 然后再绘制自己的位图。 3. 步骤 第 4 章第应用程序和环境第第81下载 图4-4 标题栏中的标识符 使用一个位图而不是 普通文本来高亮度显 示应用程序窗口标题1) 创建并装载位图 创建一个较小的位图— 确保背景为灰色,这个位图将成为新的标题。将该位图分两次 添加到资源中— 一次是作为 I D B _ A C T I V E _ C A P T I O N _ B I T M A P,另一次是作为 I D B _ I N A C T I V E _ C A P T I O N _ B I T M A P。可以在两个标题中使用同一位图,这是因为稍后将为它们 替换灰色背景。 使用位图类C W z d B i t m a p (在前面书中的实例中已经介绍过 )分两次装载该位图。实际上, C W z d B i t m a p所做的只是用位图的灰色替换装载位图时用户自己所指定的颜色。在 C M a i n F r a m e类中嵌入两个C W z d B i t m a p变量: CWzdBitmap m_bitmapActive; CWzdBitmap m_bitmapInactive; 现在分两次装载在第一步创建的位图,告诉 C W z d B i t m a p首先用灰色替换当前系统标题栏 的颜色,先对激活窗口,然后是非激活窗口: m_bitmapActive.LoadBitmapEx( IDB_ACTIVE_CAPTION_BITMAP, ::GetSysColor( COLOR_ACTIVECAPTION ) ); m_bitmapInactive.LoadBitmapEx( IDB_INACTIVE_CAPTION_BITMAP, ::GetSysColor( COLOR_INACTIVECAPTION ) ); 2) 截获在非客户区的绘制消息 使用Class Wi z a r d给C M a i n F r a m e类添加三个消息处理函数: W M _ N C PA I N T、W M _ A C I T I V E TA和W M _ N C AT I VAT E。由于该类归属于应用程序的主窗口,因此它将接收导致主 窗口被绘制的消息。 如下所示,填充这些消息处理函数。注意,必须给 C M a i n F r a m e类添加一个m _ b A c t i v e变 量,以跟踪主窗口是否被激活。同时也要注意用下面所给的方法调用 D r a w Ti t l e ( )辅助函数来 进行绘制: // WM_NCPAINT message handler void CMainFrame::OnNcPaint() { C M D I F r a m e W n d : : O n N c P a i n t ( ) ; // draw title D r a w Ti t l e ( ) ; } // WM_ACTIVE message handler void CMainFrame::OnActivate( UINT nState, CWnd* pWndOther, BOOL bMinimized ) { CMDIFrameWnd::OnActivate( nState, pWndOther, bMinimized ); // set state and draw title BOOL m_bActive; m_bActive = ( nState! = WA_INACTIVE ); D r a w Ti t l e ( ) ; } // WM_NCACTIVATE message handler BOOL CMainFrame::OnNcActivate( BOOL bActive ) 82第第第二部分第用户界面实例 下载{ BOOL b = CMDIFrameWnd::OnNcActivate( bActive ); // set state and draw title m_bActive = bActive; D r a w Ti t l e ( ) ; return b; } 3) 绘制标题栏 给C M a i n F r a m e类添加一个 D r a w Ti t l e ( )函数,该函数的起始处应确定是否有标题需要绘 制: void CMainFrame::DrawTi t l e ( ) { // if window isn't visible or is minimized, skip if ( !IsWi n d o w Visible() || IsIconic()) r e t u r n ; 根据窗口是否激活,将合适的位图对象选入内存设备环境: CDC memDC; CDC* pDC = GetWi n d o w D C ( ) ; memDC.CreateCompatibleDC( pDC ); memDC.SelectObject( m_bActive ? &m_bitmapActive:&m_bitmapInactive ); 计算绘制位图的位置。既要避免在最左方的图标上绘制,又要避免在最右方的窗口按钮 (关闭,最小化等按钮)上绘制。另外还要避免在窗口的边界上绘制: CRect rect, rectWnd; G e t WindowRect( &rect ); rect.top += GetSystemMetrics( SM_CYFRAME )+1; // for small caption use SM_CYDLGFRAME rect.bottom = rect.top + GetSystemMetrics( SM_CYSIZE )-4; // for small caption use SM_CYSMSIZE rect.left += GetSystemMetrics( SM_CXFRAME ) + // for small caption use SM_CXDLGFRAME GetSystemMetrics( SM_CXSIZE ); // for small caption use SM_CXSMSIZE rect.right -= GetSystemMetrics( SM_CXFRAME ) - // for small caption use SM_CXDLGFRAME ( 3 * // set to number of buttons already in caption + 1 GetSystemMetrics( SM_CXSIZE ) )-1; // for small caption use SM_CXSMSIZE G e t WindowRect( rectWnd ); r e c t . O ffsetRect( -rectWnd.left, -rectWnd.top ); 绘制位图然后再清除: pDC->BitBlt( rect.left, rect.top, rect.Width(), rect.Height(), &memDC, 0, 0, SRCCOPY ); m e m D C . D e l e t e D C ( ) ; ReleaseDC( pDC ); } 第 4 章第应用程序和环境第第83下载4. 注意 在该位图填充标题栏以前,旧标题仍然将被绘制,这样在填充时会引起一些闪烁。为减 少这种闪烁可以通过在 C M a i n F r a m e的PreCreate Wi n d o w ( )函数中添加如下代码来防止文档标 题包含在旧标题中: BOOL CMainFrame::PreCreateWindow( CREATESTRUCT& cs ) { cs.style &= ~ FWS_ADDTO T I T L E ; return CMDIFrameWnd::PreCreateWindow( cs ); } 也可以完全删除旧标题,但是这个标题仍然在应用程序任务栏中出现。 5. 使用光盘时注意 当运行随书附带光盘上的工程时,会注意到在应用程序的标题栏中通常的文字已被位图 所代替。 84第第第二部分第用户界面实例 下载下载下载 第 5 章 菜单、控件条和状态栏 菜单和控件条(如工具栏)代表了一种用户与应用程序之间交互的基本方式。 A p p Wi z a r d会 自动地给应用程序添加通用菜单、工具栏和状态栏。但是,这些菜单、工具栏和状态栏与用 户在Developer Studio中使用的菜单和控件条相比就显得非常“暗淡无光”。然而通过一些小 小的努力,就可以给应用程序添加外观与通用菜单相同的菜单和控件条。本章包括以下具体 实例: 实例7 在菜单中添加图标,可以模仿在 Developer Studio的菜单中看到的图标。 实例8 调整命令条外观,可以模仿在 Developer Studio中看到的工具栏的外观。 实例9 创建可编程工具栏,可以模仿在 Developer Studio中工具栏的“感觉”—用户可以 从工具栏按钮库中给工具栏选择并设置一个表示其功能的名字。 实例10 在对话框应用程序中添加一个工具栏和状态栏,这里将给对话框应用程序手工 添加一个工具栏和状态栏。 实例11 给弹出菜单添加一个位图标记,并沿着弹出菜单的边界绘制一幅位图。 实例12 在工具栏中添加下拉按钮 (Dropdown Button),创建两个工具栏按钮,用它们来 创建看起来像是从工具栏下拉 (drop down)的弹出菜单。 实例13 在状态栏中添加一个图标,本实例在状态栏中添加一个状态图标。 实例14 使用伸缩条 ( R e b a r ),给应用程序添加一个伸缩条,并用工具栏和对话框条填 充它。 5.1 实例7:在菜单中添加图标 1. 目标 模仿Developer Studio中的菜单,在每个菜单项旁边显示一个图标,而且这些菜单都有一 个工具栏按钮,如图5 - 1所示。 2. 策略 上面所看到的菜单是一个自绘制 ( o w n e r- d r a w n )的菜 单。为了创建一个自绘制的菜单,必须在一开始时就在每 个菜单项中设置一个选项,告诉系统这是一个自绘制的菜 单。由于在应用程序中没有提供菜单编辑器,因此必须动 态地完成这些事情。然后自绘制菜单就向自己的窗口发送 两个消息( W M _ M E A S U R E I T E M和W M _ D R AW I T E M )并 通过绘制菜单项时来处理该消息。本例将所有这些功能都 封装到从M F C的C M e n u类派生的菜单类中。这个新类中 的I n i t M e n u ( )函数不仅为每个菜单项进行标记表示该菜单 为自绘制菜单,而且还将菜单命令和它们的相对应工具栏匹配起来,同时还将在绘制菜单项 时使用匹配的工具栏按钮位图。 图5-1 在菜单中添加图标 在本例中,将在与工具条功能等效 的菜单旁边绘制相同的工具条图标3. 步骤 1) 创建一个来自C M e n u的新类 单击Developer Studio中的I n s e r t和New Class菜单命令来打开New Class对话框。从C l a s s t y p e组合框中选择 Generic Class。输入一个新类名,并由 C M e n u派生该类。此处不用 C l a s s Wi z a r d完成这些工作的原因是Class Wi z a r d通常不支持C M e n u。正因为如此,必须手工向该类 加入其他东西。 本实例仍然允许使用菜单编辑器来编辑应用程序的菜单。因此这个新菜单类的 I n i t M e n u ( ) 函数可以完成以下一些工作。首先,它可以将应用程序中所有的菜单项转换为自绘制的菜单 项。其次,由于自绘制的菜单不能保存自己的文本名,因此必须将每个菜单项的名字保存到 自己的数组中,还要在数组中保存每个菜单项使用像素绘制时所需的大概尺寸,以便以后快 速绘制。同时,在应用程序的工具栏中将每个菜单项命令的 I D号和其相对应的命令 I D号匹配 起来。指向工具栏的指针将作为 I n i t M e n u ( )函数的一个参数来传送。下面将开始具体工作。 2) 向新的菜单类添加自己的InitMenu( ) 给该类添加一个 I n i t M e n u ( )函数,同时给它传送一个指向应用程序的 C W n d类的指针和一 个用于封装( w r a p s )其菜单的C M e n u指针。 void CWzdMenu::InitMenu( CWnd *pWnd, CMenu *pMenu, UINT idb, C ToolBar *pToolBar ) { 如下所示,遍历所有在C m e n u指针中发现的菜单项: CDC *pDC = pWnd -> GetDC(); // for all submenus CMenu *pSubMenu = NULL; for ( int i = 0; i < ( int )pMenu -> GetMenuItemCount(); i++ ) { pSubMenu = pMenu -> GetSubMenu( i ); if ( pSubMenu ) { for ( int j = 0;j < ( int )pSubMenu -> GetMenuItemCount(); j++ ) { // if not a separator. . . UINT id = pSubMenu -> GetMenuItemID( j ); if ( id ) { // if already ownerdrawn, escape if ( pSubMenu -> GetMenuState( j,MF_BYPOSITION )&MF_OWNERDRAW ) { pWnd -> ReleaseDC( pDC ); r e t u r n ; } 注意这里忽略了分隔菜单项 (separator menu item),而且如果某个菜单项已经是自绘制的, 则由于该菜单已经进行了转换将被忽略。 不幸的是,一旦菜单项成为自绘制的,它将不会保留名字字符串 (name string)。与其保留 86第第第二部分第用户界面实例 下载下载一个存放菜单项名字的单个数组,还不如将这些名字保存在由菜单编辑器生成的菜单中。出 于这个目的,需要创建一个新的名为 M E N U I T E M的结构。在本步骤,创建一个 M E N U I T E M 的新实例,并将菜单项的名字字符串复制到其中: // fill in MENUITEM M E N U E N T RY *pEntry = new MENUENTRY; pEntry -> id = id; pSubMenu -> GetMenuString( j,pEntry -> str,MF_BYPOSITION ); 自绘制的菜单不再处理快捷键项目 (菜单中可以通过键盘访问的带有下划线的字母 )。因此 必须自己动手添加快捷键功能,为此必须保存在创建这个菜单之初利用菜单编辑器所选择的 字母。可以寻找“&”之后的字母,然后将该字符作为快捷键方式保存在 M E N U I T E M中: int k = pEntry -> str.Find( ‘&’ ); pEntry -> chr = 0; if ( k >= 0 ) pEntry -> chr = pEntry -> str[k+1]; pEntry -> chr& = ~0x20; // make upper case 以像素为单位计算绘制菜单项所需的尺寸,同时将计算结果保存在 M E N U I T E M中。以后 在处理W M _ M E A S U R E I T E M消息时,要用到这些值: pEntry -> size = pDC -> GetTextExtent( pEntry -> str, pEntry -> str.GetLength() ); pEntry -> size.cx += BUTTON_WIDTH + 2; pEntry-> size.cy = BUTTON_HEIGHT + 6; 这一步将该菜单命令和工具栏中与其相对应的命令匹配起来。为此需要查看工具栏中该 命令的I D号。如果找到,将它保存起来以便在需要绘制这个菜单项时使用: pEntry -> inx = -1; for ( int m = 0;m < pToolBar -> GetToolBarCtrl().GetButtonCount();m++ ) { int inx; UINT idx,x; p ToolBar -> GetButtonInfo( m,idx,x,inx ); if ( id = idx ) { pEntry -> inx = inx; b r e a k ; } 继续下一步,使该菜单项成为一个自绘制的菜单项: // modify menu item to be owner drawn pSubMenu -> ModifyMenu( id, MF_BYCOMMAND | MF_ENABLED | MF_OWNERDRAW, id, ( LPCTSTR )pEntry ); 同时还要装载工具栏的位图以便以后绘制图标时用。此时将使用在本系列书的前面书中 创建的C w z d B i t m a p类使图像的背景可以转换为菜单的现有颜色,这样就可以使得它们看起来 是透明的: m_bitmap.LoadBitmapEx(idb, TRUE); 系统为应用程序发送两个消息以允许绘制一个自绘制菜单。第一个消息是 W M _ 第 5 章第菜单、控件条和状态栏第第87下载下载M E A S U R E I T E M,该消息用来告诉系统菜单中的每个菜单项的尺寸大小,系统由此可以确定 整个菜单的大小。第二个消息 W M _ D R AW I T E M是为单个菜单项发送使得允许使用标准的 Wi n d o w s绘图工具来绘制菜单。 3) 为新菜单类增加M e a s u r e I t e m ( )函数 C l a s s w i z a r d还是不支持C M e n u,因此仍然需要为该类手工添加 W M _ M E A S U R E I T E M消息 处理函数。 该处理函数将返回以像素为单位的被请求菜单项的大小,系统需要该值以确定整个菜单的 宽度。由于前面已经用I n i t M e n u ( )函数计算出了这些像素值,因而可以使用这些数值如下: // size of our menu item void CWzdMenu::MeasureItem( LPMEASUREITEMSTRUCT lpMIS ) { M E N U E N T RY *pEntry = ( MENUENTRY * )lpMIS -> itemData; lpMIS -> itemWidth = pEntry -> size.cx; lpMIS -> itemHeight = pEntry -> size.cy; } 4 ) 为该新菜单类增加D r a w I t e m ( )函数 为新的菜单类手工添加W M _ D R AW I T E M消息处理函数: void CWzdMenu::DrawItem( LPDRAWITEMSTRUCT lpDIS ) { 封装从M F C类的D R AW I T E M S T R U C T结构所得到的信息以启动该消息处理函数,如下所 示: CDC dc; dc.Attach( lpDIS -> hDC ); // device context to draw to CRect rect( lpDIS -> rcItem ); // rectangular size of menu item 接下来绘制菜单项的背景。这里得到的设备环境已经绘制了标准的背景颜色。但是,如 果鼠标在该项上时,需要转换背景色和文本色: // if our item is selected, then set colors accordingly COLORREF bk = dc.GetBkColor(); COLORREF fg = dc.GetTe x t C o l o r ( ) ; if ( lpDIS -> itemState & ODS_SELECTED ) { bk = ::GetSysColor( COLOR_HIGHLIGHT ); fg = ::GetSysColor( COLOR_HIGHLIGHTTEXT ); } d c . S e t TextColor( fg ); // fill in background CBrush brush( bk ); dc.FillRect( &rect, &brush ); 如果将要绘制的菜单项设置为禁用,那么需要将该菜单项的文本和相关联的图标变灰。 这就要用到C D C : : D r a w S t a t e ( )函数,该函数与一个处理实际绘图工作的回调函数一起工作,因 此这里D r a w I t e m ( )所要做的就是设置D r a w S t a t e ( )函数以调用该回调函数: // get enabled/disabled state and draw appropriately UINT nState = DSS_NORMAL; 88第第第二部分第用户界面实例 下载if ( lpDIS -> itemState & ODS_DISABLED ) { nState = DSS_DISABLED; } dc.DrawState( rect.TopLeft(), rect.Size(), DrawStateProc, ( LPARAM )lpDIS, nState, ( HBRUSH )NULL ); 最后清除设备环境以完成D r a w I t e m ( )函数: // cleanup d c . S e t TextColor( fg ); dc.SetBkMode( nBkMode ); d c . D e t a c h ( ) ; 5) 为该新菜单类增加C D C : : D r a w S t a t e ( )回调函数 接下来创建C D C : : D r a w S t a t e ( )所需要的回调函数并再次为其封装设备环境句柄和矩形来启 动该函数: BOOL CALLBACK DrawStateProc( HDC hdc, LPARAM lData, WPARAM wData, int cx, int cy ) { CDC dc; L P D R AWITEMSTRUCT lpDIS = ( LPDRAWITEMSTRUCT )lData; dc.Attach( hdc ); M E N U E N T RY *pEntry = ( MENUENTRY * )lpDIS -> itemData; CRect rect( 0, 0, cx, cy ); 如果在以上的I n i t M e n u ( )中发现了一个匹配的工具栏,这一步将在菜单项 C D C : : B i t B l t函数 中绘制与之相关联的位图: if ( pEntry -> inx != -1 ) { CDC memDC; memDC.CreateCompatibleDC( &dc ); memDC.SelectObject( pEntry -> pBitmap); dc.BitBlt( rect.left, rect.top, BUTTON_WIDTH, BUTTO N _ H E I G H T, &memDC, pEntry -> inx*BUTTON_WIDTH, 0, SRCCOPY ); m e m D C . D e l e t e D C ( ) ; } 下一步查看该菜单项是否被设置为复选,如果是,则绘制一个特定的复选标记,该复选 标记是在图标编辑器(Icon Editor)中创建的位图。如果在最后一步没有绘制位图,该复选标记 将单独存在: HICON hIcon; if ( lpDIS -> itemState & ODS_CHECKED && ( hIcon = AfxGetApp() -> LoadIcon( IDI_CHECK_ICON ) ) ) { dc.DrawIcon( rect.left, rect.top, hIcon ); } 下面用D r a w Te x t ( ) >为该菜单项绘制文本字符串,此时使用 D r a w Te x t ( )函数使“&”之后的 字符产生下划线表示该字符标识的按键作为快捷键。 D r a w Te x t ( )还将对齐文本并扩展标签: rect.left += BUTTON_WIDTH + 2; 第 5 章第菜单、控件条和状态栏第第89下载d c . D r a w Text( pEntry -> str, &rect, D T _ L E F T | D T _ E X PA N D TABS|DT_VCENTER ); d c . D e t a c h ( ) ; return TRUE; } 6) 为新菜单类增加M e n u C h a r ( )函数 正如以上提到的,自绘制菜单不再处理快捷键命令 (菜单项中的下划线字符)。为了手工处 理该命令,需要为新菜单类增加 W M _ M E N U C H A R消息处理函数。此时使用前面在 I n i t M e n u ( ) 中保存的快捷键信息来确定所应当触发的菜单项: L R E S U LT CWzdMenu::MenuChar( UINT nChar ) { nChar& = ~0x20; // make uppercase // try to find char in current menu list for ( POSITION pos = m_CurrentMenuList.GetHeadPosition();pos; ) { M E N U E N T RY *pEntry = ( MENUENTRY * ) m_CurrentMenuList.GetNext( pos ); if ( pEntry -> chr = nChar ) { AfxGetMainWnd() -> SendMessage( WM_COMMAND,pEntry -> id ); return( MAKELONG( 0,1 ) ); } } r e t u r n ( 0 ) ; } 为了查看该菜单类的完整程序可以参考程序清单— 菜单类。 为了在应用程序中添加新菜单类,首先将其嵌入主框架类,然后将为该主框架类添加消 息处理函数以调用该菜单类的成员函数。 7) 将新菜单类合并到应用程序 C M a i n F r a m e类内部嵌入的新菜单类如下所示: // add CWzdMenu to Mainfrm.h p r i v a t e : CWzdMenu m_menu; 这一步要将C W z d M e n u类连接到主菜单。用 C l a s s Wi z a r d添加5个消息处理函数(可以再次 使用 C l a s s Wi z a r d )。所对应的窗口消息是: W M _ I N I T M E N U、W M _ I N I T P O P M E N U、 W M _ M E A S U R E I T E M、W M _ D R AW I T E M和W M _ M E N U C H A R。 在每个增加到 C M a i n F r a m e的消息处理函数内调用对应的新菜单类的成员函数,如下所 示: void CMainFrame::OnInitMenu( CMenu* pMenu ) { CMDIFrameWnd::OnInitMenu( pMenu ); m_menu.InitMenu( this,pMenu,IDR_MAINFRAME,&m_wndToolBar ); 90第第第二部分第用户界面实例 下载} void CMainFrame::OnInitMenuPopup( CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu ) { CMDIFrameWnd::OnInitMenuPopup( pPopupMenu, nIndex, bSysMenu ); m _ m e n u . I n i t M e n u P o p u p ( ) ; } void CMainFrame::OnMeasureItem( int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct ) { if ( !nIDCtl ) m_menu.MeasureItem( lpMeasureItemStruct ); CMDIFrameWnd::OnMeasureItem( nIDCtl, lpMeasureItemStruct ); } void CMainFrame::OnDrawItem( int nIDCtl, L P D R AWITEMSTRUCT lpDrawItemStruct ) { if ( !nIDCtl ) m_menu.DrawItem( lpDrawItemStruct ); CMDIFrameWnd::OnDrawItem( nIDCtl, lpDrawItemStruct ); } L R E S U LT CMainFrame::OnMenuChar( UINT nChar, UINT nFlags, CMenu* pMenu ) { return m_menu.MenuChar( nChar ); } 4. 注意 由于本例只是一个实例,所以省去了一些精华内容。菜单项中的加速键描述应该以右对 齐方式绘制。当鼠标停留在该菜单项之上时,图标将使用焦点窗口 (一个细的边框)绘制来进行 修改。 禁用( D i s a b l e d )菜单项是用C D C : : D r a w S t a t e ( )以一种浮雕灰色绘制的。位图也显示浮雕效 果,但是在某些情况下,它们的整个背景色将被转换,从而使得它们所具有的位图都显得模 糊。如果不希望出现这种效果,可以通过使整个位图的背景色设置为白色来进行纠正。为此 需要第二次装载工具栏位图并将背景色转换成白色。这就需要修改本实例中 C W z d B i t m a p类。 5. 使用光盘时注意 执行随书附带光盘上的工程时,可以注意到任何在工具栏中具有相对应命令的菜单项, 都具有一个相邻该工具栏的图标。 6. 程序清单— 菜单类 #if !defined( AFX_WZDMENU_H__18606914_D521_11D1_9B69_00AA003D8695__INCLUDED_ ) #define AFX_WZDMENU_H__18606914_D521_11 D 1 _ 9 B 6 9 _ 0 0 A A 0 0 3 D 8 6 9 5 _ _ I N C L U D E D _ #if _MSC_VER >= 1000 第 5 章第菜单、控件条和状态栏第第91下载#pragma once #endif // _MSC_VER >= 1000 // WzdMenu.h : header file / / #include "afxtempl.h" struct MENUENTRY { CString str; UINT id; UINT chr; CSize size; int inx; } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdMenu window class CWzdMenu : public CMenu { // Construction p u b l i c : C W z d M e n u ( ) ; virtual ~CWzdMenu(); // Attributes p u b l i c : // Operations p u b l i c : void InitMenu( CWnd *pWnd, CMenu *pMenu, UINT idb, CToolBar *pToolBar ); void InitMenuPopup(); void MeasureItem( LPMEASUREITEMSTRUCT lpMIS ); void DrawItem( LPDRAWITEMSTRUCT lpDIS ); L R E S U LT MenuChar( UINT nChar ); // Implementation p u b l i c : p r i v a t e : CBitmap m_bitmap; C L i s t < M E N U E N T RY * , M E N U E N T RY*> m_FullMenuList; C L i s t < M E N U E N T RY * , M E N U E N T RY*> m_CurrentMenuList; } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Developer Studio will insert additional declarations immediately // before the previous line. 92第第第二部分第用户界面实例 下载# e n d i f // !defined( AFX_WZDMENU_H__18606914_D521_11D1_9B69_00AA003D8695__INCLUDED_ ) // WzdMenu.cpp : implementation file / / #include “stdafx.h” #include “wzd.h” #include “WzdMenu.h” #include “WzdProject.h” #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f BOOL CALLBACK DrawStateProc( HDC hdc, LPARAM lData, WPARAM wData, int cx, int cy ); / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdMenu C W z d M e n u : : C W z d M e n u ( ) { } C W z d M e n u : : ~ C W z d M e n u ( ) { while ( !m_FullMenuList.IsEmpty() ) { delete m_FullMenuList.RemoveHead(); } } void CWzdMenu::InitMenu( CWnd *pWnd, CMenu *pMenu, UINT idb, CToolBar *pToolBar ) { CDC *pDC = pWnd -> GetDC(); // for all submenus CMenu *pSubMenu = NULL; for (int i = 0; i < ( int )pMenu -> GetMenuItemCount(); i++ ) { pSubMenu = pMenu -> GetSubMenu( i ); if ( pSubMenu ) { for ( int j = 0;j < ( int )pSubMenu -> GetMenuItemCount(); j++ ) { // if not a separator. . . UINT id = pSubMenu -> GetMenuItemID( j ); if ( id ) { // if already ownerdrawn, escape 第 5 章第菜单、控件条和状态栏第第93下载if ( pSubMenu -> GetMenuState( j,MF_BYPOSITION )&MF_OWNERDRAW ) { pWnd -> ReleaseDC( pDC ); r e t u r n ; } // fill in MENUITEM M E N U E N T RY *pEntry = new MENUENTRY; pEntry -> id = id; pSubMenu -> GetMenuString( j,pEntry -> str,MF_BYPOSITION ); int k = pEntry -> str.Find( ‘&’ ); pEntry -> chr = 0; if ( k >= 0 ) pEntry -> chr = pEntry -> str[k+1]; pEntry -> chr& = ~0x20; // make upper case pEntry -> size = pDC -> G e t TextExtent( pEntry -> str,pEntry -> str.GetLength() ); pEntry -> size.cx += BUTTON_WIDTH + 2; pEntry -> size.cy = BUTTON_HEIGHT + 2; pEntry -> inx = -1; for ( int m = 0;m < pToolBar -> G e t ToolBarCtrl().GetButtonCount();m++ ) { int inx; UINT idx,x; p ToolBar -> GetButtonInfo( m,idx,x,inx ); if ( id = idx ) { pEntry -> inx = inx; pEntry -> pBitmap = &m_bitmap; b r e a k ; } } // add MENUITEM to full list m _ F u l l M e n u L i s t . A d d Tail( pEntry ); // modify menu item to be owner drawn pSubMenu -> ModifyMenu( id, MF_BYCOMMAND | MF_ENABLED | M F _ O W N E R D R AW, id, ( LPCTSTR )pEntry ); } } } } m_bitmap.LoadBitmapEx( idb,TRUE ); pWnd -> ReleaseDC( pDC ); } void CWzdMenu::InitMenuPopup() { 94第第第二部分第用户界面实例 下载// empty current menu list m _ C u r r e n t M e n u L i s t . R e m o v e A l l ( ) ; } // size of our menu item void CWzdMenu::MeasureItem( LPMEASUREITEMSTRUCT lpMIS ) { M E N U E N T RY *pEntry = ( MENUENTRY * )lpMIS -> itemData; lpMIS -> itemWidth = pEntry -> size.cx; lpMIS -> itemHeight = pEntry -> size.cy; } // draw our menu item void CWzdMenu::DrawItem( LPDRAWITEMSTRUCT lpDIS ) { // get our device context and rectangle to draw to CDC dc; dc.Attach( lpDIS -> hDC ); CRect rect( lpDIS -> rcItem ); // if our item is selected, then set colors accordingly COLORREF bk = dc.GetBkColor(); COLORREF fg = dc.GetTe x t C o l o r ( ) ; if ( lpDIS -> itemState & ODS_SELECTED ) { bk = ::GetSysColor( COLOR_HIGHLIGHT ); fg = ::GetSysColor( COLOR_HIGHLIGHTTEXT ); } d c . S e t TextColor( fg ); // fill in background CBrush brush( bk ); dc.FillRect( &rect, &brush ); // draw text withhout a background int nBkMode = dc.SetBkMode( TRANSPARENT ); // get enabled/disabled state and draw appropriately UINT nState = DSS_NORMAL; if ( lpDIS -> itemState & ODS_DISABLED ) { nState = DSS_DISABLED; } dc.DrawState( rect.To p L e f t ( ) , r e c t . S i z e ( ) , D r a w S t a t e P r o c , ( LPARAM )lpDIS,nState,( HBRUSH )NULL ); // add to current menu list M E N U E N T RY *pEntry = ( MENUENTRY * )lpDIS -> itemData; m _ C u r r e n t M e n u L i s t . A d d Tail( pEntry ); // cleanup 第 5 章第菜单、控件条和状态栏第第95下载d c . S e t TextColor( fg ); dc.SetBkMode( nBkMode ); d c . D e t a c h ( ) ; } BOOL CALLBACK DrawStateProc( HDC hdc, LPARAM lData, WPARAM wData, int cx, int cy ) { CDC dc; L P D R AWITEMSTRUCT lpDIS = ( LPDRAWITEMSTRUCT )lData; dc.Attach( hdc ); M E N U E N T RY *pEntry = ( MENUENTRY * )lpDIS -> itemData; CRect rect( 0,0,cx,cy ); // draw bitmap, if any if ( pEntry -> inx != -1 ) { CDC memDC; memDC.CreateCompatibleDC( &dc ); memDC.SelectObject( pEntry -> pBitmap ); dc.BitBlt( rect.left, rect.top, BUTTO N _ W I D T H , B U T TO N _ H E I G H T, &memDC, pEntry -> inx*BUTTON_WIDTH, 0, SRCCOPY ); m e m D C . D e l e t e D C ( ) ; } // check it, if required HICON hIcon; if ( lpDIS -> itemState & ODS_CHECKED && ( hIcon = AfxGetApp() -> LoadIcon( IDI_CHECK_ICON ) ) ) { dc.DrawIcon( rect.left, rect.top, hIcon ); } // draw text rect.left += BUTTON_WIDTH + 2; d c . D r a w Text( pEntry -> str, &rect, DT_LEFT|DT_EXPA N D TABS|DT_VCENTER ); d c . D e t a c h ( ) ; return TRUE; } // draw our menu item L R E S U LT CWzdMenu::MenuChar( UINT nChar ) { nChar& = ~0x20; // make uppercase // try to find char in current menu list for ( POSITION pos = m_CurrentMenuList.GetHeadPosition();pos; ) { M E N U E N T RY *pEntry = ( MENUENTRY * )m_CurrentMenuList.GetNext( pos ); if ( pEntry -> chr = nChar ) { AfxGetMainWnd() -> SendMessage( WM_COMMAND,pEntry -> id ); return( MAKELONG( 0,1 ) ); 96第第第二部分第用户界面实例 下载} } return( 0 ); } 5.2 实例8:调整命令条外观 1. 目标 使工具栏具有与Developer Studio中的工具栏同样的外观,如图 5 - 2所示。 2. 策略 使用T B S T Y L E _ F L AT工具栏风 格使工具栏具备扁平按钮外观。在 处理发送到工具栏的 W M _ PA I N T消 息时,自己绘制移动条 ( g r a b b e r )。 这里将用C l a s s Wi z a r d把所有的功能 封装进工具栏类的内部。 注意 最新的VC++6.0版本已经 增加了一个工具栏风格,该风 格自动地在工具栏上绘制提手条(gripper bar)。该风格为C B R S _ G R I P P E R,但是它仅仅 绘制单提手条而不是如本例和Developer Studio所示的那样绘制双提手条。 3. 步骤 (1) 创建新的工具栏类 用C l a s s Wizard 创建一个从 C To o l B a r C t r l派生的新工具栏类,然后用文本编辑器用 C To o l B a r取代C To o l B a r C t r l,因为C l a s s Wi z a r d当前还不能从C To o l B a r派生新类。 用C l a s s Wi z a r d为新的工具栏类增加W M _ PAINT 消息处理函数。在工具栏处于水平时在工 具栏的前面绘制提手条,或者,在工具栏垂直停靠于应用程序的主窗口时在其顶部绘制提手 条。在工具栏浮动的时候则不绘制提手条。首先允许工具栏控件按通常方式绘制其按钮和边 界: void CWzdTo o l B a r : : O n P a i n t ( ) { C To o l B a r : : O n P a i n t ( ) ; 接下来,如果工具栏浮动则返回: // if floating, no grabber bar if ( IsFloating() ) r e t u r n ; 创建一个窗口设备环境而不是客户设备环境,这是因为没有非客户区包围工具栏: // to draw to whole window! C WindowDC dc( this ); 提手条将使用系统定义的颜色绘制。如果用户在控制面板内更改了系统的颜色方案,那 么此时使用控件条编号就不会起作用。有两种系统颜色可用于 3 D提手条:高亮度颜色和阴影 颜色,用两种颜色创建两种画笔: 第 5 章第菜单、控件条和状态栏第第97下载 图5-2 命令条外观 提手条 扁平按钮CPen penHL( PS_SOLID, 2, ::GetSysColor( COLOR_3DHIGHLIGHT ) ); CPen penSH( PS_SOLID, 3, ::GetSysColor( COLOR_3DSHADOW ) ); 现在根据工具栏的对齐方向,在工具栏的开始处或顶部绘制两条直线: CPen *pPen = dc.SelectObject( &penSH ); if ( GetBarStyle()&CBRS_ORIENT_HORZ ) { // draw shadow lines r e c t . O ffsetRect( 4,4 ); d c . M o v e To( rect.left,rect.top ); d c . L i n e To( rect.left,rect.bottom ); d c . M o v e To( rect.left + 4,rect.top ); d c . L i n e To( rect.left + 4,rect.bottom ); // draw highlight lines dc.SelectObject( &penHL ); d c . M o v e To( rect.left,rect.top ); d c . L i n e To( rect.left,rect.bottom - 1 ); d c . M o v e To( rect.left + 4,rect.top ); d c . L i n e To( rect.left + 4,rect.bottom - 1 ); } e l s e { // draw shadow lines r e c t . O ffsetRect( 4,2 ); d c . M o v e To( rect.left,rect.top ); d c . L i n e To( rect.right,rect.top ); d c . M o v e To( rect.left,rect.top + 4 ); d c . L i n e To( rect.right,rect.top + 4 ); // draw highlight lines dc.SelectObject( &penHL ); d c . M o v e To( rect.left,rect.top ); d c . L i n e To( rect.right - 1,rect.top ); d c . M o v e To( rect.left,rect.top + 4 ); d c . L i n e To( rect.right - 1,rect.top + 4 ); } d c . S e l e c t O b j e c t ( p P e n ) ; 查看新的工具栏类完整代码列表,请参考本实例结尾的程序清单— 工具栏类。 2) 实现新工具栏类 在CMainFrame 中用新的工具栏类替代原来的工具栏类。可以在如下的 M a i n F r m . h文件中 完成这项工作。在本实例中将新工具栏命名为: C W z d ToolBar m_wndTo o l B a r ; 在M a i n F r m . c p p文件中为工具栏应用 T B S T Y L E _ F L AT风格,在创建该工具栏之后,按如 下方式使用C To o l B a r : : M o d i f y S t y l e ( )函数: if ( !m_wndTo o l B a r.Create( this ) || ! m _ w n d To o l B a r. L o a d ToolBar( IDR_MAINFRAME ) ) { 98第第第二部分第用户界面实例 下载TRACE0( "Failed to create toolbar\n" ); return -1; // fail to create } m _ w n d To o l B a r.ModifyStyle( 0, TBSTYLE_FLAT ); 4. 注意 ■ 对工具栏使用平坦按钮风格的时候,很难确定工具栏在哪里结束以及按钮从哪里开始。 在拖动工具栏的时候就更困难了。这也就是采用提手条的原因。 ■ 伸缩条控件像命令条但实际上根本就不是工具栏。伸缩条是一种控件窗口,它可以包 含一个或多个其他的控件窗口,包括工具栏等等。它允许用户在它的控制范围之内移动这些 控件窗口。 5. 使用光盘时注意 执行随书附带光盘上的工程时,可以注意到工具栏看起来像 Developer Studio中的工具栏, 包括扁平按钮和提手条。 6. 程序清单— 工具栏类 #if !defined( AFX_WZDTO O L B A R _ H _ _ 0 9 3 9 E 9 11 _ E 0 E C _ 11D1_9B7A_00AA003D8695__INCLUDED_ ) #define AFX_WZDTO O L B A R _ H _ _ 0 9 3 9 E 9 11 _ E 0 E C _ 11 D 1 _ 9 B 7 A _ 0 0 A A 0 0 3 D 8 6 9 5 _ _ I N C L U D E D _ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 // WzdTo o l B a r.h : header file / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdToolBar window class CWzdToolBar : public CTo o l B a r { // Construction p u b l i c : C W z d To o l B a r ( ) ; // Attributes p u b l i c : // Operations p u b l i c : // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CWzdToolBar ) // }}AFX_VIRT U A L // Implementation p u b l i c : virtual ~CWzdTo o l B a r ( ) ; // Generated message map functions 第 5 章第菜单、控件条和状态栏第第99下载p r o t e c t e d : // {{AFX_MSG( CWzdToolBar ) afx_msg void OnPaint(); // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Developer Studio will insert additional declarations immediately // before the previous line. # e n d i f // !defined( AFX_WZDTO O L B A R _ H _ _ 0 9 3 9 E 9 11 _ E 0 E C _ 11D1_9B7A_00AA003D8695__INCLUDED_ ) // WzdTo o l B a r.cpp : implementation file / / #include "stdafx.h" #include "wzd.h" #include "WzdTo o l B a r. h " #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTo o l B a r C W z d To o l B a r : : C W z d To o l B a r ( ) { } C W z d To o l B a r : : ~ C W z d To o l B a r ( ) { } BEGIN_MESSAGE_MAP( CWzdTo o l B a r, CToolBar ) // {{AFX_MSG_MAP( CWzdToolBar ) O N _ W M _ PA I N T ( ) // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdToolBar message handlers void CWzdTo o l B a r : : O n P a i n t ( ) 100第第第二部分第用户界面实例 下载{ C To o l B a r : : O n P a i n t ( ) ; // if floating, no grabber bar if ( IsFloating() ) r e t u r n ; // draw to whole window! C WindowDC dc( this ); // draw horizontal or vertical grabber bar CRect rect; GetClientRect( &rect ); CPen penHL( PS_SOLID,2,::GetSysColor( COLOR_3DHIGHLIGHT ) ); CPen penSH( PS_SOLID,3,::GetSysColor( COLOR_3DSHADOW ) ); CPen *pPen = dc.SelectObject( &penSH ); if ( GetBarStyle()&CBRS_ORIENT_HORZ ) { // draw shadow lines r e c t . O ffsetRect( 4,4 ); d c . M o v e To( rect.left,rect.top ); d c . L i n e To( rect.left,rect.bottom ); d c . M o v e To( rect.left + 4,rect.top ); d c . L i n e To( rect.left + 4,rect.bottom ); // draw highlight lines dc.SelectObject( &penHL ); d c . M o v e To( rect.left,rect.top ); d c . L i n e To( rect.left,rect.bottom - 1 ); d c . M o v e To( rect.left + 4,rect.top ); d c . L i n e To( rect.left + 4,rect.bottom - 1 ); } e l s e { // draw shadow lines r e c t . O ffsetRect( 4,2 ); d c . M o v e To( rect.left,rect.top ); d c . L i n e To( rect.right,rect.top ); d c . M o v e To( rect.left,rect.top + 4 ); d c . L i n e To( rect.right,rect.top + 4 ); // draw highlight lines dc.SelectObject( &penHL ); d c . M o v e To( rect.left,rect.top ); d c . L i n e To( rect.right - 1,rect.top ); d c . M o v e To( rect.left,rect.top + 4 ); d c . L i n e To( rect.right - 1,rect.top + 4 ); } dc.SelectObject( pPen ); } 第 5 章第菜单、控件条和状态栏第第101下载5.3 实例9:可编程工具栏 1. 目标 允许用户从可选工具栏按钮列表中创建自己的工具栏,如图 5 - 3所示。 2. 策略 首先要为应用程序的首选项创建新的属 性表单以配置工具栏。当某属性页被选中的 时候,它将捕获鼠标光标,使得单击屏幕上 任何一处都将向该页发送鼠标单击消息。这 就允许确定用户是否在该属性页以外单击了 工 具 栏 按 钮 , 知 道 这 一 点 后 就 可 以 用 C To o l B a r的普通功能来修改工具栏。属性页 中的工具栏按钮实际上只是一个位图图像列表,它们代表工具栏按钮。此外还要为主框架类 增加装载和保存修改过的工具栏到系统注册表的功能,以便应用程序下一次运行时可以重新 装载到应用程序中。该实例可能是本书中最复杂的实例。 3. 步骤 1) 创建工具栏属性页 用Dialog Editor创建一个对话框模板并赋予 C o n t r o l风格,为其加入一个简单分组框。 用C l a s s Wi z a r d从这个派生于C P r o p e r t y P a g e的对话框创建一个工具栏属性页。 如下所示,在该属性页中嵌入一个工具栏类和位图类: C ToolBar m_To o l B a r ; CBitmap m_Bitmap; 用C l a s s Wi z a r d为该类增加W M _ I N I T D I A L O G消息处理函数。这里将装载应用程序的工具 栏和工具栏位图。在本实例中将只使用缺省工具栏。如果应用程序中还有其他更多的工具栏, 则只需将其装载即可: BOOL CTo o l B a r P a g e : : O n I n i t D i a l o g ( ) { C P r o p e r t y P a g e : : O n I n i t D i a l o g ( ) ; // create and load breeder toolbar m _ To o l B a r.Create( this ); m _ To o l B a r. L o a d ToolBar( IDR_MAINFRAME ); // load breeder toolbar bitmap m_bitmap.LoadBitmap( IDR_MAINFRAME ); return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FA L S E } 这里装载的工具栏实际上对用户并不可见,只是使用它们作为该属性页可以创建什么按 钮的参考。 102第第第二部分第用户界面实例 下载 图5-3 根据按钮选择创建工具栏 工具条按钮被拖入或 拖出工具条,同时新 的工具条被创建用C l a s s Wi z a r d为该属性页增加W M _ PA I N T消息处理函数。这里将绘制可用按钮的图像以 创建分组框内的其他工具栏,同时还将使用一些额外处理过程来创建这些按钮图像之间的空 间: void CTo o l B a r P a g e : : O n P a i n t ( ) { CPaintDC dc( this ); // device context for painting // display breeder toolbar bitmap CDC memDC; memDC.CreateCompatibleDC( &dc ); memDC.SelectObject( &m_bitmap ); int x = m_xBitmapStart; int y = m_yBitmapStart; for ( int i = 0; i < m_nButtonCount; i++ ) { dc.BitBlt( x,y, B U T TO N _ W I D T H , B U T TO N _ H E I G H T, &memDC, i * B U T TON_WIDTH, 0, SRCCOPY ); x += BUTTON_WIDTH + BUTTO N _ X S PA C I N G ; } m e m D C . D e l e t e D C ( ) ; } 2) 为该属性页增加W M _ S E TA C T I V E消息处理函数 当属性页打开时,需要获取对鼠标光标的控制权。如果用户在该页以外单击了工具栏按 钮,则需要截获该消息以便移动或者删除该按钮。如果没有获取对光标的控制权,则单击工 具栏按钮对属性页没有任何效果。使用 C l a s s Wi z a r d为该属性页类增加 W M _ S E TA C T I V E消息 处理函数。W M _ S E TA C T I V E消息表明该页被打开,然后用 : : S e t C a p t u r e ( )函数来获取光标控 制权: BOOL CTo o l B a r P a g e : : O n S e t A c t i v e ( ) { S e t C a p t u r e ( ) ; return CPropertyPage::OnSetActive(); } 3 ) 为该属性页增加W M _ K I L L A C T I V E消息处理函数 该页在关闭后,需要释放对鼠标光标的控制权,可以用 C l a s s Wi z a r d 增加 W M _ K I L L A C T I V E消息处理函数以释放光标: BOOL CTo o l B a r P a g e : : O n K i l l A c t i v e ( ) { R e l e a s e C a p t u r e ( ) ; return CPropertyPage::OnKillActive(); } 到目前为止已经有了一个属性页显示工具栏按钮图像,而且在将其打开后可以随时了解 用户在屏幕上的单击鼠标行为。这一步需要处理用于表示用户已经在屏幕上进行了单击操作 的消息。它可能表明用户将从现有工具栏拖动按钮或者从属性页拖动图像,或者还可能单击 了包含该属性页的属性表单上的另一个标签,或者根本就没有单击任何东西。 第 5 章第菜单、控件条和状态栏第第103下载4) 为属性页增加鼠标按下消息处理函数 用C l a s s Wi z a r d为该类增加W M _ L B U T TO N D O W N消息处理函数,用于处理用户在屏幕上 单击时的表示鼠标已按下的消息: void CToolBarPage::OnLButtonDown( UINT nFlags, CPoint point ) { 用C W n d : : Wi n d o w F r o m P o i n t ( )函数来确定用户单击了什么窗口: m_bMoving = FA L S E ; C l i e n t ToScreen( &point ); CWnd *pWnd = WindowFromPoint( point ); 如果用户单击了属性表单或者另一属性表单的标签,就将该消息转发给相应窗口: // if point is parent or child of this property page if ( pWnd != this && ( pWnd = GetParent() || GetParent() -> IsChild( pWnd ) ) ) { CPoint pt( point ); pWnd -> ScreenToClient( &pt ); if ( pt.y >= 0 ) { // if clicked on a cliet area, just send pWnd -> SendMessage( WM_LBUTTONDOWN,nFlags, MAKELONG( pt.x,pt.y ) ); } * e l s e { // if clicked on a non-client area, we must // perform a hit test before sending the click UINT ht = pWnd -> SendMessage( WM_NCHITTEST,0, MAKELONG( point.x,point.y ) ); pWnd -> SendMessage( WM_NCLBUTTO N D O W N , h t , MAKELONG( point.x,point.y ) ); } r e t u r n ; } 如果用户单击了现有工具栏上的按钮,就将进入 move button模式。这时通过查看用户是 否单击了与应用程序所维护的某一个工具栏相匹配的窗口,来确定用户是否单击了该工具栏 按钮。move button模式只意味着需要确定并保存用户单击的按钮。同时还要修改停留在按钮 表面的鼠标光标外形: // see if this is a toolbar button if ( m_pToolBar = GetToolBar( point ) ) { m_nButtonMoved = GetButtonIndex( m_pTo o l B a r, point ); m _ p ToolBar -> G e t ToolBarCtrl().GetButton( m_nButtonMoved,&m_tbbutton ); 104第第第二部分第用户界面实例 下载m_bMoving = TRUE; ::SetCursor( m_hMoveCursor ); } 如果用户单击了属性页内的按钮图像之一,就必须确定单击了哪一个按钮,将其保存然 后再次进入move button模式; // else if this window else if ( pWnd == this ) { S c r e e n ToClient( &point ); p o i n t . O ffset( -m_xBitmapStart,-m_yBitmapStart ); CRect rect( 0,0,(BUTTO N _ W I D T H + B U T TO N _ X S PACING )* m _ n B u t t o n C o u n t , B U T TON_HEIGHT ); if ( rect.PtInRect( point ) ) { m_nButtonMoved = 0; int i = point.x/( BUTTO N _ W I D T H + B U T TO N _ X S PACING ); for ( int j = 0;j < m_nButtonCount;j++ ) { UINT k; int l; m _ To o l B a r.GetButtonInfo( j,k,k,l ); if ( l == i ) { m_nButtonMoved = j; b r e a k ; } } m _ To o l B a r. G e t To o l B a r C t r l ( ) . G e t B u t t o n ( m_nButtonMoved,&m_tbbutton ); m_bMoving = TRUE; ::SetCursor( m_hMoveCursor ); S e t C a p t u r e ( ) ; } 忽略其他的单击行为。参考本实例结尾的程序清单— 属性页类,可以查看该消息处理 函数代码的完整列表。 用户在什么地方释放了拖动的按钮,将决定应用程序接下来应采取的行为。如果用户在 另一个工具栏上释放了它,则应用程序将把该按钮添加到该工具栏。如果用户在另一属性页 上释放该按钮,则应该取消该操作,除非按钮是从已有工具栏上拖动,在这种情况下应用程 序应该删除该按钮。如果用户在空白区域释放了按钮,则应用程序应当创建新的浮动或者停 靠工具栏。 5) 为该属性页增加鼠标单击释放消息 用C l a s s Wi z a r d为该类增加W M _ L B U T TO N U P消息处理函数: void CToolBarPage::OnLButtonUp( UINT nFlags, CPoint point ) { 如果当前不是move button模式,将忽略该消息: i f ( m _ b M o v i n g ) 第 5 章第菜单、控件条和状态栏第第105下载{ 如果从现有工具栏的按钮启动 move button模式,则删除该按钮: // delete button from source toolbar if ( m_pToolBar ) { m _ p ToolBar-> GetToolBarCtrl().DeleteButton( m_nButtonMoved ); } 查看用户是否在属性页上释放该按钮。如果是这样则不管,但如果正在拖动已经被删除 的已有按钮或者正在属性页以外拖动图像,那么该移动操作应当取消: // if dropped anywhere but toolbar property page CRect rect; C l i e n t ToScreen (&point ); G e t WindowRect( &rect ); if ( !rect.PtInRect( point ) ) { 查看用户是否在已有工具栏上释放了按钮,如果这样,就将按钮添加到用户进行释放操 作的那个工具栏: // if dropped on existing toolbar, add button to it C ToolBar *pTo o l B a r ; if ( pToolBar = GetToolBar( point ) ) { int i = GetButtonIndex( pTo o l B a r,point ); p ToolBar -> GetToolBarCtrl().InsertButton( i,&m_tbbutton ); U p d a t e ToolBar( pToolBar ); } G e t To o l B a r ( )实际上是一个为该类创建的小型辅助函数。可以在程序清单— 属性页类中 找到它。 如果用户没有在已有工具栏上或者属性页上释放按钮,那么需要创建一个新的浮动或者 停靠工具栏并将该按钮插入其中: // else create a new toolbar and add our button to it e l s e { p ToolBar = new CTo o l B a r ; C L i s t < C To o l B a r * , C ToolBar*> *pList = ( ( CMainFrame* )AfxGetMainWnd() ) -> GetTo o l B a r L i s t ( ) ; pList -> AddTail( pToolBar ); p ToolBar -> Create( GetParentFrame(), W S _ C H I L D | W S _ V I S I B L E | C B R S _ TO P | C B R S _ TO O LTIPS ); SIZE sizeButton, sizeImage; sizeImage.cx = BUTTO N _ W I D T H ; sizeImage.cy = BUTTO N _ H E I G H T; sizeButton.cx = sizeImage.cx + BUTTO N _ X S PA C I N G ; sizeButton.cy = sizeImage.cy + BUTTO N _ Y S PA C I N G ; p ToolBar -> SetSizes( sizeButton, sizeImage ); p ToolBar -> EnableDocking( CBRS_ALIGN_ANY | CBRS_FLOAT _ M U LTI ); // add all possible tool button bitmaps 106第第第二部分第用户界面实例 下载p ToolBar -> G e t ToolBarCtrl().AddBitmap( m_nButtonCount,IDR_MAINFRAME ); // add new button to this new toolbar p ToolBar -> GetToolBarCtrl().InsertButton( 0,&m_tbbutton ); 这一步必须确定所创建的工具栏是否可以被停靠到框架窗口,还是只能作为浮动条。这 里用下面列出的辅助函数 G e t To o l B a r N e a r ( )来查看所有的被停靠控件条来了解工具栏是否成行 排列,如果成行,再查看工具栏是垂直对齐还是水平对齐。如果新工具栏不能停靠,则将其 浮动: UINT nMode; BOOL bHorz; if ( GetToolBarNear( point,nMode,bHorz ) ) { // dock toolbar centered on cursor CSize size = pToolBar -> CalcFixedLayout( FALSE,bHorz ); if ( bHorz ) point.x -= size.cx/2; e l s e point.y -= size.cy/2; CRect rectx( point,size ); GetParentFrame() -> DockControlBar( pTo o l B a r,nMode,&rectx ); p ToolBar -> Invalidate(); GetParentFrame() -> RecalcLayout(); } e l s e { GetParentFrame() -> FloatControlBar( pTo o l B a r, point, CBRS_ALIGN_TOP ); } 如果按钮来自现有的工具栏,则这一步查看该工具栏是否已经没有了按钮,如果没有按 钮剩余,则将其删除: // else update source toolbar if any else if ( m_pToolBar ) { U p d a t e ToolBar( m_pToolBar ); // if no buttons left on toolbar, delete it if ( m_pToolBar && m_pToolBar -> G e t ToolBarCtrl().GetButtonCount() == 0 ) { POSITION pos; C L i s t < C To o l B a r * , C ToolBar*> *pList = ( ( CMainFrame* )AfxGetMainWnd() ) -> GetTo o l B a r L i s t ( ) ; if ( pos = pList -> Find( m_pToolBar ) ) { pList -> RemoveAt( pos ); m _ p ToolBar -> m_bAutoDelete = TRUE; if ( !m_pToolBar -> IsFloating() ) { m _ p ToolBar -> SendMessage( WM_CLOSE ); 第 5 章第菜单、控件条和状态栏第第107下载} e l s e { CDockBar* pDockBar = m_pToolBar -> m_pDockBar; CMiniDockFrameWnd* pDockFrame = ( CMiniDockFrameWnd* )pDockBar -> GetParent(); // won’t close till page is gone so hide till then pDockFrame -> ShowWindow( SW_HIDE ); pDockFrame -> SendMessage( WM_CLOSE ); } } 6) 为应用程序的属性表选项添加工具栏属性页 如下所示,添加工具栏属性页到应用程序的首选项 ( p r e f e r e n c e )中: void CMainFrame::OnOptionsPreferences() { CPropertySheet sheet( _T( “Preferences” ),this ); m _ p ToolBarPage = new CTo o l B a r P a g e ; sheet.AddPage( m_pToolBarPage ); s h e e t . D o M o d a l ( ) ; delete m_pTo o l B a r P a g e ; } 要查看该属性页类的完整代码列表,可以参考本实例结尾的程序清单— 属性表类。 虽然用户现在可以创建自己的工具栏,但是如果不能将工具栏保存起来供以后使用则该 功能毫无意义。为此需要为主框架类增加两个新的成员函数。其中一个成员函数在应用程序 将要终止时保存工具栏配置,另一个函数则在应用程序首次创建的时候装载该配置。 7) 创建S a v e To o l b a r ( )函数 在C M a i n F r a m e中创建新函数S a v e To o l b a r s ( ),这里首先保存应用程序中的自定义工具栏数 目。如下使用C Wi n A p p的Wr i t e P r o f i l e I n t ( )函数来保存该值: void CMainFrame::SaveToolbars() { int nNumToolBars = m_To o l B a r L i s t . G e t C o u n t ( ) ; AfxGetApp() -> WriteProfileInt( TO O L B A R _ S U M M A RY _ K E Y, TO O L B A R _ N U M _ K E Y, n N u m ToolBars ); 如果没有任何工具栏需要保存,则立即返回: if ( nNumToolBars ) { 接下来遍历应用程序的每个工具栏并为其创建新的系统注册表键: int i = 0; int idc = IDC_CUSTO M _ TO O L B A R S ; for ( POSITION pos = m_ToolBarList.GetHeadPosition();pos;i++ ) { 108第第第二部分第用户界面实例 下载C ToolBar *pToolBar = m_ToolBarList.GetNext( pos ); // create key for this toolbar CString key; k e y.Format( TO O L B A R _ K E Y,i ); // give toolbar a unique, sequential ID for // SaveBarState/LoadBarState p ToolBar -> SetDlgCtrlID( idc++ ); 然后遍历工具栏上的每个按钮将其信息存入系统注册表: // write number of buttons in this toolbar int nButtonCount = pToolBar -> GetTo o l B a r C t r l ( ) . G e t B u t t o n C o u n t ( ) ; AfxGetApp() -> WriteProfileInt( key, N U M _ O F _ B U T TO N S _ K E Y, nButtonCount ); // write info on each button T B B U T TON *ptbbutton = new TBBUTTO N [ n B u t t o n C o u n t ] ; for ( int j = 0;j < nButtonCount; j++ ) { p ToolBar -> GetToolBarCtrl().GetButton( j,ptbbutton+j ); } AfxGetApp() -> WriteProfileBinary( key, B U T TO N _ I N F O _ K E Y, ( BYTE* )ptbbutton,nButtonCount*sizeof( TBBUTTON ) ); delete []ptbbutton; 用C l a s s Wi z a r d为主框架类增加 W M _ C L O S E 消息处理函数,并在处理函数中调用 S a v e To o l B a r s ( )函数。同时调用C M a i n F r a m e : : S a v e B a r S t a t e ( )函数来保存应用程序中每个工具栏 的当前位置和状态: void CMainFrame::OnClose() { // save all custom toolbars S a v e To o l b a r s ( ) ; // save state of all control bars SaveBarState( “Control Bar States” ); C M D I F r a m e W n d : : O n C l o s e ( ) ; } 参考本实例结尾的程序清单— 主框架类,可以查看S a v e To o l B a r s ( )的完整代码列表。 8) 为主框架类增加L o a d To o l b a r s ( )函数 在C M a i n F r a m e中增加新函数 L o a d To o l b a r s ( )。首先更新将要创建的工具栏的数目,如果 没有则返回空: int CMainFrame::LoadTo o l b a r s ( ) { int nNumToolBars = AfxGetApp() -> GetProfileInt( TO O L B A R _ S U M M A RY _ K E Y, TO O L B A R _ N U M _ K E Y,0 ); if ( nNumToolBars ) { 如果有工具栏要创建,则遍历并创建每个工具栏。由于以后将用 CMainframe :: LoadBarState ()来恢复所有控件条的状态,所以在此不必担心各工具栏的当前状态: 第 5 章第菜单、控件条和状态栏第第109下载int idc = IDC_CUSTO M _ TO O L B A R S ; for ( int i = 0;i < nNumToolBars;i++ ) { // create empty toolbar C ToolBar *pToolBar = new CTo o l B a r ; m _ To o l B a r L i s t . A d d Tail( pToolBar ); p ToolBar -> Create( this, WS_CHILD|WS_VISIBLE|CBRS_TO P | C B R S _ TO O LTIPS, idc++ ); // set button sizes SIZE sizeButton, sizeImage; sizeImage.cx = BUTTO N _ W I D T H ; sizeImage.cy = BUTTO N _ H E I G H T; sizeButton.cx = sizeImage.cx + BUTTO N _ X S PA C I N G ; sizeButton.cy = sizeImage.cy + BUTTO N _ Y S PA C I N G ; p ToolBar -> SetSizes( sizeButton, sizeImage ); p ToolBar -> EnableDocking( CBRS_ALIGN_ANY | CBRS_FLOAT _ M U LTI ); // add all possible tool button bitmaps to this empty toolbar // after first finding out how many buttons are in this bitmap BITMAP bm; CBitmap bitmap; bitmap.LoadBitmap( IDR_MAINFRAME ); bitmap.GetObject( sizeof(BITMAP), &bm ); p ToolBar -> GetTo o l B a r C t r l ( ) . A d d B i t m a p ( b m . b m Wi d t h / B U T TON_WIDTH,IDR_MAINFRAME ); // create key for this toolbar CString key; k e y.Format( TO O L B A R _ K E Y, i ); 根据保存在系统注册表中的配置,添加按钮到这些工具栏中: // get number of buttons in this toolbar int nNumButtons = AfxGetApp() -> G e t P r o f i l e I n t ( k e y, N U M _ O F _ B U T TO N S _ K E Y, 0 ) ; // get button info and insert buttons into created toolbar UINT k; T B B U T TON *ptbbutton =; A f x G e t A p p ( ) - > GetProfileBinary( key, B U T TO N _ I N F O _ K E Y, ( BYTE ** )&ptbbutton,&k ); for ( int j = 0;j < nNumButtons;j++ ) { p ToolBar -> GetTo o l B a r C t r l ( ) . I n s e r t B u t t o n ( j , p t b b u t t o n + j ) ; } delete []ptbbutton; } 9) 修改主框架类的O n C r e a t e ( )函数 当应用程序首次启动的时候,系统注册表内没有工具栏信息,因此 L o a d To o l b a r s ( )将不装 载工具栏而返回,应用程序将初始化为没有工具栏。因此,如果注册表内没有保存工具栏信 息,可以如下编写代码,使得可以从应用程序的资源中装载缺省的工具栏: 110第第第二部分第用户界面实例 下载// create and load custom toolbars EnableDocking( CBRS_ALIGN_ANY ); if ( !LoadToolbars() ) { // if none, load standard toolbar(s) C ToolBar *pToolBar = new CTo o l B a r ; if ( !pToolBar -> Create( this ) || ! p ToolBar -> LoadToolBar( IDR_MAINFRAME ) ) { TRACE0( “Failed to create toolbar\n” ); return -1; // fail to create } : : : 在O n C r e a t e ( )函数的结尾调用L o a d B a r S t a t e ( )来恢复每个工具栏的位置、大小和状态: // reload all bar states LoadBarState( “Control Bar States” ); return 0; 参考程序清单— 主框架类,可以查看主框架类的完整代码列表。 最后一条:当用户单击了浮动工具栏的关闭按钮时会发生什么情况?缺省的行为只是隐 藏该工具栏。但在本实例中将自动地删除该工具栏。一般地,可以创建自己的工具栏类并处 理W M _ C L O S E消息来销毁窗口。但是当工具栏正在浮动的时候,它实际上位于 Mini Dock F r a m e窗口上。因此需要创建自己的 C M i n i D o c k F r a m e W n d派生类并为其增加W M _ C L O S E消息 处理函数以销毁窗口。 10) 销毁浮动工具栏 用C l a s s Wi z a r d创建一个新的C M i n F r a m e W n d派生类,然后用C M i n i D o c k F r a m e W n d取代所 有的C M i n i F r a m e W n d。C M i n i D o c k F r a m e W n d没有文档化也不被C l a s s Wi z a r d所支持。 用C l a s s Wi z a r d为该类增加W M _ C L O S E消息处理函数,这里将销毁窗口而不是隐藏它: void CWzdMiniDockFrameWnd::OnClose() { D e s t r o y Wi n d o w ( ) ; C M i n i D o c k F r a m e W n d : : O n C l o s e ( ) ; } 为了迫使应用程序使用新类,需要在 C M a i n F r a m e : : O n C r e a t e ( )函数的E n a b l e D o c k ( )一行之 后增加下列代码行: m_pFloatingFrameClass = RUNTIME_CLASS(CWzdMiniDockFrameWnd); 此处不能只用自己的新类取代原来的类就算完事,这是因为 M F C用CObject:: Create- Object ()创建了该浮动微型框架。因此该方法属于非正式的方法,并没有列入 M F C文档。 当工具栏被销毁后,需要将其从应用程序包括的工具栏列表中删除。为此需使用 C l a s s Wi z a r d创建C To o l B a r C t r l的一个新工具栏类。用文本编辑器将 C To o l B a r C t r l用C To o l B a r来 取代。 用C l a s s Wi z a r d来为新工具栏类增加 W M _ D E S T R O Y消息处理函数。从中访问主框架类中 的工具栏列表以删除该工具栏: 第 5 章第菜单、控件条和状态栏第第111下载void CWzdTo o l B a r : : O n D e s t r o y ( ) { C To o l B a r : : O n D e s t r o y ( ) ; C L i s t < C To o l B a r * , C ToolBar*> *pList = ( ( CMainFrame* )AfxGetMainWnd() ) -> GetTo o l B a r L i s t ( ) ; POSITION pos = pList -> Find( this ); if ( pos ) { pList -> RemoveAt( pos ); } } 4. 注意 ■ 由于实例篇幅所限,介绍的内容省略了很多精华。举个例子,当用户在属性页中选中 一个工具栏位图“按钮”的时候,可以在页面上放一些描述性的消息以描述按钮。当鼠标设 置为move button模式的时候,可以在工具栏上绘制一条线指示按钮将插入的位置 (当前按钮被 插入到用户释放鼠标按钮的位置 )。和Developer Studio的可编程工具栏一样,还可以在本实例 中增加工具栏种类,一次仅显示某些特定的工具栏位图。 ■ 当用户单击了浮动工具栏的关闭按钮,本实例将从应用程序的工具栏列表中删除保存在 C M a i n F r a m e中的工具栏。用户也可以在应用程序的Vi e w菜单命令下维护工具栏的动态列表。 5. 使用光盘时注意 执行随书附带光盘上的工程时,单击 O p t i o n,然后单击P r e f e r e n c e s菜单命令来调用首选项 属性表单。为了从已有工具栏中删除一个按钮,可以将它拖到首选项表单。如果要将其添加回 到工具栏,则从首选项表单中拖动对应的图像到工具栏即可。为了创建新的工具栏,可从任何 位置拖动一个按钮到空白区域。若要删除工具栏则可以将其浮动,然后再单击其关闭按钮。 6. 程序清单——主框架类 // MainFrm.h : interface of the CMainFrame class / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #if !defined( AFX_MAINFRM_H__CA9038EA_B0DF_11D1_A18C_DCB3C85EBD34__INCLUDED_ ) #define AFX_MAINFRM_H__CA9038EA_B0DF_11 D 1 _ A 1 8 C _ D C B 3 C 8 5 E B D 3 4 _ _ I N C L U D E D _ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 #include "To o l B a r P a g e . h " #include class CMainFrame : public CMDIFrameWnd { DECLARE_DYNAMIC( CMainFrame ) p u b l i c : C M a i n F r a m e ( ) ; // Attributes 112第第第二部分第用户界面实例 下载p u b l i c : // Operations p u b l i c : void LoadTo o l B a r s ( ) ; void SaveTo o l B a r s ( ) ; C L i s t < C To o l B a r * , C ToolBar*> *GetToolBarList(){return &m_To o l B a r L i s t ; } ; // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CMainFrame ) virtual BOOL PreCreateWindow( CREATESTRUCT& cs ); // }}AFX_VIRT U A L // Implementation p u b l i c : virtual ~CMainFrame(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump( CDumpContext& dc ) const; # e n d i f protected: // control bar embedded members CStatusBar m_wndStatusBar; // Generated message map functions p r o t e c t e d : // {{AFX_MSG(CMainFrame) afx_msg int OnCreate( LPCREATESTRUCT lpCreateStruct ); afx_msg void OnClose(); afx_msg void OnOptionsPreferences(); // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : C ToolBarPage *m_pTo o l B a r P a g e ; int LoadTo o l b a r s ( ) ; void SaveTo o l b a r s ( ) ; C L i s t < C To o l B a r * , C ToolBar*> m_To o l B a r L i s t ; } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Developer Studio will insert additional declarations immediately // before the previous line. # e n d i f // !defined( AFX_MAINFRM_H__CA9038EA_B0DF_11D1_A18C_DCB3C85EBD34__INCLUDED_ ) // MainFrm.cpp : implementation of the CMainFrame class / / 第 5 章第菜单、控件条和状态栏第第113下载#include "stdafx.h" #include "Wzd.h" #include "WzdProject.h" #include "WzdTo o l B a r. h " #include "WzdMiniDockFrameWnd.h" #include "MainFrm.h" #include #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CMainFrame IMPLEMENT_DYNAMIC( CMainFrame, CMDIFrameWnd ) BEGIN_MESSAGE_MAP( CMainFrame, CMDIFrameWnd ) // {{AFX_MSG_MAP( CMainFrame ) O N _ W M _ C R E AT E ( ) O N _ W M _ C L O S E ( ) ON_COMMAND( ID_OPTIONS_PREFERENCES, OnOptionsPreferences ) O N _ W M _ PA L E T T E C H A N G E D ( ) O N _ W M _ Q U E RY N E W PA L E T T E ( ) // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) static UINT indicators[] = { I D _ S E PA R ATOR, // status line indicator I D _ I N D I C ATO R _ C A P S , I D _ I N D I C ATO R _ N U M , I D _ I N D I C ATO R _ S C R L , } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CMainFrame construction/destruction C M a i n F r a m e : : C M a i n F r a m e ( ) { } C M a i n F r a m e : : ~ C M a i n F r a m e ( ) { while ( !m_ToolBarList.IsEmpty() ) { 114第第第二部分第用户界面实例 下载delete m_To o l B a r L i s t . R e m o v e H e a d ( ) ; } } int CMainFrame::OnCreate( LPCREATESTRUCT lpCreateStruct ) { if ( CMDIFrameWnd::OnCreate( lpCreateStruct ) = -1 ) return -1; // create and load custom toolbars EnableDocking( CBRS_ALIGN_ANY ); m_pFloatingFrameClass = RUNTIME_CLASS( CWzdMiniDockFrameWnd ); if ( !LoadToolbars() ) { // if none, load standard toolbar(s) C ToolBar *pToolBar = ( CWzdToolBar* )new CWzdTo o l B a r ; if ( !pToolBar -> Create( this ) || ! p ToolBar -> LoadToolBar( IDR_MAINFRAME ) ) { TRACE0( “Failed to create toolbar\n” ); return -1; // fail to create } m _ To o l B a r L i s t . A d d Tail( pToolBar ); // TODO: Remove this if you don’t want tool tips or a resizeable toolbar p ToolBar -> SetBarStyle( pToolBar -> GetBarStyle() | C B R S _ TO O LTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC ); // TODO: Delete these three lines if you don’t want the toolbar to // be dockable p ToolBar -> EnableDocking( CBRS_ALIGN_ANY ); DockControlBar( pToolBar ); } if ( !m_wndStatusBar.Create( this ) || ! m _ w n d S t a t u s B a r.SetIndicators( indicators, sizeof( indicators )/sizeof( UINT ) ) ) { TRACE0( “Failed to create status bar\n” ); return -1; // fail to create } // reload all bar states LoadBarState( “Control Bar States” ); return 0; } BOOL CMainFrame::PreCreateWindow( CREATESTRUCT& cs ) { // TODO: Modify the Window class or styles here by modifying // the CREATESTRUCT cs 第 5 章第菜单、控件条和状态栏第第115下载return CMDIFrameWnd::PreCreateWindow( cs ); } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CMainFrame diagnostics #ifdef _DEBUG void CMainFrame::AssertValid() const { C M D I F r a m e W n d : : A s s e r t Va l i d ( ) ; } void CMainFrame::Dump( CDumpContext& dc ) const { MDIFrameWnd::Dump( dc ); } #endif //_DEBUG / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CMainFrame message handlers void CMainFrame::OnClose() { // save all custom toolbars S a v e To o l b a r s ( ) ; // save state of all control bars SaveBarState( “Control Bar States” ); C M D I F r a m e W n d : : O n C l o s e ( ) ; } void CMainFrame::OnOptionsPreferences() { CPropertySheet sheet( _T( "Preferences" ),this ); m _ p ToolBarPage = new CTo o l B a r P a g e ; sheet.AddPage( m_pToolBarPage ); s h e e t . D o M o d a l ( ) ; delete m_pTo o l B a r P a g e ; } int CMainFrame::LoadTo o l b a r s ( ) { int nNumToolBars = AfxGetApp() -> GetProfileInt( TO O L B A R _ S U M M A RY _ K E Y, TO O L B A R _ N U M _ K E Y,0 ); if ( nNumToolBars ) { int idc = IDC_CUSTO M _ TO O L B A R S ; for ( int i = 0;i < nNumToolBars;i++ ) { 116第第第二部分第用户界面实例 下载// create empty toolbar C ToolBar *pToolBar = ( CToolBar* )new CWzdTo o l B a r ; m _ To o l B a r L i s t . A d d Tail( pToolBar ); p ToolBar -> Create( this, WS_CHILD | WS_VISIBLE | C B R S _ TOP | C B R S _ TO O LTIPS, idc++ ); SIZE sizeButton, sizeImage; sizeImage.cx = BUTTO N _ W I D T H ; sizeImage.cy = BUTTO N _ H E I G H T; sizeButton.cx = sizeImage.cx + BUTTO N _ X S PA C I N G ; sizeButton.cy = sizeImage.cy + BUTTO N _ Y S PA C I N G ; p ToolBar -> SetSizes( sizeButton, sizeImage ); p ToolBar -> EnableDocking( CBRS_ALIGN_ANY | C B R S _ F L O AT _ M U LTI ); // add all possible tool button bitmaps to this empty toolbar // after first finding out how many buttons are in this bitmap BITMAP bm; CBitmap bitmap; bitmap.LoadBitmap( IDR_MAINFRAME ); bitmap.GetObject( sizeof(BITMAP), &bm ); p ToolBar -> G e t ToolBarCtrl().AddBitmap( bm.bmWi d t h / B U T TON_WIDTH,IDR_MAINFRAME ); // create key for this toolbar CString key; k e y.Format( TO O L B A R _ K E Y,i ); // get number of buttons in this toolbar int nNumButtons = AfxGetApp() -> GetProfileInt( key, N U M _ O F _ B U T TO N S _ K E Y,0 ); // get button info and insert buttons into created toolbar UINT k; T B B U T TON *ptbbutton; AfxGetApp() -> GetProfileBinary( key, B U T TO N _ I N F O _ K E Y,( BYTE ** )&ptbbutton,&k ); for ( int j = 0;j < nNumButtons;j++ ) { p ToolBar -> GetToolBarCtrl().InsertButton( j,ptbbutton+j ); } delete []ptbbutton; } } r e t u r n ( n N u m To o l B a r s ) ; } void CMainFrame::SaveTo o l b a r s ( ) { int nNumToolBars = m_To o l B a r L i s t . G e t C o u n t ( ) ; AfxGetApp() -> WriteProfileInt( TO O L B A R _ S U M M A RY _ K E Y, TO O L B A R _ N U M _ K E Y, n N u m ToolBars ); if ( nNumToolBars ) 第 5 章第菜单、控件条和状态栏第第117下载{ int i = 0; int idc = IDC_CUSTO M _ TO O L B A R S ; for ( POSITION pos = m_ToolBarList.GetHeadPosition();pos;i++ ) { C ToolBar *pToolBar = m_ToolBarList.GetNext( pos ); // create key for this toolbar CString key; k e y.Format( TO O L B A R _ K E Y,i ); // give toolbar a unique, sequential ID for SaveBarState/LoadBarState p ToolBar -> SetDlgCtrlID( idc++ ); // write number of buttons in this toolbar int nButtonCount = pToolBar -> GetTo o l B a r C t r l ( ) . G e t B u t t o n C o u n t ( ) ; AfxGetApp() -> WriteProfileInt( key, N U M _ O F _ B U T TO N S _ K E Y,nButtonCount ); // write info on each button T B B U T TON *ptbbutton = new TBBUTTO N [ n B u t t o n C o u n t ] ; for ( int j = 0;j < nButtonCount;j++ ) { p ToolBar -> GetToolBarCtrl().GetButton( j,ptbbutton+j ); } AfxGetApp() -> WriteProfileBinary( key, B U T TO N _ I N F O _ K E Y, ( BYTE* )ptbbutton,nButtonCount*sizeof( TBBUTTON ) ); delete []ptbbutton; } } } 7. 程序清单— 属性页类 #if !defined TO O L B A R PA G E _ H #define TO O L B A R PA G E _ H // ToolBarPage.h : header file / / #include “WzdProject.h” / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CToolBarPage dialog class CToolBarPage : public CPropertyPage { D E C L A R E _ D Y N C R E ATE( CToolBarPage ) // Construction p u b l i c : C To o l B a r P a g e ( ) ; 118第第第二部分第用户界面实例 下载~ C To o l B a r P a g e ( ) ; // Dialog Data // {{AFX_DATA( CToolBarPage ) enum { IDD = IDD_TO O L B A R _ PAGE }; // }}AFX_DATA // Overrides // ClassWizard generate virtual function overrides // {{AFX_VIRTUAL( CToolBarPage ) p u b l i c : virtual BOOL OnSetActive(); virtual BOOL OnKillActive(); p r o t e c t e d : virtual void DoDataExchange( CDataExchange* pDX ); // DDX/DDV support // }}AFX_VIRT U A L // Implementation p r o t e c t e d : // Generated message map functions // {{AFX_MSG( CToolBarPage ) virtual BOOL OnInitDialog(); afx_msg void OnLButtonDown( UINT nFlags, CPoint point ); afx_msg void OnLButtonUp( UINT nFlags, CPoint point ); afx_msg void OnPaint(); // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : C To o l B a r m _ To o l B a r ; C B i t m a p m _ b i t m a p ; i n t m _ x B i t m a p S t a r t ; i n t m _ y B i t m a p S t a r t ; i n t m _ n B u t t o n C o u n t ; B O O L m _ b M o v i n g ; i n t m _ n B u t t o n M o v e d ; T B B U T TO N m _ t b b u t t o n ; H I C O N m _ h M o v e C u r s o r ; C To o l B a r * m _ p To o l B a r ; C To o l B a r * G e t ToolBar( CPoint point ); i n t GetButtonIndex( CToolBar *pTo o l B a r, CPoint point ); v o i d U p d a t e ToolBar( CToolBar *pToolBar ); B O O L G e t ToolBarNear( CPoint &point,UINT &nMode,BOOL &bHorz ); } ; #endif // ToolBarPage.cpp : implementation file / / #include “stdafx.h” 第 5 章第菜单、控件条和状态栏第第119下载#include "wzd.h" #include "To o l B a r P a g e . h " #include "WzdTo o l B a r. h " #include "WzdProject.h" #include "MainFrm.h" #include #include #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CToolBarPage property page I M P L E M E N T _ D Y N C R E ATE( CToolBarPage, CPropertyPage ) C To o l B a r P a g e : : C ToolBarPage() : CPropertyPage( CToolBarPage::IDD ) { // {{AFX_DATA_INIT( CToolBarPage ) // }}AFX_DATA _ I N I T m_bMoving = FA L S E ; m_nButtonCount = 0; m _ p ToolBar = NULL; m_hMoveCursor = AfxGetApp() -> LoadCursor( IDI_MOVING_BUTTON ); } C To o l B a r P a g e : : ~ C To o l B a r P a g e ( ) { } void CToolBarPage::DoDataExchange( CDataExchange* pDX ) { CPropertyPage::DoDataExchange( pDX ); // {{AFX_DATA_MAP( CToolBarPage ) // }}AFX_DATA _ M A P } BEGIN_MESSAGE_MAP( CToolBarPage, CPropertyPage ) // {{AFX_MSG_MAP(CTo o l B a r P a g e ) O N _ W M _ L B U T TO N D O W N ( ) O N _ W M _ L B U T TO N U P ( ) O N _ W M _ PA I N T ( ) // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / 120第第第二部分第用户界面实例 下载// CToolBarPage message handlers BOOL CTo o l B a r P a g e : : O n I n i t D i a l o g ( ) { C P r o p e r t y P a g e : : O n I n i t D i a l o g ( ) ; // create and load breeder toolbar m _ To o l B a r.Create( this ); m _ To o l B a r. L o a d ToolBar( IDR_MAINFRAME ); // load breeder toolbar bitmap m_bitmap.LoadBitmap( IDR_MAINFRAME ); // get window and button count m_nButtonCount = m_To o l B a r. G e t To o l B a r C t r l ( ) . G e t B u t t o n C o u n t ( ) ; CRect rect; GetClientRect( &rect ); m_xBitmapStart = rect.Wi d t h ( ) / 6 ; m_yBitmapStart = rect.Height()/6; return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FA L S E } void CTo o l B a r P a g e : : O n P a i n t ( ) { CPaintDC dc( this ); // device context for painting // display breeder toolbar bitmap CDC memDC; memDC.CreateCompatibleDC( &dc ); memDC.SelectObject( &m_bitmap ); int x = m_xBitmapStart; int y = m_yBitmapStart; for ( int i = 0; i < m_nButtonCount; i++ ) { dc.BitBlt( x,y, B U T TO N _ W I D T H , B U T TO N _ H E I G H T, &memDC, i*BUTTO N _ W I D T H , 0, SRCCOPY ); x += BUTTON_WIDTH + BUTTO N _ X S PA C I N G ; } m e m D C . D e l e t e D C ( ) ; } BOOL CTo o l B a r P a g e : : O n S e t A c t i v e ( ) { S e t C a p t u r e ( ) ; return CPropertyPage::OnSetActive(); } BOOL CTo o l B a r P a g e : : O n K i l l A c t i v e ( ) 第 5 章第菜单、控件条和状态栏第第121下载{ R e l e a s e C a p t u r e ( ) ; return CPropertyPage::OnKillActive(); } void CToolBarPage::OnLButtonDown( UINT nFlags, CPoint point ) { m_bMoving = FA L S E ; C l i e n t ToScreen( &point ); CWnd *pWnd = WindowFromPoint( point ); // if point is parent or child of if ( pWnd != this && ( pWnd == GetParent() || GetParent() -> IsChild( pWnd ) ) ) { CPoint pt( point ); pWnd -> ScreenToClient( &pt ); if ( pt.y >= 0 ) { pWnd -> SendMessage( WM_LBUTTONDOWN,nFlags,MAKELONG( pt.x,pt.y ) ); } e l s e { UINT ht = pWnd -> SendMessage( WM_NCHITTEST,0,MAKELONG( point.x,point.y ) ); pWnd -> SendMessage( WM_NCLBUTTONDOWN,ht,MAKELONG( point.x,point.y ) ); } r e t u r n ; } // see if this is a toolbar button if ( m_pToolBar = GetToolBar( point ) ) { m_nButtonMoved = GetButtonIndex( m_pTo o l B a r, point ); m _ p ToolBar -> GetToolBarCtrl().GetButton( m_nButtonMoved,&m_tbbutton ); m_bMoving = TRUE; ::SetCursor( m_hMoveCursor ); } // else if this window else if ( pWnd == this ) { S c r e e n ToClient( &point ); p o i n t . O ffset( -m_xBitmapStart,-m_yBitmapStart ); CRect rect( 0,0, ( BUTTO N _ W I D T H + B U T TO N _ X S PACING )*m_nButtonCount,BUTTON_HEIGHT ); if ( rect.PtInRect( point ) ) { m_nButtonMoved = 0; int i = point.x/( BUTTON_WIDTH + BUTTO N _ X S PACING ); 122第第第二部分第用户界面实例 下载for ( int j = 0;j < m_nButtonCount;j++ ) { UINT k; int l; m _ To o l B a r.GetButtonInfo( j,k,k,l ); if ( l == i ) { m_nButtonMoved = j; b r e a k ; } } m _ To o l B a r. G e t ToolBarCtrl().GetButton( m_nButtonMoved,&m_tbbutton ); m_bMoving = TRUE; ::SetCursor( m_hMoveCursor ); S e t C a p t u r e ( ) ; } } CPropertyPage::OnLButtonDown( nFlags, point ); } void CToolBarPage::OnLButtonUp( UINT nFlags, CPoint point ) { if ( m_bMoving ) { // delete button from source toolbar if ( m_pToolBar ) { m _ p ToolBar -> GetToolBarCtrl().DeleteButton( m_nButtonMoved ); } // if dropped anywhere but toolbar property page CRect rect; C l i e n t ToScreen( &point ); G e t WindowRect( &rect ); if ( !rect.PtInRect( point ) ) { // if dropped on existing toolbar, add button to it C ToolBar *pTo o l B a r ; if ( pToolBar = GetToolBar( point ) ) { int i = GetButtonIndex( pTo o l B a r,point ); p ToolBar -> GetToolBarCtrl().InsertButton( i,&m_tbbutton ); U p d a t e ToolBar( pToolBar ); } // else create a new toolbar and add our button to it e l s e { p ToolBar = ( CWzdToolBar* )new CWzdTo o l B a r ; C L i s t < C To o l B a r * , C ToolBar*> *pList = ( ( CMainFrame* )AfxGetMainWnd() ) -> GetTo o l B a r L i s t ( ) ; 第 5 章第菜单、控件条和状态栏第第123下载pList -> AddTail( pToolBar ); p ToolBar -> Create( GetParentFrame(), W S _ C H I L D | W S _ V I S I B L E | C B R S _ TO P | C B R S _ TO O LTIPS ); SIZE sizeButton, sizeImage; sizeImage.cx = BUTTO N _ W I D T H ; sizeImage.cy = BUTTO N _ H E I G H T; sizeButton.cx = sizeImage.cx + BUTTO N _ X S PA C I N G ; sizeButton.cy = sizeImage.cy + BUTTO N _ Y S PA C I N G ; p ToolBar -> SetSizes( sizeButton, sizeImage ); p ToolBar -> EnableDocking( CBRS_ALIGN_ANY | CBRS_FLOAT _ M U LTI ); // add all possible tool button bitmaps p ToolBar -> G e t ToolBarCtrl().AddBitmap( m_nButtonCount,IDR_MAINFRAME ); // add new button to this new toolbar p ToolBar -> GetToolBarCtrl().InsertButton( 0,&m_tbbutton ); UINT nMode; BOOL bHorz; if ( GetToolBarNear( point,nMode,bHorz ) ) { // dock toolbar centered on cursor CSize size = pToolBar -> CalcFixedLayout( FALSE,bHorz ); if ( bHorz ) point.x -= size.cx/2; e l s e point.y -= size.cy/2; CRect rectx( point,size ); GetParentFrame() -> DockControlBar( pTo o l B a r,nMode,&rectx ); p ToolBar -> Invalidate(); GetParentFrame() -> RecalcLayout(); } e l s e { GetParentFrame() -> FloatControlBar( pTo o l B a r, point, CBRS_ALIGN_TOP ); } } } // else update source toolbar if any else if ( m_pToolBar ) { U p d a t e ToolBar( m_pToolBar ); // if no buttons left on toolbar, delete it if ( m_pToolBar && m_pToolBar -> G e t ToolBarCtrl().GetButtonCount() == 0 ) { POSITION pos; C L i s t < C To o l B a r * , C ToolBar*> *pList = 124第第第二部分第用户界面实例 下载( ( CMainFrame* )AfxGetMainWnd() ) -> GetTo o l B a r L i s t ( ) ; if ( pos = pList -> Find( m_pToolBar ) ) { pList -> RemoveAt( pos ); m _ p ToolBar -> m_bAutoDelete = TRUE; if ( !m_pToolBar -> IsFloating() ) { m _ p ToolBar -> SendMessage( WM_CLOSE ); } e l s e { CDockBar* pDockBar = m_pToolBar -> m_pDockBar; CMiniDockFrameWnd* pDockFrame = ( CMiniDockFrameWnd* )pDockBar -> GetParent(); pDockFrame -> ShowWindow( SW_HIDE ); pDockFrame -> SendMessage( WM_CLOSE ); } } } } m_bMoving = FA L S E ; ::SetCursor( AfxGetApp() -> LoadStandardCursor( IDC_ARROW ) ); } CPropertyPage::OnLButtonUp( nFlags, point ); } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Helper Toolbar Functions C ToolBar *CTo o l B a r P a g e : : G e t ToolBar( CPoint point ) { CRect rect; C L i s t < C To o l B a r * , C ToolBar*> *pList = ( ( CMainFrame* )AfxGetMainWnd() ) -> GetTo o l B a r L i s t ( ) ; if ( !pList -> IsEmpty() ) { for ( POSITION pos = pList -> GetHeadPosition(); pos; ) { C ToolBar *pToolBar = pList -> GetNext( pos ); p ToolBar -> GetWindowRect( &rect ); if ( rect.PtInRect( point ) ) { return pTo o l B a r ; } } } return( NULL ); } int CToolBarPage::GetButtonIndex( CToolBar *pTo o l B a r, CPoint point ) 第 5 章第菜单、控件条和状态栏第第125下载{ CRect rect; p ToolBar -> ScreenToClient( &point ); int nButtons = pToolBar ->GetTo o l B a r C t r l ( ) . G e t B u t t o n C o u n t ( ) ; for ( int i = 0; i < nButtons; i++ ) { p ToolBar -> GetItemRect( i,&rect ); if ( rect.PtInRect( point ) ) { return( i ); } } return( -1 ); } void CTo o l B a r P a g e : : U p d a t e ToolBar( CToolBar *pToolBar ) { if ( pToolBar ) { if ( pToolBar -> IsFloating() ) { CDockBar* pDockBar = pToolBar -> m_pDockBar; CMiniDockFrameWnd* pDockFrame = ( CMiniDockFrameWnd* )pDockBar -> GetParent(); pDockFrame -> RecalcLayout( TRUE ); pDockFrame -> UpdateWi n d o w ( ) ; } e l s e { p ToolBar -> Invalidate(); GetParentFrame() -> RecalcLayout(); } } } BOOL CTo o l B a r P a g e : : G e t ToolBarNear( CPoint &point,UINT &nMode,BOOL &bHorz ) { CRect rect; BOOL bDock = FA L S E ; CDockState dockstate; ( ( CMainFrame * )AfxGetMainWnd() ) -> GetDockState( dockstate ); for ( int i = 0; i < dockstate.m_arrBarInfo.GetSize(); i++ ) { CControlBarInfo *pBarInfo = ( CControlBarInfo * )dockstate.m_arrBarInfo[i]; CControlBar *pBar = ( CControlBar * )pBarInfo -> m_pBar; // if this bar is visible, can be docked and isn’t floating, check it out if ( pBarInfo - >m_bVisible && pBarInfo -> m_bDocking && !pBar -> IsFloating() ) { pBarInfo -> m_pBar -> GetWindowRect( &rect ); 126第第第二部分第用户界面实例 下载DWORD dwStyle = pBar -> GetBarStyle(); if ( bHorz = ( dwStyle & CBRS_ORIENT_HORZ ) ) { // if user clicked in this region, dock new toolbar here if ( point.y >= rect.top && point.y < rect.bottom) { bDock = TRUE; point.y = rect.top; if ( dwStyle & CBRS_ALIGN_TOP ) nMode = AFX_IDW_DOCKBAR_TO P ; e l s e nMode = AFX_IDW_DOCKBAR_BOTTO M ; b r e a k ; } } e l s e { // else if user clicked in this region, dock here if ( point.x >= rect.left && point.x < rect.right ) { bDock = TRUE; point.x = rect.left; if ( dwStyle & CBRS_ALIGN_LEFT ) nMode = AFX_IDW_DOCKBAR_LEFT; e l s e nMode = AFX_IDW_DOCKBAR_RIGHT; b r e a k ; } } } } return( bDock ); } 5.4 实例10:在对话框中添加工具栏、菜单和状态栏 1. 目标 如图5 - 4所示为对话框应用程序增加菜单、工具栏和状态栏。 第 5 章第菜单、控件条和状态栏第第127下载 图5-4 具有工具栏和状态栏的对话框 该对话框应用程序具 有一个菜单、工具条 和状态栏2. 策略 与S D I和M D I应用程序不同, A p p Wi z a r d不能创建具有菜单、工具栏和状态栏的对话框应 用程序。这并不意味着对话框应用程序就不能具有这些用户输入方式,毕竟所有具有菜单、 工具栏或者状态栏的弹出窗口实际上都是可以添加到其他窗口的控件窗口,不同之处在于需 要手工添加这些项目。 3. 步骤 1) 增加菜单 用Menu Editor创建新菜单,或者从另一个工程中拷贝一个。甚至可以创建一个新的 S D I 应用程序,并从其. r c文件中获取器菜单并将其粘贴到本对话框应用程序的 . r c文件。 用Dialog Editor将该菜单增加到应用程序的对话框模板。 用C l a s s Wi z a r d为对话框类增加命令消息处理函数以处理这些菜单项。 2) 增加工具栏 用Toolbar Editor创建新工具栏,或者如上所述的最后步骤从另一个工程中拷贝。 由于Dialog Editor不允许在模板中添加工具栏控件,所以需要用自己的对话框类动态地 创建一个。确定创建该工具栏的位置的一个好办法是用 Dialog Editor在要添加工具栏的地方 创建一个静态控件占位符 ( p l a c e h o l d e r ),然后将该控件的位置和大小传送给创建中的工具栏。 因此用Dialog Editor来增加静态控件到要添加工具栏的对话框模板,将可以使工具栏看起来 又细又长。 在对话框类中嵌入C To o l b a r变量: C Toobar m_wndTo o l B a r ; 在对话框类的O n I n i t D i a l o g ( )消息处理函数内创建工具栏: if ( !m_wndTo o l B a r.Create( this ) || ! m _ w n d To o l B a r. L o a d ToolBar( IDR_MAINFRAME ) ) { TRACE0( “Failed to create toolbar\n” ); return -1; // fail to create } 在先前用对话框编辑器创建的静态控件内定位工具栏: CRect rect; GetDlgItem( IDC_TO O L B A R _ S TATIC ) -> GetWindowRect( &rect ); S c r e e n ToClient( &rect ); m _ w n d To o l B a r. M o v e Window( &rect ); 3) 增加状态栏 用String Table Editor定义状态栏窗格: I D _ I N D I C ATOR_ONE “xxx” I D _ I N D I C ATOR_TWO “yyy” 再次使用Dialog Editor向对话框模板中将要添加状态栏的位置上添加静态控件。 在对话框类中嵌入一个状态栏控件: CStatusBar m_wndStatusBar; 将用String Editor创建的状态窗格列表增加到对话框类代码的顶部: static UINT indicators[] = 128第第第二部分第用户界面实例 下载{ I D _ S E PA R ATO R , // status line indicator I D _ I N D I C ATO R _ O N E , I D _ I N D I C ATO R _ T W O , } ; 创建状态栏并将其定位于静态控件之上: if ( !m_wndStatusBar.Create(this, WS_CHILD|WS_VISIBLE|WS_BORDER) || ! m _ w n d S t a t u s B a r.SetIndicators( indicators, sizeof( indicators )/sizeof( UINT ) ) ) { TRACE0( “Failed to create status bar\n” ); return -1; // fail to create } GetDlgItem( IDC_STAT U S B A R _ S TATIC ) -> GetWindowRect( &rect ); S c r e e n ToClient( &rect ); m _ w n d S t a t u s B a r. M o v e Window( &rect ); 4. 注意 ■ 由C S t a t u s B a r创建的状态栏中的第一个窗格没有边界。虽然这对 S D I或者M D I应用程序 没有什么影响,因为状态栏一般是位于视图的下方,但在对话框应用程序中该窗格实际上却 消失在对话框窗口内了。为了使该窗格具有明确的边界可以在状态栏创建后使用下列代码: UINT nID, nStyle; int nW i d t h ; m _ w n d S t a t u s B a r.GetPaneInfo( 0, nID, nStyle, nWidth ); m _ w n d S t a t u s B a r.SetPaneInfo( 0, nID, SBPS_STRETCH, nWidth ); ■ 正如工具栏和状态栏必须手工增加一样,程序员还要负责手工更新它们的状态。对工 具栏就意味手工变灰或者检查各个按钮,对状态栏则意味着手工设置每个窗格的文本。 ■ 如果已经实现菜单,可以从对话框应用程序的系统菜单中删除帮助菜单项,然后在对 话框类中删除其支持逻辑代码。 5. 使用光盘时注意 执行随书附带光盘上的工程的时候,将会注意到该对话框应用程序有一个菜单、工具栏 和状态栏。 5.5 实例11:在弹出菜单中增加位图标记 1. 目标 如图5 - 5所示在弹出菜单中增加位图 标记。 2. 策略 实际上只对位图使用一点小技巧即 可。并不需要为菜单增加一个位图,而 是打开具有垂直而细长的位图对话框, 在与之相邻处可以打开一个弹出菜单。 另一个更复杂的方式将涉及到自己绘制 第 5 章第菜单、控件条和状态栏第第129 图5-5 具有标记的弹出菜单 该弹出菜单 旁边具有一 个位图标记 下载菜单,这可以参考实例7。 3. 步骤 1 ) 创建弹出菜单对话框类 为所需的菜单项创建弹出菜单资源。 创建一个对话框资源,其一边是图像控件,另一边是静态控件。配置该图像控件使其拥 有出现在静态控件旁边的位图。 重新改变静态控件和对话框模板的大小,使它们与弹出菜单的大小相符合。由于还需要 完成以下步骤才能看见最后结果,因此目前只能进行大概的估计。 用C l a s s Wi z a r d创建一个使用该模板的对话框类。 用C l a s s Wi z a r d为该对话框类增加W M _ I N I T D I A L O G消息处理函数。在此可以查找光标位 置,重新定位对话框以便于模仿菜单出现在光标旁边的感觉: BOOL CWzdDlg::OnInitDialog() { C D i a l o g : : O n I n i t D i a l o g ( ) ; // position where mouse button was clicked CPoint pt; GetCursorPos( &pt ); S e t WindowPos( NULL,pt.x,pt.y,0,0,SWP_NOSIZE ); return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FA L S E } 用C l a s s Wi z a r d为该类增加 W M _ PA I N T消息处理函数,用于在静态控件之上打开弹出菜 单: void CWzdDlg::OnPaint() { C D i a l o g : : O n P a i n t ( ) ; // load up menu CMenu menu; menu.LoadMenu( IDR_WZD_MENU ); CMenu* pPopup = menu.GetSubMenu( 0 ); // get location of static control and display popup menu there CRect rect; GetDlgItem( IDC_MENU_STATIC ) -> GetWindowRect( &rect ); int nLeft = rect.right + 2; G e t WindowRect( &rect ); pPopup -> TrackPopupMenu( TPM_RIGHTBUTTON, nLeft, rect.top, GetParent() ); // cancel this dialog PostMessage( WM_CLOSE ); } 注意一旦用户选择菜单项,将发送一个消息给自己以便立即关闭该对话框。 130第第第二部分第用户界面实例 下载2) 实现弹出菜单对话框类 用C l a s s Wi z a r d向欲实现该菜单的类中增加 W M _ R B U T TO N D O W N消息处理函数。在该实 例中,该菜单用Vi e w类实现。这里将用D o M o d a l ( )创建对话框: void CWzdView::OnRButtonDown( UINT nFlags, CPoint point ) { CWzdDlg dlg; d l g . D o M o d a l ( ) ; C View::OnRButtonDown( nFlags, point ); } 4. 注意 正如以上提到过的,还可以自己绘制菜单,将菜单项文本定位于居左或者居右的位置, 并且在绘制菜单项时绘制位图的某些部分,这种方式也可以实现以上的效果。 5. 使用光盘时注意 执行随书附带光盘上的工程时,在视图内用鼠标右键单击并打开弹出菜单,将在其旁边 显示位图。 5.6 实例12:工具栏上的下拉按钮 1. 目标 如图5 - 6所示在工具栏上创建下拉按钮。 2. 策略 这里要用到MFC 6.0中新增加的两种 工 具 栏 风 格 。 应 用 程 序 必 须运 行 于 Windows 98或者带有 Internet Explorer 4.01 以及安装更高版本的系统上。第一 种风格是TBSTYLE_ DROPDOWN风格, 该风格导致工具栏按钮在其被按下时发 出T B N _ D R O P D O W N通知消息(一般地, 工具栏按钮在这种情况发出包含该按钮 I D号的W M _ C O M M A N D消息)。该通知 消息被用于在按钮下方显示弹出菜单,就和一般菜单一样。第二种风格 T B S T Y L E _ E X _ D R AW D D A R R O W S则导致具有第一种风格的按钮绘制出图 5 - 6所示的下拉箭头。 为了防止C M a i n F r a m e类因为应用这些新风格的代码而变得混乱,这里将其封装到用户自 己的C To o l b a r派生工具栏类。 3. 步骤 1) 创建新的工具栏类 用C l a s s Wi z a r d创建C To o l b a r C t r l的派生类,然后用 Text Editor将所有C To o l b a r C t r l改变为 C To o l b a r。 为该类增加一个新函数以装载工具栏,但是它将对选中的工具栏按钮应用两种新的工具 栏风格,这将使它显示下拉按钮,并在被按下时发出 T B N _ D R O P D O W N通知消息: 第 5 章第菜单、控件条和状态栏第第131下载 图5-6 工具栏上的下拉按钮 该工具条按钮打 开一个下拉菜单BOOL CWzdTo o l B a r : : L o a d ToolBarEx( UINT id ) { // load toolbar info BOOL bRet; bRet = CTo o l B a r : : L o a d ToolBar( id ); // find where our dropdown button will go int pos = CommandToIndex( IDC_WZD_DROPBUTTON ); // set this button to be a dropdown button int iImage; UINT nID,nStyle; GetButtonInfo( pos,nID,nStyle,iImage ); SetButtonInfo( pos,nID,nStyle|TBSTYLE_DROPDOWN,iImage ); // ask toolbar to draw a down arrow next to this and // any button with TBSTYLE_DROPDOWN SendMessage( TB_SETEXTENDEDSTYLE,0,TBSTYLE_EX_DRAWDDARROWS ); return bRet; } 注意需要用新工具栏消息 T B _ S E T E X T E N D E D S T Y L E来设置 T B S T Y L E _ E X _ D R A W D D A R R O W S风格。 如果工具栏的下拉按钮位于浮动条上,则工具栏末端的按钮可能被剪切。因为 C To o l b a r 的C a l c D y n a m i c L a y o u t ( )函数没有注意到这一事实:下拉按钮比正常的按钮要长。为了弥补这 一缺陷,需要重载工具栏类的 C a l c D y n a m i c L a y o u t ( )函数以加大工具栏的像素尺寸值,所以在 代码中声明该重载并将其实现: // in your toolbar class’s declaration file: CSize CalcDynamicLayout( int nLength, DWORD dwMode ); : : : // in your toolbar class’s implementation file: CSize CWzdToolBar::CalcDynamicLayout( int nLength, DWORD dwMode ) { CSize size = CToolBar::CalcDynamicLayout( nLength,dwMode ); // make room on non-fixed toolbars for additional down arrows if ( dwMode&LM_HORZ ) size.cx += 16*1; // 16 * number of buttons // with arrows return size; } 这一步为该类手工增加T B N _ D R O P D O W N消息处理函数。在声明文件内增加如下代码: // {{AFX_MSG( CWzdToolBar ) // }}AFX_MSG // add the following void OnDropdownButton( LPNMTOOLBAR pNotifyStruct, LRESULT* result ); D E C L A R E _ M E S S A G E _ M A P ( ) 为消息映射增加如下代码: BEGIN_MESSAGE_MAP( CWzdTo o l B a r, CToolBar ) // {{AFX_MSG_MAP( CWzdToolBar ) 132第第第二部分第用户界面实例 下载// }}AFX_MSG_MAP ON_NOTIFY_REFLECT( TBN_DROPDOWN,OnDropdownButton ) // <<< add E N D _ M E S S A G E _ M A P ( ) 通过在工具栏按钮右下方打开弹出菜单来处理该通知消息: void CWzdToolBar::OnDropdownButton( LPNMTOOLBAR lpnmtb, L R E S U LT *result ) { // get location of clicked button CRect rect; GetItemRect( CommandToIndex( lpnmtb -> iItem ),&rect ); C l i e n t ToScreen( &rect ); // putup a popup menu there CMenu menu; menu.LoadMenu( IDR_WZD_MENU ); CMenu* pPopup = menu.GetSubMenu( 0 ); pPopup -> TrackPopupMenu( TPM_RIGHTBUTTON, rect.left, rect.bottom, this ); *result = TBDDRET_DEFA U LT; // drop-down was handled, also // TBDDRET_NODEFA U LT drop-down was not handled. // TBDDRET_TREATPRESSED generate a WM_COMMAND message } 为查看该类的完整程序,可以参考程序清单— 工具栏类 2) 实现新的工具栏类 在C M a i n F r a m e中使用新的工具栏类并用L o a d B i t m a p E x ( )装载工具栏资源。 4. 注意 如果工具栏为固定大小,则不需要考虑重载 C a l c D y n a m i c L a y o u t ( )以设置工具栏大小,工 具栏将总是保持其固有大小。请参阅第 2章以了解工具栏和控件条的更多内容。如果工具栏可 以垂直停靠,可以注意到下拉箭头将被工具栏剪切。这可以在 C a l c D y n a m i c L a y o u t ( )中修改代 码以改变这一效果,但是这看起来好像是微软的特色而不像是一个程序错误 (换句话说,在其 他微软应用程序中的下拉工具栏按钮也剪切下拉箭头。 ) 5. 使用光盘时注意 ■ 执行随书附带光盘上的工程时,可以注意到工具栏上的下拉按钮,当其被单击的时候 将在工具栏按钮下出现一个菜单。 ■ 由于该工程需要用到只在Developer Studio v6.0中才提供的功能,所以该光盘中只有该 工程的VC 6.0版本。 6. 程序清单— 工具栏类 #if !defined( AFX_WZDTO O L B A R _ H _ _ 2 7 6 4 9 E 3 1 _ C 8 0 7 _ 11D1_9B5D_00AA003D8695__INCLUDED_ ) #define AFX_WZDTO O L B A R _ H _ _ 2 7 6 4 9 E 3 1 _ C 8 0 7 _ 11 D 1 _ 9 B 5 D _ 0 0 A A 0 0 3 D 8 6 9 5 _ _ I N C L U D E D _ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 第 5 章第菜单、控件条和状态栏第第133下载// WzdTo o l B a r.h : header file / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdToolBar window class CWzdToolBar : public CTo o l B a r { // Construction p u b l i c : C W z d To o l B a r ( ) ; BOOL LoadToolBarEx( UINT id ); // Attributes p u b l i c : // Operations p u b l i c : // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CWzdToolBar ) // }}AFX_VIRT U A L CSize CalcDynamicLayout( int nLength, DWORD dwMode ); // Implementation p u b l i c : virtual ~CWzdTo o l B a r ( ) ; // Generated message map functions p r o t e c t e d : // {{AFX_MSG( CWzdToolBar ) // }}AFX_MSG void OnDropdownButton( LPNMTOOLBAR pNotifyStruct, LRESULT* result ); D E C L A R E _ M E S S A G E _ M A P ( ) } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Developer Studio will insert additional declarations immediately // before the previous line. # e n d i f // !defined( AFX_WZDTO O L B A R _ H _ _ 2 7 6 4 9 E 3 1 _ C 8 0 7 _ 11D1_9B5D_00AA003D8695__INCLUDED_ ) // WzdTo o l B a r.cpp : implementation file / / #include "stdafx.h" #include "wzd.h" 134第第第二部分第用户界面实例 下载#include "WzdTlBar. h " #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTo o l B a r C W z d To o l B a r : : C W z d To o l B a r ( ) { } C W z d To o l B a r : : ~ C W z d To o l B a r ( ) { } BEGIN_MESSAGE_MAP( CWzdTo o l B a r, CToolBar ) // {{AFX_MSG_MAP( CWzdToolBar ) // }}AFX_MSG_MAP ON_NOTIFY_REFLECT( TBN_DROPDOWN,OnDropdownButton ) E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdToolBar message handlers BOOL CWzdTo o l B a r : : L o a d ToolBarEx( UINT id ) { // load toolbar info BOOL bRet; bRet = CTo o l B a r : : L o a d ToolBar( id ); // find where our dropdown button will go int pos = CommandToIndex( IDC_WZD_DROPBUTTON ); // set this button to be a dropdown button int iImage; UINT nID,nStyle; GetButtonInfo( pos,nID,nStyle,iImage ); SetButtonInfo( pos,nID,nStyle|TBSTYLE_DROPDOWN,iImage ); // ask toolbar to draw a down arrow next to this and any button with // TBSTYLE_DROPDOWN SendMessage( TB_SETEXTENDEDSTYLE,0,TBSTYLE_EX_DRAWDDARROWS ); return bRet; } CSize CWzdToolBar::CalcDynamicLayout( int nLength, DWORD dwMode ) 第 5 章第菜单、控件条和状态栏第第135下载{ CSize size = CToolBar::CalcDynamicLayout( nLength,dwMode ); // make room on non-fixed toolbars for additional down arrows if (dwMode&LM_HORZ) size.cx += 16*1; // 16 * number of buttons // with arrows return size; } void CWzdToolBar::OnDropdownButton( LPNMTOOLBAR lpnmtb, LRESULT *result ) { // get location of clicked button CRect rect; GetItemRect( CommandToIndex( lpnmtb -> iItem ),&rect ); C l i e n t ToScreen( &rect ); // putup a popup menu there CMenu menu; menu.LoadMenu( IDR_WZD_MENU ); CMenu* pPopup = menu.GetSubMenu( 0 ); pPopup -> TrackPopupMenu( TPM_RIGHTBUTTON, rect.left, rect.bottom, this ); *result = TBDDRET_DEFA U LT; //drop-down was handled, also // TBDDRET_NODEFA U LT drop-down was not handled. // TBDDRET_TREATPRESSED treat click as a button press } 5.7 实例13:在状态栏中添加图标 1. 目标 如图5 - 7所示在状态栏上添加一个图标。 2. 策略 正如菜单可以自绘制一样,状态栏上 的窗格也可以做到这一点。因此本例将为 状态栏增加新的窗格并改变其风格为自绘 制类型。在本实例中,将根据状态标记, 使用两个L E D图标之一来绘制该窗格。 3. 步骤 1) 创建新资源 用Icon Editor创建两个或者多个图标 以指示某些应用程序的不同状态。在本实例中将创建一个红色和一个绿色的 L E D图标。 用String Table Editor创建新的空白字符串。在该字符串中留多少空白将确定状态窗格的 宽度。本实例中用I D _ I N D I C ATO R _ R E D来标识该字符串。 2) 创建新的状态栏类 用C l a s s Wi z a r d创建新的 C S t a t u s B a r C t r l派生类,然后用 Text Editor将全部引用从 C S t a t u s B a r C t r l更改为CStatusBar (ClassWi z a r d不支持C S t a t u s B a r )。 136第第第二部分第用户界面实例 下载 图5-7 状态栏上的图标 该“L E D” 将由状态条 手工绘制在该新类中嵌入H I C O N变量并在构造函数中装载新图标: C W z d S t a t u s B a r : : C W z d S t a t u s B a r ( ) { // load any graphics m_hRedLedIcon = AfxGetApp() -> LoadIcon( IDI_RED_LED ); m_hGreenLedIcon = AfxGetApp() -> LoadIcon( IDI_GREEN_LED ); } 这一步需要为欲绘制的窗格应用自绘制风格。将该功能封装到类函数 I n i t D r a w i n g ( )中: void CWzdStatusBar::InitDrawing() { UINT nID, nStyle; int nPane, nWi d t h ; // for each pane that we will be drawing nPane = CommandToIndex( ID_INDICATOR_LED ); GetPaneInfo( nPane, nID, nStyle, nWidth ); SetPaneInfo( nPane, nID, SBPS_OWNERDRAW|SBPS_NOBORDERS, nWidth ); } 这一步重载该类的 D r a w I t e m ( )函数来绘制该窗格。通过从 D R AW I T E M S T R U C T调用参数 得到设备环境、窗格I D和要绘制的窗格区域等信息来启动该功能: void CWzdStatusBar::DrawItem( LPDRAWITEMSTRUCT lpDrawItemStruct ) { // get graphic context to draw to CDC* pDC = CDC::FromHandle( lpDrawItemStruct -> hDC) ; // get the pane’s rectangle CRect rect( lpDrawItemStruct -> rcItem ); // get the pane’s id UINT nID, nStyle; int nWi d t h ; GetPaneInfo( lpDrawItemStruct -> itemID, nID, nStyle, nWidth ); 由于该类中需要绘制一个以上的窗格,因此为窗格 I D创建一个开关变量并根据某种条件 绘制两种图标之一。注意首先用按钮表面的当前颜色填充背景区域以便所绘制的内容与状态 栏的剩余部分相协调: // draw to that pane based on a status switch ( nID ) { case ID_INDICATO R _ L E D : // draw the background CBrush bkColor( GetSysColor( COLOR_3DFACE ) ); CBrush* pOldBrush = ( CBrush* )pDC -> SelectObject( &bkColor ); pDC -> FillRect( rect, &bkColor ); pDC -> SelectObject( pOldBrush ); // draw the appropriate LED HICON hicon = ( ( CMainFrame* )AfxGetMainWnd() ) -> m_bTe s t ? 第 5 章第菜单、控件条和状态栏第第137下载m _ h R e d L e d I c o n : m _ h G r e e n L e d I c o n ; pDC -> DrawIcon( rect.left,rect.top,hicon ); b r e a k ; } } // end of DrawItem() 参考本实例结尾的清单— 状态栏类可以查看状态栏类的完整代码列表。 3) 实现新的状态栏类 用该新类的名字替代M a i n f r m . h中的C S t a t u s B a r。 在M a i n f r m . c p p表中想让该窗格出现的位置增加新的状态窗格 I D: static UINT indicators[] = { I D _ S E PA R ATOR, // status line indicator I D _ I N D I C ATO R _ C A P S , I D _ I N D I C ATO R _ N U M , I D _ I N D I C ATO R _ S C R L , I D _ I N D I C ATOR_LED, <<<<<< ADD HERE } ; 在C M a i n F r a m e的O n C r e a t e ( )函数中调用I n i t D r a w i n g ( )函数初始化新的状态栏类: // change selected pane style(s) to self-draw m _ w n d S t a t u s B a r. I n i t D r a w i n g ( ) ; 现在可以打开状态栏。在想改变 L E D的状态时,应该使状态栏无效以使其重新绘制: // change status m _ b Test = FA L S E ; // force icon to be re-drawn m _ w n d S t a t u s B a r. I n v a l i d a t e ( ) ; 4. 注意 可以改变图标的大小以使其适合状态栏。确保使用 3 2×3 2图标大小,但仅能用于左上角。 这里当然还可以使用位图,但是图标比较适合这种方案,因为它一般较小并自动具有透明颜 色,这将允许图标融合到状态栏中,并产生图标仿佛是状态栏一部分的视觉效果。 5. 使用光盘时注意 执行随书附带光盘上的工程时,可以注意到状态栏中具有绿色或者红色的图标。单击 Te s t / W z d菜单命令可以改变其颜色。 6. 程序清单— 状态栏类 #if !defined( AFX_WZDSTAT B _ H _ _ 5 D F 0 1 3 6 0 _ 8 7 6 F _ 11D2_A18D_D6622706D73F__INCLUDED_ ) #define AFX_WZDSTAT B _ H _ _ 5 D F 0 1 3 6 0 _ 8 7 6 F _ 11 D 2 _ A 1 8 D _ D 6 6 2 2 7 0 6 D 7 3 F _ _ I N C L U D E D _ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 // WzdStatB.h : header file / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdStatusBar window 138第第第二部分第用户界面实例 下载class CWzdStatusBar : public CStatusBar { // Construction p u b l i c : C W z d S t a t u s B a r ( ) ; // Attributes p u b l i c : // Operations p u b l i c : void InitDrawing(); // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CWzdStatusBar ) // }}AFX_VIRT U A L virtual void DrawItem( LPDRAWITEMSTRUCT lpDrawItemStruct ); // Implementation p u b l i c : virtual ~CWzdStatusBar(); // Generated message map functions p r o t e c t e d : // {{AFX_MSG( CWzdStatusBar ) // NOTE - the ClassWizard will add and remove member functions here. // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : HICON m_hRedLedIcon; HICON m_hGreenLedIcon; } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Visual C++ will insert additional declarations immediately before // the previous line. # e n d i f // !defined( AFX_WZDSTAT B _ H _ _ 5 D F 0 1 3 6 0 _ 8 7 6 F _ 11D2_A18D_D6622706D73F__INCLUDED_ ) // WzdStatB.cpp : implementation file / / #include "stdafx.h" #include "wzd.h" #include "WzdStatB.h" #include "MainFrm.h" 第 5 章第菜单、控件条和状态栏第第139下载#ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdStatusBar C W z d S t a t u s B a r : : C W z d S t a t u s B a r ( ) { // load any graphics m_hRedLedIcon = AfxGetApp() -> LoadIcon( IDI_RED_LED ); m_hGreenLedIcon = AfxGetApp() -> LoadIcon( IDI_GREEN_LED ); } C W z d S t a t u s B a r : : ~ C W z d S t a t u s B a r ( ) { } BEGIN_MESSAGE_MAP( CWzdStatusBar, CStatusBar ) // {{AFX_MSG_MAP( CWzdStatusBar ) // NOTE - the ClassWizard will add and remove mapping macros here. // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdStatusBar message handlers void CWzdStatusBar::InitDrawing() { UINT nID,nStyle; int nPane,nWi d t h ; // for each pane that we will be drawing nPane = CommandToIndex( ID_INDICATOR_LED ); GetPaneInfo( nPane, nID, nStyle, nWidth ); SetPaneInfo( nPane, nID, SBPS_OWNERDRAW|SBPS_NOBORDERS, nWidth ); } void CWzdStatusBar::DrawItem( LPDRAWITEMSTRUCT lpDrawItemStruct ) { // get graphic context to draw to CDC* pDC = CDC::FromHandle( lpDrawItemStruct -> hDC ); // get the pane’s rectangle CRect rect( lpDrawItemStruct -> rcItem ); 140第第第二部分第用户界面实例 下载// get the pane’s id UINT nID,nStyle; int nW i d t h ; GetPaneInfo( lpDrawItemStruct -> itemID, nID, nStyle, nWidth ); // draw to that pane based on a status switch ( nID ) { case ID_INDICATO R _ L E D : // draw the background CBrush bkColor( GetSysColor( COLOR_3DFACE ) ); CBrush* pOldBrush = ( CBrush* )pDC -> SelectObject( &bkColor ); pDC -> FillRect( rect, &bkColor ); pDC -> SelectObject( pOldBrush ); // draw the appropriate LED HICON hicon = ( ( CMainFrame* )AfxGetMainWnd() ) -> m_bTe s t ? m _ h R e d L e d I c o n : m _ h G r e e n L e d I c o n ; pDC -> DrawIcon( rect.left,rect.top,hicon ); b r e a k ; } } 5.8 实例14:使用伸缩条 1. 目标 如图5 - 8所示为应用程序增加伸缩条。 2. 策略 M F C的新C R e B a r和C R e B a r C t r l类可以自 动完成为应用程序添加伸缩条的大多数工 作。但是这些类只能在 6 . 0版本中使用,而 且伸缩条仅能用于 Wi n d o w s 9 8系统或者安装 了I E 4 . 0 1及其以上版本的系统。 3. 步骤 1) 创建伸缩条 在C M a i n F r a m e类中嵌入伸缩条类: CReBar m_wndReBar; 在C M a i n F r a m e的O n C r e a t e ( )函数中创建伸缩条的窗口: if ( !m_wndReBar.Create( this ) ) { TRACE0( "Failed to create rebar\n" ); return -1; // fail to create } 2) 为伸缩条增加控件 第 5 章第菜单、控件条和状态栏第第141下载 图5-8 伸缩条 该应用程序具 有两个伸缩条用如下A d d B a r ( )函数为该伸缩条增加工具栏: m _ w n d R e B a r.AddBar( &m_wndTo o l B a r, " W Z D " , // rebar title N U L L , // a CBitmap background R B B S _ G R I P P E R A LWAYS | RBBS_FIXEDBMP // style ) ; 不要为该工具栏启动停靠功能或使用 A S S E RT。工具栏不需要停靠— 伸缩条具有允许用 户在其内部移动控件条的自动功能。 为了在伸缩条中添加其他类型的控件,将其单独或者连同其他一个或者多个控件放入对 话框模板,然后为该模板创建对话条类并在 C M a i n F r a m e中嵌入对话条类变量,接着创建对话 条并将其添加到伸缩条内: if ( !m_WzdDialogBar.Create( this, IDD_WZD_DIALOG, WS_CHILD|WS_VISIBLE,-1 ) || !m_WzdDialogBar.InitDialog() ) { TRACE0( "Failed to create dialog bar\n" ); return -1; // fail to create } m _ w n d R e B a r.AddBar( &m_WzdDialogBar, “ W Z D ” , // rebar title N U L L , // a CBitmap background R B B S _ G R I P P E R A LWAYS | RBBS_FIXEDBMP // style ) ; 4. 注意 伸缩条的最大优点是允许用户定制自己的工具栏和其他控件的位置。 M F C应用程序已经 具有使用停靠栏的功能。因而伸缩条也许是微软为非 M F C应用程序提供的功能,这也就是为 什么不能使工具栏既有停靠功能同时又包括在伸缩条中的原因。作为 M F C应用程序的开发者, 实际上为使工具栏停靠可以避免使用伸缩条,除非为了达到模仿特定外观的目的。 5. 使用光盘时注意 执行随书附带光盘上的工程时,可以注意到应用程序顶部的伸缩条内有一个工具栏和对 话条,这些控件条可以通过拖动它们前面的提手条而移动。 142第第第二部分第用户界面实例 下载下载下载 第 6 章 视 S D I和M D I应用程序的视是用户与应用程序(特别是应用程序正在编辑的文档)交互的主要 方式。本章的所有实例都涉及到视,从创建属性表的视、打印视到向视拖放文件等等。这些 实例包括: 实例15 创建标签窗体视,本例将创建具有属性表的视。 实例16 创建一个简单组合框的视,作为视的通用控件,本例中将看到如何创建控件视 (按钮、列表框和编辑框都是控件)。 实例17 打印报表,使用MFC CVi e w类的内置功能打印报表。 实例18 打印视,自动地捕获应用程序的屏幕图像然后打印。 实例19 绘制M D I客户视(MDI Client Vi e w),在本例中将看到一种为 M D I应用程序的普通 背景添加颜色和模式的方法。背景是 M D I的客户视,是M D I应用程序的视驻足的地方。 实例20 拖放文件到视,然后打开拖放到视中的文件。 6.1 实例15:创建标签窗体视 1. 目标 创建一个标签窗体视,使其包含几个对话框模板,如图 6 - 1所示。 2. 策略 从C S c r o l l Vi e w派生自己的视 类,在这个视类中简单地创建表单 控件窗口。 3. 步骤 1) 为标签窗体视创建页面 用Dialog Editor创建一个或者一 个以上的对话框模板,使之成为标 签视的页面。并将它们的风格设置 为细边框的子窗口。这些页面的窗 口标题将成为出现在其标签中的名 字。 使用Class Wi z a r d为这些页面创建类并将其从 C P r o p e r t y P a g e类派生。 2) 创建标签窗体视类 用Class Wi z a r d创建派生于C S c r o l l Vi e w的新类。 在该新类中嵌入属性表变量,如下所示: // WzdTa b b e d Vi e w. h p r i v a t e : CPropertySheet m_sheet; 在该类中再嵌入一个按钮控件,它将成为 A p p l y按钮: 图6-1 标签窗体视 视中的属 性页CButton m_button; 属性表一般创建自己的 A p p l y按钮,但这仅在创建有模式对话框的情况下。因此必须动态 地为该视类增加自己的A p p l y按钮。 用Class Wi z a r d为该类添加W M _ C R E AT E消息处理函数。在此创建该属性表和 A p p l y按钮, 并将先前创建的属性页类添加到该属性表之中: int CWzdTa b b e d View::OnCreate( LPCREATESTRUCT lpCreateStruct ) { if ( CScrollView::OnCreate( lpCreateStruct ) == -1 ) return -1; // create property sheet m_sheet.AddPage( &m_pageOne ); m_sheet.AddPage( &m_pageTwo ); m_sheet.Create( this,WS_CHILD|WS_VISIBLE ); // create apply button CRect rect ( 0,0,10,10 ); CFont *pFont = CFont::FromHandle( ( HFONT )::GetStockObject( ANSI_VAR_FONT ) ); m_button.Create( "&Apply", WS_VISIBLE | WS_CHILD, rect, this, I D C _ W Z D _ A P P LY ); m_button.SetFont( pFont ); return 0; } 注意,现在不必关心这些控件放在哪儿或者有多大,以后将移动它们。 用Class Wi z a r d重载该新Vi e w类的O n I n i t U p d a t e ( )函数。首先移动属性表和 A p p l y按钮并且 改变其大小。然后用 C S c r o l l Vi e w : : R e s i z e P a r e n t To F i t ( )函数缩小包围它们的视,在此首先用 S e t S c r o l l S i z e ( )函数告诉C S c o l l Vi e w视的大小: void CWzdTa b b e d Vi e w : : O n I n i t i a l U p d a t e ( ) { C S c r o l l Vi e w : : O n I n i t i a l U p d a t e ( ) ; // home the property sheet CRect rect; m_sheet.GetClientRect( &rect ); m _ s h e e t . M o v e Window( rect ); rect.bottom += BUTTON_HEIGTH + 10; // move apply button into place CRect brect( rect.right-BUTTO N _ W I D T H - 5 , r e c t . b o t t o m - B U T TO N _ H E I G T H - 5 , r e c t . r i g h t - 5 , rect.bottom-5 ); m _ b u t t o n . M o v e Window( brect ); // size child frame around property sheet SIZE size = { rect.Width(), rect.Height() }; 144第第第二部分第用户界面实例 下载SetScrollSizes( MM_TEXT, size ); R e s i z e P a r e n t ToFit( FALSE ); // make sure the scroll bars are gone SetScrollSizes( MM_TEXT, CSize( 20,20 ) ); // copy document into property pages m_pageOne.m_bWzd1 = ( ( CWzdDoc* )GetDocument() ) -> m_bWzd1; m _ p a g e Two.m_sEdit = ( ( CWzdDoc* )GetDocument() ) -> m_sEdit; } 最后要完成的工作是用文档中的值初始化属性页的值,这将涉及许多其他的工作。 由于属性表和按钮控件并不覆盖所有的视,所以需要绘制视的背景。为此,用 C l a s s Wi z a r d为该类添加 W M _ E R A S E B K G N D消息处理函数,并用系统按钮的表面颜色如下绘制 背景: BOOL CWzdTa b b e d View::OnEraseBkgnd(CDC* pDC) { CPen pen( PS_SOLID, 0, GetSysColor( COLOR_BTNFACE ) ); CPen *pPen = pDC -> SelectObject( &pen ); CBrush brush( GetSysColor( COLOR_BTNFACE ) ); CBrush *pBrush = pDC -> SelectObject( &brush ); CRect rect; GetClientRect( &rect ); pDC -> Rectangle( rect ); pDC -> SelectObject( pPen ); pDC -> SelectObject( pBrush ); return TRUE; } 为A p p l y按钮添加消息处理函数。这可以通过手工来完成,或者使用与按钮相同的命令标 识符创建A p p l y菜单选项,然后用 C l a s s Wi z a r d自动地为自己的视类增加消息处理函数,并通 知属性表更新其页面。即使属性表以无模式对话框的方式使用 (在这种情况下没有A p p l y按钮), 也可以通过C P r o p e r t y S h e e t的P r e s s B u t t o n ( )函数按照下述方法访问该按钮的功能: void CWzdTa b b e d Vi e w : : O n W z d A p p l y ( ) { m_sheet.PressButton( PSBTN_APPLYNOW ); ( ( CWzdDoc* )GetDocument() ) -> m_bWzd1 = m_pageOne.m_bWzd1; ( ( CWzdDoc* )GetDocument() ) -> m_sEdit = m_pageTw o . m _ s E d i t ; } 最后要完成的工作是从属性页中将变量值拷贝到文档,这里也有许多其他工作要做。 参考程序清单— 标签窗体视类可查看该类的完整代码列表。 3) 实现新标签窗体视类 在应用程序类的I n i t I n s t a n c e ( )函数中替换新类,如下所示: C M u l t i D o c Template* pDocTe m p l a t e ; p D o c Template = new CMultiDocTe m p l a t e ( 第 6 章第视第第145下载I D R _ W Z D T Y P E , RUNTIME_CLASS( CWzdDoc ), RUNTIME_CLASS( CChildFrame ), RUNTIME_CLASS( CWzdTa b b e d View ) ); <<<<<<<< ADD HERE A d d D o c Template( pDocTemplate ); 可以删除由App Wi z a r d所创建的任何视类。 为了避免用户重新设置视的大小,需要修改 M a i n F r a m e或者C h i l d F r a m e类的P r e C r e a t e Wi n d o w ( )函数: BOOL CChildFrame::PreCreateWindow( CREATESTRUCT& cs ) { // removes min/max boxes cs.style &= ~( WS_MAXIMIZEBOX|WS_MINIMIZEBOX ); // makes dialog box unsizable cs.style &= ~WS_THICKFRAME; return CMDIChildWnd::PreCreateWindow( cs ); } 4. 注意 为什么要从 C S c r o l l Vi e w类而不是从 C Vi e w类派生新类呢?因为这样做可以使用 C S c r o l l View:: ResizeParentTo F i t ( )函数来缩小包围属性表和按钮的视。 5. 使用光盘时注意 执行随书附带光盘中的工程,可以注意到视实际上是包含属性页的属性表。 6. 程序清单——标签窗体视类 #if !defined(AFX_WZDTA B B E D V I E W _ H _ _ 9 A 0 B 9 5 0 4 _ E 0 4 3 _ 11 D 1 _ 9 B 7 7 _ 0 0 A A 0 0 3 D 8 6 9 5 _ _ I N C L U D E D _ ) #define AFX_WZDTA B B E D V I E W _ H _ _ 9 A 0 B 9 5 0 4 _ E 0 4 3 _ 11 D 1 _ 9 B 7 7 _ 0 0 A A 0 0 3 D 8 6 9 5 _ _ I N C L U D E D _ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 // WzdTa b b e d Vi e w.h : header file / / #include "PageOne.h" #include "PageTw o . h " / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTa b b e d View view class CWzdTa b b e d View : public CScrollVi e w { p r o t e c t e d : C W z d Ta b b e d View(); // protected constructor used by dynamic creation D E C L A R E _ D Y N C R E ATE( CWzdTa b b e d View ) // Attributes p u b l i c : 146第第第二部分第用户界面实例 下载// Operations p u b l i c : // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CWzdTa b b e d View ) p r o t e c t e d : virtual void OnDraw( CDC* pDC ); // overridden to draw this view virtual void OnInitialUpdate(); // first time after construct // }}AFX_VIRT U A L // Implementation p r o t e c t e d : virtual ~CWzdTa b b e d Vi e w ( ) ; #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump( CDumpContext& dc ) const; # e n d i f // Generated message map functions // {{AFX_MSG(CWzdTa b b e d Vi e w ) afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnWzdApply(); afx_msg BOOL OnEraseBkgnd(CDC* pDC); // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : CButton m_button; CPageOne m_pageOne; C P a g e Two m_pageTw o ; CPropertySheet m_sheet; } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Developer Studio will insert additional declarations immediately // before the previous line. # e n d i f / / ! d e f i n e d ( A F X _ W Z D TA B B E D V I E W _ H _ 9 A 0 B 9 5 0 4 _ E 0 4 3 _ 11 D 1 _ 9 B 7 7 _ 0 0 A A 0 0 3 D 8 6 9 5 _ _ I N C L U D E D _ ) // WzdTa b b e d Vi e w.cpp : implementation file / / #include "stdafx.h" #include "wzd.h" #include "WzdDoc.h" #include "WzdTa b b e d Vi e w. h " #ifdef _DEBUG 第 6 章第视第第147下载#define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f #define BUTTON_WIDTH 40 #define BUTTON_HEIGTH 25 / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTa b b e d Vi e w I M P L E M E N T _ D Y N C R E ATE( CWzdTa b b e d Vi e w, CScrollView ) C W z d Ta b b e d Vi e w : : C W z d Ta b b e d Vi e w ( ) { } C W z d Ta b b e d Vi e w : : ~ C W z d Ta b b e d Vi e w ( ) { } BEGIN_MESSAGE_MAP( CWzdTa b b e d Vi e w, CScrollView ) // {{AFX_MSG_MAP( CWzdTa b b e d View ) O N _ W M _ C R E AT E ( ) ON_COMMAND( IDC_WZD_APPLY, OnWzdApply ) O N _ W M _ E R A S E B K G N D ( ) // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTa b b e d View drawing void CWzdTa b b e d Vi e w : : O n I n i t i a l U p d a t e ( ) { C S c r o l l Vi e w : : O n I n i t i a l U p d a t e ( ) ; // home the property sheet CRect rect; m_sheet.GetClientRect( &rect ); m _ s h e e t . M o v e Window( rect ); rect.bottom += BUTTON_HEIGTH + 10; // move apply button into place CRect brect( rect.right-BUTTO N _ W I D T H - 5 , r e c t . b o t t o m - B U T TO N _ H E I G T H - 5 , r e c t . r i g h t - 5 , r e c t . b o t t o m - 5 ) ; m _ b u t t o n . M o v e Window( brect ); // size child frame around property sheet 148第第第二部分第用户界面实例 下载SIZE size = {rect.Width(), rect.Height()}; SetScrollSizes( MM_TEXT, size ); R e s i z e P a r e n t ToFit( FALSE ); // make sure the scroll bars are gone SetScrollSizes( MM_TEXT, CSize( 20,20 ) ); // copy document into property pages m_pageOne.m_bWzd1 = ( ( CWzdDoc* )GetDocument() ) -> m_bWzd1; m _ p a g e Two.m_sEdit = ( ( CWzdDoc* )GetDocument() ) -> m_sEdit; } void CWzdTa b b e d View::OnDraw( CDC* pDC ) { CDocument* pDoc = GetDocument(); // TODO: add draw code here } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTa b b e d View diagnostics #ifdef _DEBUG void CWzdTa b b e d Vi e w : : A s s e r t Valid() const { C S c r o l l Vi e w : : A s s e r t Va l i d ( ) ; } void CWzdTa b b e d View::Dump(CDumpContext& dc) const { C S c r o l l View::Dump( dc ); } #endif //_DEBUG / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTa b b e d View message handlers int CWzdTa b b e d View::OnCreate( LPCREATESTRUCT lpCreateStruct ) { if ( CScrollView::OnCreate( lpCreateStruct ) == -1 ) return -1; // create property sheet m_sheet.AddPage( &m_pageOne ); m_sheet.AddPage( &m_pageTwo ); m_sheet.Create( this,WS_CHILD|WS_VISIBLE ); // create apply button CRect rect ( 0,0,10,10 ); CFont *pFont = CFont::FromHandle( ( HFONT )::GetStockObject( ANSI_VAR_FONT ) ); m_button.Create( “&Apply”, WS_VISIBLE | WS_CHILD, rect, this, IDC_WZD_APPLY ); 第 6 章第视第第149下载m_button.SetFont( pFont ); return 0; } void CWzdTa b b e d View::OnWzdApply() { m_sheet.PressButton( PSBTN_APPLYNOW ); ( ( CWzdDoc* )GetDocument() ) -> m_bWzd1 = m_pageOne.m_bWzd1; ( ( CWzdDoc* )GetDocument() ) -> m_sEdit = m_pageTw o . m _ s E d i t ; } BOOL CWzdTa b b e d View::OnEraseBkgnd( CDC* pDC ) { CPen pen( PS_SOLID,0,GetSysColor( COLOR_BTNFACE ) ); CPen *pPen = pDC -> SelectObject( &pen ); CBrush brush( GetSysColor( COLOR_BTNFACE ) ); CBrush *pBrush = pDC -> SelectObject( &brush ); CRect rect; GetClientRect( &rect ); pDC -> Rectangle( rect ); pDC -> SelectObject( pPen ); pDC -> SelectObject( pBrush ); return TRUE; } 6.2 实例16:创建具有通用控件的视 1. 目标 创建具有任何通用控件的视。本 实例将创建具有图 6 - 2所示简单组合 框的视。 2. 策略 本例将在标准的 C Vi e w类中嵌入 通 用 控 件 的 M F C 类 ( 本 例 使 用 C C o m b o B o x),并添加两个消息处理 函 数 以 迫 使 该 控 件 窗 口 占 据 视 。 W M _ C R E AT E消息处理函数将创建 该控件窗口。 W M _ S I Z E消息处理函 数则扩展该控件窗口以填充视,然后重载 O n I n i t i a l U p d a t e ( )和O n U p d a t e ( )函数为控件赋予文 档值。 3. 步骤 1) 创建组合框视 使用A p p Wi z a r d按照通常方式创建应用程序,并以缺省的 C Vi e w类创建自己的视类。 150第第第二部分第用户界面实例 下载 图6-2 简单组合框的视 该视具有一个 简单组合框, 其中既有编辑 框又有列表框在视类中嵌入组合框: CComboBox m_combobox; 用C l a s s Wi z a r d添加一个W M _ C R E ATE 消息处理函数到自己的视类中,并按照下列风格创 建简单的组合框: int CWzdView::OnCreate( LPCREATESTRUCT lpCreateStruct ) { if ( CView::OnCreate( lpCreateStruct ) = -1 ) return -1; CRect rect( 0,0,0,0 ); m_combobox.Create( WS_CHILD| WS_VISIBLE| CBS_SIMPLE| CBS_NOINTEGRALHEIGHT | W S _ V S C R O L L , rect, this, IDC_WZD_COMBOBOX ); return 0; } 要转变为视的任何控件窗口都应该具有 W S _ C H I L D和W S _ V I S I B L E风格设置。其他风格 则随着控件的不同而不同。 由于简单组合框的列表框总是可见的,因此使用 C B S _ S I M P L E风格创建简单的组合框。 否则,视将总是一个具有下拉按钮的编辑框。 用C L a s s Wi z a r d为视类添加W M _ S I Z E消息处理函数。用C W n d : : M o v e Wi n d o w ( )函数来扩展 控件的大小以填充视: void CWzdView::OnSize( UINT nType, int cx, int cy ) { C View::OnSize( nType, cx, cy ); m _ c o m b o b o x . M o v e Window( 0,0,cx,cy ); } 2) 更新组合框视 用C l a s s Wi z a r d重载C Vi e w的I n I n i t i a l U p d a t e ( )和O n U p d a t e ( )成员函数。将用文档中的数据 填充控件: void CWzdVi e w : : O n I n i t i a l U p d a t e ( ) { C Vi e w : : O n I n i t i a l U p d a t e ( ) ; // get initial data from document m_combobox.AddString( "Monday" ); m_combobox.AddString( "Wednesday" ); m_combobox.AddString( "Friday" ); } void CWzdView::OnUpdate( CView* pSender, LPARAM lHint, CObject* pHint ) { // in response to UpdateAllViews from document } 第 6 章第视第第151下载为控件通知消息手工添加处理函数(例如,通知视列表框的选择项发生变化或者用户在编 辑框中进行了击键操作等)。确定应该加入内容的简单方法是创建对话框然后为它增加一个简 单的组合框,用 C l a s s Wi z a r d创建对话框类并添加视类需要的处理函数到该对话框类。最后只 需从中剪切或者粘贴所需要的消息宏。 参考本实例结尾的程序清单—组合框视以了解该类的全部源代码。 4. 注意 本实例的一个改进是用两个或者两个以上的控件窗口创建一个视。举例说,可以在顶部 创建一个静态控件窗口以显示某些静态文本,在底部则创建一个编辑框来显示某些数据。通 过在视类中嵌入这两种控件类可以做到这一点,它们都由 W M _ C R E AT E消息处理函数创建, 然后在W M _ S I Z E消息处理函数中决定每个控件窗口得到多少视区域。 5. 使用光盘时注意 执行随书附带的光盘中的工程时,可以注意到视由一个简单的组合框填充。 6. 程序清单— 组合框视类 // WzdVi e w.h : interface of the CWzdView class / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #if !defined( AFX_WZDVIEW_H__CA9038F0_B0DF_11 D 1 _ A 1 8 C _ D C B 3 C 8 5 E B D 3 4 _ _ I N C L U D E D _ ) #define AFX_WZDVIEW_H__CA9038F0_B0DF_11 D 1 _ A 1 8 C _ D C B 3 C 8 5 E B D 3 4 _ _ I N C L U D E D _ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 class CWzdView : public CVi e w { protected: // create from serialization only C W z d Vi e w ( ) ; D E C L A R E _ D Y N C R E ATE( CWzdView ) // Attributes p u b l i c : CWzdDoc* GetDocument(); // Operations p u b l i c : // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CWzdView ) p u b l i c : virtual void OnDraw( CDC* pDC ); // overridden to draw this view virtual BOOL PreCreateWindow( CREATESTRUCT& cs ); virtual void OnInitialUpdate(); p r o t e c t e d : virtual BOOL OnPreparePrinting( CPrintInfo* pInfo ); virtual void OnBeginPrinting( CDC* pDC, CPrintInfo* pInfo ); 152第第第二部分第用户界面实例 下载virtual void OnEndPrinting( CDC* pDC, CPrintInfo* pInfo ); virtual void OnUpdate( CView* pSender, LPARAM lHint, CObject* pHint ); // }}AFX_VIRT U A L // Implementation p u b l i c : virtual ~CWzdVi e w ( ) ; #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump( CDumpContext& dc ) const; # e n d i f p r o t e c t e d : // Generated message map functions p r o t e c t e d : // {{AFX_MSG( CWzdView ) afx_msg int OnCreate( LPCREATESTRUCT lpCreateStruct ); afx_msg void OnSize( UINT nType, int cx, int cy ); // }}AFX_MSG afx_msg void OnEditchangeCombo(); afx_msg void OnEditupdateCombo(); afx_msg void OnSelchangeCombo(); D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : CComboBox m_combobox; } ; #ifndef _DEBUG // debug version in WzdVi e w. c p p inline CWzdDoc* CWzdVi e w : : G e t D o c u m e n t ( ) { return ( CWzdDoc* )m_pDocument; } # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Developer Studio will insert additional declarations immediately // before the previous line. # e n d i f // !defined( AFX_WZDVIEW_H__CA9038F0_B0DF_11D1_A18C_DCB3C85EBD34__INCLUDED_ ) // WzdVi e w.cpp : implementation of the CWzdView class / / #include "stdafx.h" #include "Wzd.h" #include "WzdDoc.h" #include "WzdVi e w. h " #ifdef _DEBUG 第 6 章第视第第153下载#define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdVi e w I M P L E M E N T _ D Y N C R E ATE( CWzdVi e w, CView ) BEGIN_MESSAGE_MAP( CWzdVi e w, CView ) // {{AFX_MSG_MAP( CWzdView ) O N _ W M _ C R E AT E ( ) O N _ W M _ S I Z E ( ) // }}AFX_MSG_MAP ON_CBN_EDITCHANGE( IDC_WZD_COMBOBOX, OnEditchangeCombo ) O N _ C B N _ E D I T U P D ATE( IDC_WZD_COMBOBOX, OnEditupdateCombo ) ON_CBN_SELCHANGE( IDC_WZD_COMBOBOX, OnSelchangeCombo ) // Standard printing commands ON_COMMAND( ID_FILE_PRINT, CView::OnFilePrint ) ON_COMMAND( ID_FILE_PRINT_DIRECT, CView::OnFilePrint ) ON_COMMAND( ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview ) E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdView construction/destruction C W z d Vi e w : : C W z d Vi e w ( ) { // TODO: add construction code here } C W z d Vi e w : : ~ C W z d Vi e w ( ) { } BOOL CWzdVi e w : : P r e C r e a t e Window( CREATESTRUCT& cs ) { // TODO: Modify the Window class or styles here by modifying // the CREATESTRUCT cs return CVi e w : : P r e C r e a t e Window( cs ); } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdView drawing void CWzdView::OnDraw( CDC* pDC ) { CWzdDoc* pDoc = GetDocument(); 154第第第二部分第用户界面实例 下载A S S E RT _ VALID( pDoc ); // TODO: add draw code for native data here } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdView printing BOOL CWzdView::OnPreparePrinting( CPrintInfo* pInfo ) { // default preparation return DoPreparePrinting( pInfo ); } void CWzdView::OnBeginPrinting( CDC* /*pDC*/, CPrintInfo* /*pInfo*/ ) { // TODO: add extra initialization before printing } void CWzdView::OnEndPrinting( CDC* /*pDC*/, CPrintInfo* /*pInfo*/ ) { // TODO: add cleanup after printing } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdView diagnostics #ifdef _DEBUG void CWzdVi e w : : A s s e r t Valid() const { C Vi e w : : A s s e r t Va l i d ( ) ; } void CWzdView::Dump( CDumpContext& dc ) const { C View::Dump( dc ); } CWzdDoc* CWzdView::GetDocument() // non-debug version is inline { A S S E RT( m_pDocument -> IsKindOf( RUNTIME_CLASS( CWzdDoc ) ) ); return ( CWzdDoc* )m_pDocument; } #endif //_DEBUG / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdView message handlers int CWzdView::OnCreate( LPCREATESTRUCT lpCreateStruct ) { if ( CView::OnCreate( lpCreateStruct ) == -1 ) 第 6 章第视第第155下载return -1; CRect rect( 0,0,0,0 ); m_combobox.Create( WS_CHILD|WS_VISIBLE|CBS_SIMPLE|CBS_NOINTEGRALHEIGHT|WS_ VSCROLL, rect, this, IDC_WZD_COMBOBOX ); return 0; } void CWzdView::OnSize( UINT nType, int cx, int cy ) { C View::OnSize( nType, cx, cy ); m _ c o m b o b o x . M o v e Window( 0,0,cx,cy ); } void CWzdVi e w : : O n I n i t i a l U p d a t e ( ) { C Vi e w : : O n I n i t i a l U p d a t e ( ) ; // get initial data from document m_combobox.AddString( “Monday” ); m_combobox.AddString( “Wednesday” ); m_combobox.AddString( “Friday” ); } void CWzdView::OnUpdate( CView* pSender, LPARAM lHint, CObject* pHint ) { // in response to UpdateAllViews from document } void CWzdVi e w : : O n E d i t c h a n g e C o m b o ( ) { // TODO: Add your control notification handler code here } void CWzdVi e w : : O n E d i t u p d a t e C o m b o ( ) { // TODO: Add your control notification handler code here } void CWzdVi e w : : O n S e l c h a n g e C o m b o ( ) { // TODO: Add your control notification handler code here } 6.3 实例17:打印报表 1. 目标 156第第第二部分第用户界面实例 下载如图6 - 3所示打印一个文档中的报表信息。 图6-3 打印报表预览 2. 策略 M F C应用程序中打印报表习惯上在视类中实现,可以认为这是查看文档的另一方式。 F i l e / P r i n t和File/Print Preview菜单命令由视类自动地处理。为了充分利用这个自动功能则必须 重载两个视类函数,它们是 O n B e g i n P r i n t ( ),负责打印前初始化打印变量,以及 O n P r i n t ( ),该 函数在每次新的页面需要被打印时即被调用。在此将遍历文档的数据集合并将其绘制到打印 设备环境。本实例中还将提示用户要打印的报表类型。 3. 步骤 1) 提示用户要打印的报表类型 编写一个小型的帮助函数可以提示用户要打印的报表类型,将该函数添加到视类中。下 面的实例简单地创建一个对话框询问用户想要一个简短的还是一个较长的报表。实际编写时 可以询问更多的细节: BOOL CWzdVi e w : : G e t R e p o r t O p t i o n s ( ) { CWzdDialog dlg; d l g . m _ n R e p o r t Type = m_nReportTy p e ; if( dlg.DoModal() == IDOK ) { m _ n R e p o r t Type = dlg.m_nReportTy p e ; return TRUE; } return FA L S E ; } 最好在用户单击了P r i n t或者Print Preview菜单按钮后用对话框提示用户。但是,如果要直 接截获这些命令( I D _ F I L E _ P R I N T和I D _ F I L E _ P R I N G T _ P R E V I E W)并在对话框内放置其消息 处理函数,那么在用户预览打印页的时候可能会出问题。当用户使用预览页面上的打印按钮 直接打印该页面时,将会发出另一个 I D _ F I L E _ P R I N T命令而再次打开该提示对话框。为了避 免出现这样的问题,在发送出 I D C _ F I L E _ P R I N T和I D C _ F I L E _ P R I N T _ P R E V I E W消息前必须 修改主菜单并发送两个新的命令消息( I D C _ F I L E _ P R I N T和I D C _ F I L E _ P R I N T _ P R E V I E W)以提 第 6 章第视第第157下载 从自己的文档中创建一个 可在打印预览中看到并能 够打印到打印机的报表示用户报表类型。 用菜单编辑器 (Menu Editor)分别改变 F i l e / P r i n t和File/Print Preview的命令标识符为 I D C _ F I L E _ P R I N T和I D C _ F I L E _ P R I N T _ P R E V I E W。 用 C l a s s Wi z a r d 为 视类增 加这两 个消息 的命令 消息处 理函数 。从中 可以调用 G e t R e p o r t O p t i o n s ( )函数,它将打开对报表类型的提示对话框,然后生成使得视类进入打印模 式的命令标识符: void CWzdVi e w : : O n F i l e P r i n t ( ) { if ( GetReportOptions() ) { AfxGetMainWnd() -> SendMessage( WM_COMMAND, ID_FILE_PRINT ); } } void CWzdVi e w : : O n F i l e P r i n t P r e v i e w ( ) { if ( GetReportOptions() ) { AfxGetMainWnd() -> SendMessage( WM_COMMAND, ID_FILE_PRINT_PREVIEW ); } } G e t R e p o r t O p t i o n s ( )函数的代码可以在程序清单—视类中找到。 2) 初始化视类以供打印 填充视类的O n B e g i n P r i n t ( )成员函数。由于A p p Wi z a r d已经添加了这个函数,因此不再需 要Class Wi z a r d。在打印开始之前将调用一次 O n B e g i n P r i n t ( )。该函数将计算某些供打印需要 的数值,包括平均打印字符大小、打印行允许字符数目、每页可能行数以及报表所需要的整 个打印页数等: void CWzdView::OnBeginPrinting( CDC* pDC, CPrintInfo* pInfo ) { // get printed character height and width TEXTMETRIC tm; pDC -> GetTextMetrics( &tm ); m_nPrintCharHeight = tm.tmHeight; m _ n P r i n t C h a r Width = tm.tmAv e C h a r Wi d t h ; // get number of characters per line int nPageWidth = pDC -> GetDeviceCaps( HORZRES ); m _ n P a g e Width = nPageWi d t h / m _ n P r i n t C h a r Wi d t h ; // get number of lines per page (with and w/o title on each page) int nPageHeight = pDC -> GetDeviceCaps( VERTRES ); int nPrintLinesPerPage = nPageHeight / m_nPrintCharHeight; m_nPrintableLinesPerPage = n P r i n t L i n e s P e r P a g e - L I N E S _ I N _ R E P O RT _ T I T L E ; // determine number of total pages in this document 158第第第二部分第用户界面实例 下载int nLines = GetDocument() -> GetWzdInfoList() -> GetCount(); int nPages = ( nLines + m_nPrintableLinesPerPage - 1 )/ m _ n P r i n t a b l e L i n e s P e r P a g e ; if ( nPages <= 0 ) nPages = 1; pInfo -> SetMaxPage( nPages ); pInfo -> m_nCurPage = 1; } 3 ) 打印页面 用C l a s s Wi z a r d重载视类中的O n P r i n t ( )成员函数。一旦每次准备打印新的页面, OnPrint () 函数将由视类调用直到打印结束。因此 O n P r i n t ( )函数必须要做的一件事就是确定文档中页面 从哪儿开始。为了打印本实例中的报表, O n P r i n t ( )函数调用了3个辅助函数:P r i n t Ti t l e ( )打印 报表标题,P r i n t C o l u m n H e a d e r ( )打印报表的列头,而P r i n t L i n e ( )则打印每一行: void CWzdView::OnPrint( CDC* pDC, CPrintInfo* pInfo ) { // print title int y = 0; P r i n t Title( pDC,&y,pInfo ); // print column headers PrintColumnHeaders( pDC,&y ); // determine part of document to print on this page int nWzdInfoListInx = ( pInfo -> m_nCurPage-1 )*m_nPrintableLinesPerPage; int nWzdInfoListEnd = GetDocument() -> GetWzdInfoList() -> GetCount(); if ( nWzdInfoListEnd > nWzdInfoListInx + m_nPrintableLinesPerPage ) { nWzdInfoListEnd = nWzdInfoListInx + m_nPrintableLinesPerPage; } // print report lines for ( ; nWzdInfoListInx < nWzdInfoListEnd; nWzdInfoListInx++ ) { POSITION pos = GetDocument() -> GetWzdInfoList() -> FindIndex(nWzdInfoListInx); CWzdInfo *pInfo = GetDocument() -> GetWzdInfoList() -> GetAt( pos ); PrintLine( pDC,&y,pInfo ); y += m_nPrintCharHeight; } C View::OnPrint( pDC, pInfo ); } 将这 3个辅助函数: P r i n t Ti t l e ( )、P r i n t C o l u m n H e a d e r ( )和P r i n t L i n e ( )添加到视类中。 P r i n t Ti t l e ( )将使用C D C类的Te x t O u t成员函数打印标题。P r i n t C o l u m n H e a d e r ( )和P r i n t L i n e ( )则用 Ta b b e d Te x t O u t ( )函数自动地创建各列。作为这些辅助函数的例子,请参考下面的程序清单 第 6 章第视第第159下载— 视类。 4. 注意 如果视包含了图像而不是列表或者其他文本,那就不需要为视类增加任何东西来打印该 视。这是因为C Vi e w类将调用其O n D r a w ( )成员函数,而该函数具有其打印设备环境而不是屏 幕设备环境,这样系统自动地将视绘制到打印机。 为了创建其他视类类型的报表,请参考实例 1 8。 5. 使用光盘时注意 执行光盘中的该工程时,单击 F i l e,然后单击是P r i n t或者Print Preview菜单命令,将出现 一个对话框询问用户是否想打印短的还是长的报表。选择一项并按下 O K按钮,可以注意到一 份报表被打印到了纸上或者屏幕上。 6. 程序清单— 视类 // WzdVi e w.h : interface of the CWzdView class / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #if !defined( AFX_WZDVIEW_H__CA9038F0_B0DF_11D1_A18C_DCB3C85EBD34__INCLUDED_ ) #define AFX_WZDVIEW_H__CA9038F0_B0DF_11 D 1 _ A 1 8 C _ D C B 3 C 8 5 E B D 3 4 _ _ I N C L U D E D _ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 class CWzdView : public CVi e w { protected: // create from serialization only C W z d Vi e w ( ) ; D E C L A R E _ D Y N C R E ATE( CWzdView ) // Attributes p u b l i c : CWzdDoc* GetDocument(); enum { S H O RT R E P O RT, L O N G R E P O RT } ; // Operations p u b l i c : // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CWzdView ) p u b l i c : virtual void OnDraw( CDC* pDC ); // overridden to draw this view virtual BOOL PreCreateWindow( CREATESTRUCT& cs ); p r o t e c t e d : virtual BOOL OnPreparePrinting( CPrintInfo* pInfo ); 160第第第二部分第用户界面实例 下载virtual void OnBeginPrinting( CDC* pDC, CPrintInfo* pInfo ); virtual void OnEndPrinting( CDC* pDC, CPrintInfo* pInfo ); virtual void OnPrint( CDC* pDC, CPrintInfo* pInfo ); // }}AFX_VIRT U A L // Implementation p u b l i c : virtual ~CWzdVi e w ( ) ; #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump( CDumpContext& dc ) const; # e n d i f p r o t e c t e d : // Generated message map functions p r o t e c t e d : // {{AFX_MSG(CWzdVi e w ) afx_msg void OnFilePrint(); afx_msg void OnFilePrintPreview(); // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : int m_nPrintCharHeight; int m_nPrintCharWi d t h ; int m_nPageWi d t h ; int m_nPrintableLinesPerPage; int m_nReportTy p e ; static int m_reportTa b s [ ] ; BOOL GetReportOptions(); void PrintTitle( CDC *pDC, int *y, CPrintInfo* pInfo ); void PrintColumnHeaders( CDC *pDC, int *y ); void PrintLine( CDC *pDC, int *y, CWzdInfo *pInfo ); } ; #ifndef _DEBUG // debug version in WzdVi e w. c p p inline CWzdDoc* CWzdVi e w : : G e t D o c u m e n t ( ) { return ( CWzdDoc* )m_pDocument; } # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Developer Studio will insert additional declarations immediately // before the previous line. # e n d i f // !defined( AFX_WZDVIEW_H__CA9038F0_B0DF_11D1_A18C_DCB3C85EBD34__INCLUDED_ ) // WzdVi e w.cpp : implementation of the CWzdView class 第 6 章第视第第161下载/ / #include "stdafx.h" #include "Wzd.h" #include "WzdDoc.h" #include "WzdVi e w. h " #include "WzdDialog.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f int CWzdVi e w : : m _ r e p o r t Tabs[] = {1200,2000}; #define NUM_TABS sizeof(m_reportTa b s ) / s i z e o f ( i n t ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdVi e w I M P L E M E N T _ D Y N C R E ATE( CWzdVi e w, CView ) BEGIN_MESSAGE_MAP( CWzdVi e w, CView ) // {{AFX_MSG_MAP( CWzdView ) ON_COMMAND( IDC_FILE_PRINT, OnFilePrint ) ON_COMMAND( IDC_FILE_PRINT_PREVIEW, OnFilePrintPreview ) // }}AFX_MSG_MAP // Standard printing commands ON_COMMAND( ID_FILE_PRINT, CView::OnFilePrint ) ON_COMMAND( ID_FILE_PRINT_DIRECT, CView::OnFilePrint ) ON_COMMAND( ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview ) E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdView construction/destruction C W z d Vi e w : : C W z d Vi e w ( ) { m _ n R e p o r t Type = 0; } C W z d Vi e w : : ~ C W z d Vi e w ( ) { } BOOL CWzdVi e w : : P r e C r e a t e Window( CREATESTRUCT& cs ) { // TODO: Modify the Window class or styles here by modifying 162第第第二部分第用户界面实例 下载// the CREATESTRUCT cs return CVi e w : : P r e C r e a t e Window( cs ); } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdView drawing void CWzdView::OnDraw( CDC* pDC ) { CWzdDoc* pDoc = GetDocument(); A S S E RT _ VALID( pDoc ); // TODO: add draw code for native data here } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdView printing #define LINES_IN_REPORT_TITLE 7 BOOL CWzdView::OnPreparePrinting( CPrintInfo* pInfo ) { // default preparation return DoPreparePrinting( pInfo ); } void CWzdView::OnBeginPrinting( CDC* pDC, CPrintInfo* pInfo ) { // get printed character height and width TEXTMETRIC tm; pDC -> GetTextMetrics( &tm ); m_nPrintCharHeight = tm.tmHeight; m _ n P r i n t C h a r Width = tm.tmAv e C h a r Wi d t h ; // get number of characters per line int nPageWidth = pDC -> GetDeviceCaps( HORZRES ); m _ n P a g e Width = nPageWi d t h / m _ n P r i n t C h a r Wi d t h ; // get number of lines per page (with and w/o title on each page) int nPageHeight = pDC -> GetDeviceCaps( VERTRES ); int nPrintLinesPerPage = nPageHeight / m_nPrintCharHeight; m_nPrintableLinesPerPage = nPrintLinesPerPage-LINES_IN_REPORT _ T I T L E ; // determine number of total pages in this document int nLines = GetDocument() -> GetWzdInfoList() -> GetCount(); int nPages = ( nLines + m_nPrintableLinesPerPage - 1 )/ m _ n P r i n t a b l e L i n e s P e r P a g e ; if ( nPages <= 0 ) nPages = 1; pInfo -> SetMaxPage( nPages ); pInfo -> m_nCurPage = 1; 第 6 章第视第第163下载} void CWzdView::OnEndPrinting( CDC* /*pDC*/, CPrintInfo* /*pInfo*/ ) { // TODO: add cleanup after printing } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdView diagnostics #ifdef _DEBUG void CWzdVi e w : : A s s e r t Valid() const { C Vi e w : : A s s e r t Va l i d ( ) ; } void CWzdView::Dump( CDumpContext& dc ) const { C View::Dump( dc ); } CWzdDoc* CWzdView::GetDocument() // non-debug version is inline { A S S E RT( m_pDocument -> IsKindOf( RUNTIME_CLASS( CWzdDoc ) ) ); return ( CWzdDoc* )m_pDocument; } #endif //_DEBUG / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdView message handlers void CWzdVi e w : : O n F i l e P r i n t ( ) { if ( GetReportOptions() ) { AfxGetMainWnd() -> SendMessage( WM_COMMAND, ID_FILE_PRINT ); } } void CWzdVi e w : : O n F i l e P r i n t P r e v i e w ( ) { if ( GetReportOptions() ) { AfxGetMainWnd() -> SendMessage( WM_COMMAND, ID_FILE_PRINT_PREVIEW ); } } BOOL CWzdVi e w : : G e t R e p o r t O p t i o n s ( ) { CWzdDialog dlg; d l g . m _ n R e p o r t Type = m_nReportTy p e ; 164第第第二部分第用户界面实例 下载if( dlg.DoModal() == IDOK ) { m _ n R e p o r t Type = dlg.m_nReportTy p e ; return TRUE; } return FA L S E ; } void CWzdView::OnPrint( CDC* pDC, CPrintInfo* pInfo ) { // print title int y = 0; P r i n t Title( pDC,&y,pInfo ); // print column headers PrintColumnHeaders( pDC,&y ); // determine part of document to print on this page int nWzdInfoListInx = ( pInfo -> m_nCurPage-1 )*m_nPrintableLinesPerPage; int nWzdInfoListEnd = GetDocument() -> GetWzdInfoList() -> GetCount(); if ( nWzdInfoListEnd > nWzdInfoListInx + m_nPrintableLinesPerPage ) { nWzdInfoListEnd = nWzdInfoListInx + m_nPrintableLinesPerPage; } // print report lines for ( ; nWzdInfoListInx < nWzdInfoListEnd; nWzdInfoListInx++ ) { POSITION pos = GetDocument() -> GetWzdInfoList() -> FindIndex( nWzdInfoListInx ); CWzdInfo *pInfo = GetDocument() -> GetWzdInfoList() -> GetAt( pos ); PrintLine( pDC,&y,pInfo ); y += m_nPrintCharHeight; } C View::OnPrint( pDC, pInfo ); } void CWzdVi e w : : P r i n t Title( CDC *pDC, int *y, CPrintInfo* pInfo ) { // title int x = pDC -> GetDeviceCaps( HORZRES ); pDC -> SetTextAlign( TA_CENTER ); pDC -> TextOut( x/2, *y, “WZD REPORT” ); // page # CString str; s t r.Format( “Page %d of %d”,pInfo -> m_nCurPage,pInfo -> GetMaxPage() ); pDC -> SetTextAlign( TA_RIGHT ); pDC -> TextOut( x, *y, str ); 第 6 章第视第第165下载// report type *y += m_nPrintCharHeight; pDC -> SetTextAlign( TA_CENTER ); switch ( m_nReportType ) { case CWzdVi e w : : S H O RT R E P O RT: str = "Short Report"; b r e a k ; case CWzdVi e w : : L O N G R E P O RT: str = "Long Report"; b r e a k ; } pDC -> TextOut( x/2, *y, str ); // date *y += m_nPrintCharHeight; C O l e D a t e Time dt( COleDateTi m e : : G e t C u r r e n t Time() ); pDC -> TextOut( x/2, *y, dt.Format( “%c” ) ); *y += m_nPrintCharHeight; *y += m_nPrintCharHeight; // leave space between title and column headers } void CWzdView::PrintColumnHeaders( CDC *pDC, int *y ) { CString str; switch ( m_nReportType ) { case CWzdVi e w : : S H O RT R E P O RT: str = “Group\tVe r s i o n ” ; b r e a k ; case CWzdVi e w : : L O N G R E P O RT: str = “Group\tComment\tVe r s i o n ” ; b r e a k ; } pDC -> SetTextAlign( TA_LEFT ); pDC -> Ta b b e d TextOut( 0,*y, s t r, N U M _ TA B S , m _ r e p o r t Tabs,0 ); *y += m_nPrintCharHeight; *y += m_nPrintCharHeight; // leave space between column headers and report } void CWzdView::PrintLine( CDC *pDC, int *y, CWzdInfo *pInfo ) { CString str; switch ( m_nReportType ) { case CWzdVi e w : : S H O RT R E P O RT: 166第第第二部分第用户界面实例 下载s t r.Format( "%s\t%d", pInfo -> m_sGroupName, pInfo -> m_nVersion ); b r e a k ; case CWzdVi e w : : L O N G R E P O RT: s t r.Format( "%s\t%s\t%d",pInfo -> m_sGroupName, pInfo -> m_sComment, pInfo -> m_nVersion ); b r e a k ; } pDC -> SetTextAlign( TA_LEFT ); pDC -> Ta b b e d TextOut( 0,*y, s t r, N U M _ TA B S , m _ r e p o r t Tabs,0 ); *y += m_nPrintCharHeight; } 6.4 实例18: 打印视 1. 目标 动态地抓取应用程序的屏幕图像并将其打印。 2. 策略 上一个实例打印的是文档的报表,而不是当前出现在屏幕上的内容。在本例中,将实现 屏 幕 抓 取 并 打 印 其 内 容 。 M F C 为 打 印 视 提 供 了 有 限 的 支 持 , 但 只 要 用 M F C 在 C Vi e w : : O n D r a w ( )中所提供的设备环境,就可以绘制自己的视。在打印视的时候, M F C只需调 用具备打印机设备环境的 O n D r a w ( )函数即可。但是,如果不绘制自己的视,例如,用一个或 者一个以上的控件窗口填充自己的视就不会打印任何东西。每个控件将使用自己的设备环境 将自己打印到屏幕,因此打印视的唯一途径就是抓取屏幕(拷贝其内容到一个位图对象)并将其 打印到打印机。由于该功能整个与位图相关,因此将该功能封装到了位图类中。 3. 步骤 1) 创建新的位图类 用C l a s s Wi z a r d创建一个派生自C B i t m a p的新类。 为该类添加在其他实例中将提到的两个函数。 C a p t u r e ( )函数用于拷贝屏幕某区域的内容 到一个位图对象,如实例 3 3所示的那样。C r e a t e D I B ( )函数则用于将一个设备位图转变成设备 独立位图(D I B),在实例3 4中将回顾该函数。 最后为新类增加的函数是 P r i n t ( )函数,在调用该函数之前,首先假设视已经被 C a p t u r e ( )函 数抓取到了位图中,为其提供了打印机设备环境之后 P r i n t ( )函数就会将所包含的该位图打印到 打印机: void CWzdBitmap::Print( CDC *pDC ) { // get DIB version of bitmap int bmData; HANDLE hDIB = CreateDIB( &bmData ); 现在可以将该D I B位图拷贝到打印机设备环境中。由于打印机一般有比屏幕高得多的分辨 率,因此需要扩展位图以填充空白。接下来就确定需要填充的打印机区域有多大: // stretch bitmap to fill printed page with 1/4 inch borders int cxBorder = pDC -> GetDeviceCaps(LOGPIXELSX)/4; 第 6 章第视第第167下载int cyBorder = pDC -> GetDeviceCaps(LOGPIXELSY)/4; int cxPage = pDC -> GetDeviceCaps(HORZRES) - (cxBorder*2); int cyPage = ( int )( ( ( double )cxPage/ ( double )m_nWidth ) * ( double )m_nHeight ); 使用S t r e t c h D I B i t s ( )函数在打印机设备环境中将位图伸展。 S t r e t c h D I B i t s ( )函数需要指向 D I B位图的两个指针:一个指向头,另一个指向数据: LPBITMAPINFOHEADER lpDIBHdr = ( LPBITMAPINFOHEADER )::GlobalLock( hDIB ); LPSTR lpDIBBits = ( LPSTR )lpDIBHdr+bmData; 最后完成扩展和清除工作: // stretch the bitmap for the best fit on the printed page pDC -> SetStretchBltMode( COLORONCOLOR ); ::StretchDIBits( pDC -> m_hDC, c x B o r d e r, c y B o r d e r, c x P a g e , c y P a g e , // destination dimensions 0 , 0 , m _ n Wi d t h , m _ n H e i g h t , // source bitmap dimensions (use all of bitmap) l p D I B B i t s , // bitmap picture data ( LPBITMAPINFO )lpDIBHdr, // bitmap header info D I B _ R G B _ C O L O R S , // specify color table has // RGB values S R C C O P Y // simple source to destination copy ) ; // cleanup ::GlobalUnlock( hDIB ); ::GlobalFree( hDIB ); r e t u r n ; } 参考本实例结尾的程序清单—位图类可以查看该类的完整列表。 可以用该类随意地捕获屏幕并将其打印到打印机。但更好的办法是将该类加入视类,这 样用户一单击F i l e / P r i n t或者File/Print Preview命令,视就被打印到打印机,上接下来就实现该 方式。 2) 实现新的位图类 在视类中嵌入该新类: CWzdBitmap m_bitmap; 注释掉缺省的 I D _ F I L E _ P R I N T和I D _ F I L E _ P R I N T _ P R E V I E W命令处理函数并使用 C l a s s Wi z a r d来增加自己的命令处理函数。 BEGIN_MESSAGE_MAP( CWzdVi e w, CView ) // {{AFX_MSG_MAP( CWzdView ) ON_COMMAND( ID_FILE_PRINT, OnFilePrint ) ON_COMMAND( ID_FILE_PRINT_PREVIEW, OnFilePrintPreview ) // }}AFX_MSG_MAP // Standard printing commands // ON_COMMAND( ID_FILE_PRINT, CView::OnFilePrint ) ON_COMMAND( ID_FILE_PRINT_DIRECT, CView::OnFilePrint ) // ON_COMMAND( ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview ) E N D _ M E S S A G E _ M A P ( ) 168第第第二部分第用户界面实例 下载填充O n F i l e P r i n t ( )和O n F i l e P r i n t P r e v i e w ( )函数以使用新类的C a p t u r e ( )来抓取视: void CWzdVi e w : : O n F i l e P r i n t ( ) { // capture our view CRect rect; G e t WindowRect( &rect ); m_bitmap.Capture( rect ); C Vi e w : : O n F i l e P r i n t ( ) ; } void CWzdVi e w : : O n F i l e P r i n t P r e v i e w ( ) { // capture our view CRect rect; G e t WindowRect( &rect ); m_bitmap.Capture( rect ); C Vi e w : : O n F i l e P r i n t P r e v i e w ( ) ; } 用C l a s s Wizard 重载O n P r i n t ( )函数并在其中填充位图类的 P r i n t ( )函数: void CWzdView::OnPrint( CDC* pDC, CPrintInfo* pInfo ) { // print captured bitmap to pDC m_bitmap.Print( pDC ); // CView::OnPrint( pDC, pInfo ); } 4. 注意 为什么用: : S t r e t c h D I B i t s ( )而不是用C D C : : S t r e t c h B l t ( )函数来扩展位图呢?为什么在打印之 前需要将位图转变成D I B呢?为什么不能仅仅将屏幕调色板选入打印机设备环境,让设备环境 出于真正的协同工作性而将其选出呢?答案是:从设备调色板转变为其他设备调色板的功能 比起从已有的D I B格式开始转变,其开销将明显大很多。 5. 使用光盘时注意 执行随书附带的光盘中的工程时,单击 F i l e,然后再单击P r i n t或者Print Preview菜单命令。 视(碰巧是空的)将打印到打印机或者打印预览对话框。 6. 程序清单— 位图类 #ifndef WZDBITMAP_H #define WZDBITMAP_H class CWzdBitmap : public CBitmap { p u b l i c : DECLARE_DYNAMIC( CWzdBitmap ) // Constructors 第 6 章第视第第169下载C W z d B i t m a p ( ) ; void Capture( CRect &rect ); CPalette *GetPalette(){return m_pPalette;}; HANDLE CreateDIB( int *pbmData = NULL ); void Print( CDC *pDC ); // Implementation p u b l i c : virtual ~CWzdBitmap(); // Attributes int m_nWi d t h ; int m_nHeight; // Operations p r i v a t e : CPalette *m_pPalette; } ; # e n d i f // WzdBitmap.cpp : implementation of the CWzdBitmap class / / #include "stdafx.h" #include "WzdBtmap.h" / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdBitmap IMPLEMENT_DYNAMIC( CWzdBitmap, CBitmap ) C W z d B i t m a p : : C W z d B i t m a p ( ) { m_pPalette = NULL; } C W z d B i t m a p : : ~ C W z d B i t m a p ( ) { if ( m_pPalette ) { delete m_pPalette; } } void CWzdBitmap::Capture(CRect &rect) { // cleanup from last capture if (m_pPalette) { delete m_pPalette; 170第第第二部分第用户界面实例 下载D e l e t e O b j e c t ( ) ; } // save width and height m _ n Width = rect.Wi d t h ( ) ; m_nHeight = rect.Height(); / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // copy screen image into a bitmap object / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // create a device context that accesses the whole screen CDC dcScreen; dcScreen.CreateDC( “DISPLAY”, NULL, NULL, NULL ); // create an empty bitmap in memory CDC dcMem; dcMem.CreateCompatibleDC( &dcScreen ); CreateCompatibleBitmap( &dcScreen, m_nWidth, m_nHeight ); dcMem.SelectObject( this ); // copy screen into empty bitmap dcMem.BitBlt( 0,0,m_nWidth,m_nHeight,&dcScreen,rect.left,rect.top,SRCCOPY ); // this bitmap is worthless without the current system palette, so... / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // save system palette in this bitmap’s palette / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // create an empty logical palette that’s big enough to hold all the colors int nColors = ( 1 << ( dcScreen.GetDeviceCaps( BITSPIXEL ) * dcScreen.GetDeviceCaps( PLANES ) ) ); L O G PALETTE *pLogPal = ( LOGPALETTE * )new BYTE[ sizeof( LOGPALETTE ) + ( nColors * sizeof( PA L E T T E E N T RY ) )]; // initialize this empty palette’s header pLogPal -> palVersion = 0x300; pLogPal -> palNumEntries = nColors; // load this empty palette with the system palette’s colors ::GetSystemPaletteEntries( dcScreen.m_hDC, 0, nColors, ( LPPA L E T T E E N T RY )( pLogPal -> palPalEntry ) ); // create the palette with this logical palette m_pPalette = new CPalette; m_pPalette -> CreatePalette( pLogPal ); // clean up delete []pLogPal; d c M e m . D e l e t e D C ( ) ; 第 6 章第视第第171下载d c S c r e e n . D e l e t e D C ( ) ; } HANDLE CWzdBitmap::CreateDIB( int *pbmData ) { / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // create DIB header from our BITMAP header / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / BITMAPINFOHEADER bi; memset( &bi, 0, sizeof( bi ) ); bi.biSize = sizeof( BITMAPINFOHEADER ); bi.biPlanes = 1; bi.biCompression = BI_RGB; // get and store dimensions of bitmap BITMAP bm; GetObject( sizeof( bm ),( LPSTR )&bm ); b i . b i Width = bm.bmWi d t h ; bi.biHeight = bm.bmHeight; // get number of bits required per pixel int bits = bm.bmPlanes * bm.bmBitsPixel; if (bits <= 1) bi.biBitCount = 1; else if ( bits <= 4 ) bi.biBitCount = 4; else if ( bits <= 8 ) bi.biBitCount = 8; e l s e bi.biBitCount = 24; // calculate color table size int biColorSize = 0; if ( bi.biBitCount! = 24 ) biColorSize = ( 1 << bi.biBitCount ); biColorSize* = sizeof( RGBQUAD ); // calculate picture data size bi.biSizeImage = ( DWORD )bm.bmWidth * bi.biBitCount; // bits per row bi.biSizeImage = ( ( ( bi.biSizeImage ) + 31 ) / 32 ) * 4; // DWORD aligned bi.biSizeImage* = bm.bmHeight; // bytes required for whole bitmap // return size to caler in case they want to save to file if ( pbmData ) *pbmData = bi.biSize + biColorSize; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // get DIB color table and picture data / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // allocate a hunk of memory to hold header, color table and picture data HANDLE hDIB = ::GlobalAlloc( GHND, bi.biSize + biColorSize + bi.biSizeImage ); 172第第第二部分第用户界面实例 下载// get a memory pointer to this hunk by locking it LPBITMAPINFOHEADER lpbi = ( LPBITMAPINFOHEADER )::GlobalLock( hDIB ); // copy our header structure into hunk *lpbi = bi; // get a device context and select our bitmap’s palette into it CDC dc; dc.Attach( ::GetDC( NULL ) ); CPalette *pPal = dc.SelectPalette( m_pPalette,FALSE ); d c . R e a l i z e P a l e t t e ( ) ; // load our memory hunk with the color table and picture data ::GetDIBits( dc.m_hDC, ( HBITMAP )m_hObject, 0, ( UINT )bi.biHeight, ( LPSTR )lpbi + (WORD)lpbi->biSize + biColorSize, (LPBITMAPINFO)lpbi, D I B _ R G B _ C O L O R S ) ; // clean up ::GlobalUnlock( hDIB ); dc.SelectPalette( pPal,FALSE ); d c . R e a l i z e P a l e t t e ( ) ; // return handle to the DIB return hDIB; } void CWzdBitmap::Print( CDC *pDC ) { // get DIB version of bitmap int bmData; HANDLE hDIB = CreateDIB( &bmData ); // get memory pointers to the DIB’s header and data bits LPBITMAPINFOHEADER lpDIBHdr = ( LPBITMAPINFOHEADER )::GlobalLock( hDIB ); LPSTR lpDIBBits = ( LPSTR )lpDIBHdr+bmData; // stretch bitmap to fill printed page with 1/4 inch borders int cxBorder = pDC -> GetDeviceCaps(LOGPIXELSX)/4; int cyBorder = pDC -> GetDeviceCaps(LOGPIXELSY)/4; int cxPage = pDC -> GetDeviceCaps(HORZRES) - (cxBorder*2); int cyPage = ( int )( ( ( double )cxPage/ ( double )m_nWidth ) * ( double )m_nHeight ); // stretch the bitmap for the best fit on the printed page pDC -> SetStretchBltMode( COLORONCOLOR ); int i = ::StretchDIBits( pDC -> m_hDC, c x B o r d e r, c y B o r d e r, c x P a g e , c y P a g e , // destination dimensions 0 , 0 , m _ n Wi d t h , m _ n H e i g h t , // source bitmap dimensions // (use all of bitmap) l p D I B B i t s , // bitmap picture data ( L P B I T M A P I N F O ) l p D I B H d r, // bitmap header info 第 6 章第视第第173下载D I B _ R G B _ C O L O R S , // specify color table // has RGB values S R C C O P Y // simple source to destination copy ) ; // cleanup ::GlobalUnlock( hDIB ); ::GlobalFree( hDIB ); r e t u r n ; } 6.5 实例19:绘制MDI客户视 1. 目标 如图6 - 4所示,在M D I应用程序的 M D I客户视(MDI Client Vi e w)中绘制 装饰图案。 2. 策略 M D I应用程序由几个子框架窗口 填充,这样的每个子框架窗口包含一 个视类的窗口。这些子框架窗口覆盖 的区域并不只是主框架窗口的客户 区。实际上该区域是由 MDI Client窗 口类创建的子窗口所覆盖。因此,为了绘制背景,不能简单地为 C M a i n f r a m e 类增加 W M _ PA I N T消息处理函数并绘制背景。实际必须对 MDI Client子类化并截获W M _ PA I N T窗口 消息。 3. 步骤 1) 创建新的MDI Client窗口类 用C L a s s Wi z a r d创建一个从generic CWnd派生的新类。 在该新类中嵌入一个C B i t m a p变量: CBitmap m_bitmap; 在新类的构造函数内为该成员变量装载要显示到 MDI Client区域的位图: C M D I C l i e n t W n d : : C M D I C l i e n t W n d ( ) { // get bitmap and info m_bitmap.LoadBitmap( IDB_WZD_MDIBITMAP ); m_bitmap.GetObject( sizeof( BITMAP ), &m_bm ); } 用C l a s s Wi z a r d为该类添加 W M _ PA I N T消息处理函数。在该处理函数内为客户区绘制位 图: void CMDIClientWnd::OnPaint() { CPaintDC dc( this ); // device context for painting 174第第第二部分第用户界面实例 下载 图6-4 填充MDI客户区 M D I应用程序 平淡无奇的背 景将绘制的位 图来替代CDC dcMem; dcMem.CreateCompatibleDC( &dc ); dcMem.SelectObject( m_bitmap ); // stretch bitmap CRect rect; G e t C l i e n t R e c t ( & r e c t ) ; dc.StretchBlt( rect.left, rect.top, rect.right, rect.bottom, &dcMem, 0, 0, m_bm.bmWidth-1, m_bm.bmHeight-1, SRCCOPY ); 本实例结尾的程序清单— MDI CLient窗口类提供以上MDI Client窗口类的完整列表。 现在已经拥有了一个新的M D I窗口类,需要将实际的MDI Client窗口子类化以便新类能够 截获W M _ PA I N T消息。 2) 实现新的MDI Client窗口类 在C M a i n F r a m e中嵌入新的MDI Client窗口类: p u b l i c : CMDIClientWnd m_wndMDIClient; 然后在C M a i n F r a m e的O n C r e a t e成员函数中用新的窗口类子类化当前的 MDI Client窗口: int CMainFrame::OnCreate( LPCREATESTRUCT lpCreateStruct ) { : : : // subclass our CMDIClientWnd class to MDIClient window if( !m_wndMDIClient.SubclassWindow( m_hWndMDIClient ) ) { TRACE( " Failed to subclass MDI client window\n" ); return -1; } 主窗口被重置大小时,只有 MDI Client窗口新区域无效——创建了一块扩展的位图。为了 消除这种现象,应该使整个客户区在重置主窗口时都无效。 用C l a s s Wi z a r d添加一个WM_SIZE 消息处理函数并如下加入代码: void CMainFrame::OnSize( UINT nType, int cx, int cy ) { CMDIFrameWnd::OnSize( nType, cx, cy ); m _ w n d M D I C l i e n t . I n v a l i d a t e ( ) ; } 4. 注意 本例在MDI Client窗口中打印一个大大的W。更漂亮点的位图可能是包含重复小图片的那 种,例如按照行和列逐个显示图标等。为实现在适当位置堆砌图标当然要为 W M _ PA I N T处理 函数添加更多的代码。 既然已经控制了M D I应用程序的客户区,在空间限制以内就可以添加控件统计列表等等。 5. 使用光盘时注意 执行随书附带光盘中的该工程,将注意到在 MDI Client区域中有一个大大的W。重置主窗 第 6 章第视第第175下载口将使得该W字随之扩大或者缩小。 6. 程序清单— M D I客户窗口类 #if !defined MDICLIENTWND_H #define MDICLIENTWND_H / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CMDIClientWnd window class CMDIClientWnd : public CWnd { // Construction p u b l i c : C M D I C l i e n t W n d ( ) ; // Attributes p u b l i c : // Operations p u b l i c : // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CMDIClientWnd ) // }}AFX_VIRT U A L // Implementation p u b l i c : virtual ~CMDIClientWnd(); // Generated message map functions p r o t e c t e d : // {{AFX_MSG( CMDIClientWnd ) afx_msg void OnPaint(); // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : BITMAP m_bm; CBitmap m_bitmap; } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / # e n d i f // MDIClientWnd.cpp : implementation file / / #include "stdafx.h" #include "wzd.h" #include "MDIClientWnd.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE 176第第第二部分第用户界面实例 下载static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CMDIClientWnd C M D I C l i e n t W n d : : C M D I C l i e n t W n d ( ) { // get bitmap and info m_bitmap.LoadBitmap( IDB_WZD_MDIBITMAP ); m_bitmap.GetObject( sizeof( BITMAP ), &m_bm ); } C M D I C l i e n t W n d : : ~ C M D I C l i e n t W n d ( ) { } BEGIN_MESSAGE_MAP( CMDIClientWnd, CWnd ) // {{AFX_MSG_MAP( CMDIClientWnd ) O N _ W M _ PA I N T ( ) // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CMDIClientWnd message handlers void CMDIClientWnd::OnPaint() { CPaintDC dc( this ); // device context for painting CDC dcMem; dcMem.CreateCompatibleDC( &dc ); dcMem.SelectObject( m_bitmap ); // stretch bitmap CRect rect; GetClientRect( &rect ); dc.StretchBlt( rect.left, rect.top, rect.right, rect.bottom, &dcMem, 0, 0, m_bm.bmWidth-1, m_bm.bmHeight-1, SRCCOPY ); } 6.6 实例20:拖放文件到视 1. 目标 打开拖放到视中的文件。 2. 策略 众所周知,可以从Windows 资源管理器或者其他工具软件中拖动一个文件放到 D e v e l o p e r S t u d i o中,S t u d i o就可以魔术般地打开该文件。为了在自己的应用程序中实现同样的功能,则 需要首先用C W n d : : D r a g A c c e p t F i l e s ( )函数通知系统应用程序窗口中可以支持文件拖放。然后 第 6 章第视第第177下载为C M a i n F r a m e类增加一个W M _ D R O P F I L E S消息处理函数,以获取所拖放的文件的名字并打 开相应的文件。在本实例中,将修改一个 M D I应用程序来为每个拖放的文件打开新文档和视。 3. 步骤 修改主框架类 从C M a i n F r a m e的O n C r e a t e ( )消息处理函数内调用下列函数,将使应用程序能够接受拖放 的文件: // allow files to be dropped on main window or any child window D r a g A c c e p t F i l e s ( ) ; 用C l a s s Wi z a r d为C M a i n F r a m e类添加W M _ D R O P F I L E S消息处理函数,使得可以使用 C Wi n A p p : : O p e n D o c u m e n t F i l e ( )函数为该文件创建新的文档 /视: void CMainFrame::OnDropFiles( HDROP hDropInfo ) { // get filename stored in hDropInfo and use app to open it TCHAR szFileName[_MAX_PAT H ] ; ::DragQueryFile( hDropInfo, 0, szFileName, _MAX_PATH ); ::DragFinish( hDropInfo ); AfxGetApp() -> OpenDocumentFile( szFileName ); CMDIFrameWnd::OnDropFiles( hDropInfo ); } 4. 注意 该消息处理函数在 S D I应用程序中也可以使用,但在打开新文件之前将破坏当前的文档 / 视。 5. 使用光盘时注意 执行随书附带的光盘中的该工程时,打开 Windows 资源浏览器,从 Windows Explorer拖 动一个文件到该应用程序中,注意一个新的文档 /视被打开了。 178第第第二部分第用户界面实例 下载下载下载 第 7 章 对话框和对话条 对话框和对话条可以提示用户使用一些类型的控件,例如按钮和列表框。对话条是工具 栏和对话框的混合物,对话条可以停靠到应用程序的某一边,并同时可以包含所有可用于对 话框的控件。 M F C和Developer Studio将为用户自动创建对话框和类。在这一章中,我们将着眼于几种 用手工修改对话框的方法,使得对话框对用户更为有用,本章的实例包括: 实例21 动态改变对话框的尺寸,单击一个按钮将使对话框扩大,以便允许在上面添加更 多的控件。 实例22 自定义数据交换(Customizing Data Exchange)和验证( Va l i d a t i o n ),将看到如何创 建用户自己的可在对话框的类成员变量和控件之间进行交换的数据类型。 实例23 重载通用文件对话框,将看到如何定制文件对话框,这个对话框对于所有的应用 程序都适用,并且可以免去令人头痛的编写代码工作。 实例24 重载通用颜色对话框,与前一个例子相同,只是在该例中看到的是颜色对话框。 实例25 获得目录名,将看到如何调用 A P I来获得目录名而不是整个文件的路径。 实例26 子对话框,将使用一个对话框,该对话框就像是另一个对话框中的普通控件。 实例27 子属性表,将使用一个属性表,该属性表就像是另一个对话框中的普通控件。 7.1 实例21:动态改变对话框的尺寸 1. 目标 为了在对话框上添加更多的控件将对话框放大,如图 7 - 1所示,单击More >>按扭可将对 话框尺寸放大。 图7-1 动态改变对话框的尺寸 2. 策略 以通常方式创建一个具有 More >>按钮的对话框资源和对话框类,但在对话框类的 O n I n i t D i a l o g ( )函数中用C W n d : : M o v e Wi n d o w ( )函数来将对话窗口缩为More >>按钮。这样,当 单击More >>按钮时,将使用M o v e Wi n d o w ( )函数来切换对话窗口的尺寸:从大到小或从小到 通过单击M o r e / L e s s按钮在两者 间互相转换大。 3. 步骤 切换对话框的尺寸 按通常方式,用 Dialog Editor和C l a s s Wi z a r d创建对话框模板和对话框类,在窗口中央添 加More >>按钮,并将其他控件放在它的一侧。 用C l a s s Wi z a r d重载O n I n i t D i a l o g ( )函数,在O n I n i t D i a l o g ( )函数中首先保存当前对话框的尺 寸,该尺寸将是对话框的最大尺寸,然后定位到 M o r e > >按钮代码并使窗口缩小为该按钮大小, 代码如下: BOOL CWzdDialog::OnInitDialog() { C D i a l o g : : O n I n i t D i a l o g ( ) ; // save our full size G e t WindowRect( &m_rectFull ); m_rectHalf = m_rectFull; // calculate our half size based on bottom of "More" button CRect rect; m _ c t r l M o r e B u t t o n . G e t WindowRect( &rect ); m_rectHalf.bottom = rect.bottom+10; // + 10 for cosmetics // toggle window size To g g l e S i z e ( ) ; return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FA L S E } 由于More >>按钮也将切换对话框窗口的尺寸,因此在下面要填充的 To g g l e S i z e ( )函数中 写入控制逻辑代码来缩小窗口的尺寸。 用C l a s s Wi z a r d为More >>按钮增加消息处理函数来调用 To g g l e S i z e ( )函数: void CWzdDialog::OnMoreButton() { ToggleSize(); } To g g l e S i z e ( )函数将按钮上的名字改变为 < < L e s s,然后用C W n d : : S e t Wi n d o w P o s ( )来改变窗 口的尺寸,代码如下: void CWzdDialog::To g g l e S i z e ( ) { CRect rect; CString str; if ( m_bToggleSize ) { str = "<< &Less"; rect = m_rectFull; } e l s e 180第第第二部分第用户界面实例 下载{ str = "&More >>"; rect = m_rectHalf; } S e t WindowPos( NULL,0,0,rect.Wi d t h ( ) , r e c t . H e i g h t ( ) , SWP_NOZORDER|SWP_NOMOVE ); m _ c t r l M o r e B u t t o n . S e t Wi n d o w Text( str ); m _ b ToggleSize = !m_bTo g g l e S i z e ; } 要查看该对话框类的全部程序代码,请参考本实例结尾处的程序清单— 对话框类。 4. 注意 如果用户愿意在More >>按钮和< < L e s s按钮上添加更多的饰物,那么用 Dialog Editor 设置 其类型为B S _ B I T M A P,并且用CButton 中的S e t B i t m a p ( )函数来改变位图。 5. 使用光盘时注意 运行随书附带光盘中的工程时,单击 Te s t按钮,然后再单击 W z d菜单命令,打开对话框, 然后再单击More >>按钮进一步打开对话框。 6. 程序清单— 对话框类 #if !defined( AFX_WZDDIALOG_H__1EC8499A_C589_11D1_9B5C_00AA003D8695__INCLUDED_ ) #define AFX_WZDDIALOG_H__1EC8499A_C589_11 D 1 _ 9 B 5 C _ 0 0 A A 0 0 3 D 8 6 9 5 _ _ I N C L U D E D _ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 // WzdDialog.h : header file / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdDialog dialog class CWzdDialog : public CDialog { // Construction p u b l i c : CWzdDialog( CWnd* pParent = NULL ); // standard constructor // Dialog Data // {{AFX_DATA( CWzdDialog ) enum { IDD = IDD_WZD_DIALOG }; CButton m_ctrlMoreButton; // }}AFX_DATA // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CWzdDialog ) p r o t e c t e d : virtual void DoDataExchange( CDataExchange* pDX ); // DDX/DDV support // }}AFX_VIRT U A L // Implementation 第 7 章第对话框和对话条第第181下载p r o t e c t e d : // Generated message map functions // {{AFX_MSG( CWzdDialog ) virtual BOOL OnInitDialog(); afx_msg void OnMoreButton(); // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : BOOL m_bTo g g l e S i z e ; CRect m_rectFull; CRect m_rectHalf; void ToggleSize() ; } ; // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Developer Studio will insert additional declarations immediately // before the previous line. # e n d i f // !defined( AFX_WZDDIALOG_H__1EC8499A_C589_11D1_9B5C_00AA003D8695__INCLUDED_ ) // WzdDialog.cpp : implementation file / / #include "stdafx.h" #include "wzd.h" #include "WzdDialog.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdDialog dialog CWzdDialog::CWzdDialog( CWnd* pParent /* = NULL*/ ) : CDialog( CWzdDialog::IDD, pParent ) { // {{AFX_DATA_INIT( CWzdDialog ) // NOTE: the ClassWizard will add member initialization here // }}AFX_DATA _ I N I T m _ b ToggleSize = FA L S E ; } void CWzdDialog::DoDataExchange( CDataExchange* pDX ) { CDialog::DoDataExchange( pDX ); // {{AFX_DATA_MAP( CWzdDialog ) DDX_Control( pDX, IDC_MORE_BUTTON, m_ctrlMoreButton ); // }}AFX_DATA _ M A P } 182第第第二部分第用户界面实例 下载BEGIN_MESSAGE_MAP( CWzdDialog, CDialog ) // {{AFX_MSG_MAP( CWzdDialog ) ON_BN_CLICKED( IDC_MORE_BUTTON, OnMoreButton ) // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdDialog message handlers BOOL CWzdDialog::OnInitDialog() { C D i a l o g : : O n I n i t D i a l o g ( ) ; // save our full size G e t WindowRect( &m_rectFull ); m_rectHalf = m_rectFull; // calculate our half size based on bottom of "More" button CRect rect; m _ c t r l M o r e B u t t o n . G e t WindowRect( &rect ); m_rectHalf.bottom = rect.bottom+10; // + 10 for cosmetics // toggle window size To g g l e S i z e ( ) ; return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FA L S E } void CWzdDialog::OnMoreButton() { To g g l e S i z e ( ) ; } void CWzdDialog::To g g l e S i z e ( ) { CRect rect; CString str; if ( m_bToggleSize ) { str = "<< &Less"; rect = m_rectFull; } e l s e { str = "&More >>"; rect = m_rectHalf; } S e t WindowPos( NULL,0,0,rect.Width(),rect.Height(),SWP_NOZORDER|SWP_NOMOVE ); m _ c t r l M o r e B u t t o n . S e t Wi n d o w Text( str ); m _ b ToggleSize = !m_bTo g g l e S i z e ; } 第 7 章第对话框和对话条第第183下载7.2 实例22:自定义数据交换并验证 1. 目标 创建一个新的数据交换和验证函数来支持在对话框控件和对话框类成员变量之间的动态 数据交换。 2. 策略 正如本系列丛书先前书中所看到的,动态数据交换 (Dynamic data exchange)允许对话框控 件和对话框类的成员变量之间自动交换数据,而动态数据验证 (Dynamic data validation)则可 以自动检测用户输入数据中的语法错误。 M F C已经支持几种数据类型 (例如i n t和d o u b l e数据类 型)的数据交换和验证,但仍然可以通过提供用户自己的静态转换函数来支持自定义的数据类 型(例如C O c t a l S t r i n g ),其中该静态函数使用适当的命名和调用约定 (如: DDX_something()或 DDV_something() )。在这个实例中,将对二进制数据串进行数据交换和验证,其中二进制数 据串是由实例6 4中的自定义C S t r i n g类所提供的。 3. 步骤 1) 创建自定义的数据交换函数 用下面的框架代码( o u t l i n e )创建静态例程,对于一个完整的实例,请参考实例结尾处的程 序清单— 二进制数据交换和验证函数。 void AFXAPI DDX_Xxxx( CDataExchange* pDX, int nIDC, CXxxx& value ) { CWzdString str; HWND hWndCtrl = pDX -> PrepareEditCtrl( nIDC ); // getting data from control if ( pDX -> m_bSaveAndValidate ) { // hWndCtrl == window handle of the control int nLen = ::GetWi n d o w TextLength( hWndCtrl ); : : G e t Wi n d o w Text( hWndCtrl, str. G e t B u fferSetLength( nLen ), nLen+1 ); s t r. R e l e a s e B u ff e r ( ) ; // temporary str now has text data, now put into member variable } // putting data into control window e l s e { // copy data from member variable into str // sets window text only if it’s diff e r e n t A f x S e t Wi n d o w Text( hWndCtrl, str ); } } 2) 创建自定义的数据验证函数 用下面的框架代码( o u t l i n e )创建静态例程,对于一个完整的实例,请参考实例结尾处的程 序清单— 二进制数据交换和验证函数。 184第第第二部分第用户界面实例 下载void AFXAPI DDV_MaxChars( CDataExchange* pDX, CByteArray const& value, int nBytes ) { // if retrieving from control, make check of value if ( pDX -> m_bSaveAndValidate ) { if ( error ) { CString str; s t r.Format( "Maximum characters you can enter is %d!", nBytes ); AfxMessageBox( str, MB_ICONEXCLAMATION ); pDX -> Fail(); } } // else if sending to control, setup any control function // that will allow it to perform it’s own check else if ( pDX -> m_hWndLastControl != NULL && pDX -> m_bEditLastControl ) { / / / } } 3) 实现新的数据交换和验证函数 在对话框类中包含对新的数据的定义。 #include "WzdXchng.h" 插入对话框类中D o D a t a E x c h a n g ( )例程所需的函数,但是必须插在 { { } }括号后面,这样才 能使C l a s s Wi z a r d不发现它们: void CWzdDialog::DoDataExchange( CDataExchange* pDX ) { CDialog::DoDataExchange( pDX ); // {{AFX_DATA_MAP( CWzdDialog ) // }}AFX_DATA _ M A P D D X _ Text( pDX, IDC_WZD_EDIT, m_WzdArray ); <<<< DDV_MaxChars( pDX, m_WzdArray, 7 ); <<<< } 确保数据验证函数在其检测到数据交换函数之后马上被调用, C d a t a E x c h a n g e中 m _ h W n d L a s t C o n t r o l函数的成员变量由数据交换函数初始化,当然用户也可以在数据验证程序 中重新初始化该变量,但是这样要浪费 C P U的周期,是没有必要的。 4. 注意 没有必要创建一个单独的数据交换或数据验证函数,相反应该直接修改 D o D a t a E x c h a n g e ( )例程。在C l a s s Wizard 的{ { } }括号后面增加一些附加函数代码,使这些添加的函数代码与上 面的函数类似,并且记住当 m_bSave And Va l i d a t e为T R U E时,应当从控件转移到对话框的成 员变量。 void CWzdDialog::DoDataExchange( CDataExchange* pDX ) { 第 7 章第对话框和对话条第第185下载CDialog::DoDataExchange( pDX ); // {{AFX_DATA_MAP( CWzdDialog ) : : : // }}AFX_DATA _ M A P if ( pDX -> m_bSaveAndValidate ) { // get from control } e l s e { // store to control } } 当然这个方法的缺点是用户不能轻易地从新的逻辑代码转移到另一个对话框类。 5. 使用光盘时注意 运行随书附带光盘上的工程时,在W z d Vi e w. c p p中的O n Te s t W z d ( )函数中设置断点,单击Te s t 按钮和Wzd然后输入一个数字到对话框中,观察它是如何自动转换成字节数组(byte array)的。 6. 程序清单— 二进制数据转换和验证函数 #ifndef WZDXCHNG_H #define WZDXCHNG_H void AFXAPI DDX_Text( CDataExchange* pDX, int nIDC, CByteArray& value ); void AFXAPI DDV_MaxChars( CDataExchange* pDX, CByteArray const& value, int nBytes ); # e n d i f // WzdXchng / / #include "stdafx.h" #include "WzdXchng.h" #include "WzdString.h" #include / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdXchng void AFXAPI DDX_Text( CDataExchange* pDX, int nIDC, CByteArray& value ) { CWzdString str; HWND hWndCtrl = pDX -> PrepareEditCtrl( nIDC ); if ( pDX -> m_bSaveAndValidate ) { int nLen = ::GetWi n d o w TextLength( hWndCtrl ); : : G e t Wi n d o w Text( hWndCtrl, str. G e t B u fferSetLength( nLen ), nLen+1 ); s t r. R e l e a s e B u ff e r ( ) ; int size = str. G e t L e n g t h ( ) ; if ( size%2 ) { 186第第第二部分第用户界面实例 下载AfxMessageBox( "Please enter even number of digits." ); pDX -> Fail(); // throws exception } size /= 2; value.SetSize( size ); s t r.GetBinary( value.GetData(),size ); } e l s e { s t r.PutBinary( value.GetData(),value.GetSize() ); A f x S e t Wi n d o w Text( hWndCtrl, str ); // sets window text only if // it’s diff e r e n t } } void AFXAPI DDV_MaxChars( CDataExchange* pDX, CByteArray const& value, int nBytes ) { if ( pDX -> m_bSaveAndValidate && value.GetSize() > nBytes ) { CString str; s t r.Format( "Maximum characters you can enter is %d!", nBytes ); AfxMessageBox( str, MB_ICONEXCLAMATION ); s t r. E m p t y ( ) ; // will not return after exception pDX -> Fail(); } else if ( pDX -> m_hWndLastControl != NULL && pDX -> m_bEditLastControl ) { // set edit box not to allow more then these bytes // note that this function depends on being called // right after the DDX function ::SendMessage( pDX -> m_hWndLastControl, EM_LIMITTEXT, nBytes*2, 0 ); } } 7.3 实例23:重载通用文件对话框 1. 目标 在通用文件对话框上增加一些自己的特色,如图 7 - 2所示。 2. 策略 从Common File Dialog类中派 生出用户自己的新类 C f i l e D i a l o g, 而且为对话框增加一些控件。同 时使用其构造函数中的参数以及 设置其O P E N F I L E N A M E结构来定 制该对话框。 3. 步骤 第 7 章第对话框和对话条第第187下载 图7-2 自定义文件对话框 添加到普通文件对 话框底部的用户自 行创建的控件1) 创建新的通用文件对话框类 注意 如果用户不希望为文件对话框增加自己的控件,便不需要创建新的类,因此可以 略过这一部分。 使用C l a s s Wi z a r d从C F f i l e D i a l o g中派生新的类。 用Dialog Editor创建一个小的对话框模板,模板中仅包括用户希望添加到通用文件对话框 上的控件。该通用对话框将自动创建对话框中可见的其余标准控件,因此用户不必担心它们, 只需创建新的控件即可。模板应该具有一个不带框架的子窗口风格。 用C l a s s Wi z a r d为新的控件添加消息处理函数,为了访问由文件对话框自动添加的控件, 可以用G e t D l g I t e m ( )函数和下面任一个I D号,I D与控件匹配关系在以后的 Wi n d o w s版本中可能 会改变: p s h 1 , p s h 2 , . . . , p s h 1 2 // push buttons c h x 1 - 1 2 // checkboxes r a d 1 - 1 2 // radio buttons g r p 1 - 1 2 // group boxes s t c 1 - 1 2 // statics l s t 1 - 1 2 // listboxes/views c m b 1 - 1 2 // comboboxes e d t 1 - 1 2 // edit boxes s c r 1 - 1 2 // scrollbars 2) 初始化通用文件对话框类 为使用C F i l e D i a l o g对话框类或自己的派生类,首先构造通用文件对话框类如下: CFileDialog dlg( T R U E , // TRUE = create a File Open dialog, // FALSE = create a File Save As // dialog _ T ( " . l o g " ) , // default filename extension " ", // initial filename in edit box // functionality flags O F N _ A L L O W M U LT I S E L E C T | // allow multiple files // to be selected // OFN_CREATEPROMPT | // if File Save As, prompts user // if they want to create // non-existant file // OFN_OVERWRITEPROMPT | // if File Save As—prompts user to // ask if they want to overwrite // an existing file // OFN_ENABLESIZING | // if Windows NT 5.0 or Win 98, // causes box to be resizable // by user // OFN_EXTENSIONDIFFERENT| // allows user to enter a filename // with a different extension // from the default // OFN_FILEMUSTEXIST // file must exist // OFN_NOLONGNAMES | // causes dialog to use short // filenames (8.3) // OFN_PATHMUSTEXIST | // user can only type valid paths 188第第第二部分第用户界面实例 下载// and filenames // OFN_NOVA L I D ATE | // the returned filname can have // invalid characters // appearence flags // OFN_HIDEREADONLY | // hides read-only check box // OFN_NONETWORKBUTTON | // hides Network button // OFN_READONLY | // initially check Read Only // check box // OFN_SHOWHELP | // Help button appears—when clicked // the hook procedure gets a // CDN_HELP message // custom template flags O F N _ E N A B L E T E M P L ATE | // you will be supplying your own // custom dialog box template 0 , "Accounting Files ( *.log;*.txt )|*.log;*.txt|All Files ( *.* )|*.*||", // file filter N U L L ) ; // parent window 如果要在对话框中添加自己的控件,那么也必须定义文件对话框打开时的对话框模板, 代码如下: d l g . m _ o f n . l p Te m p l a t e N a m e = M A K E I N T R E S O U R C E ( I D D _ W Z D _ F I L E O P E N ) ; 当构造上面的类时,必须设置 O F N _ E N A B L E T E M P L ATE 标志。 用下面的代码使通用文件对话框在开始时使用一个特定目录: // set an initial directory char lpszInitDir[] = {"c:\\temp"}; dlg.m_ofn.lpstrInitialDir = lpszInitDir; 用下面的代码将文件对话框命名为 O p e n或Save File As: //set the dialog’s title char lpszTitle[] = {"Open Wzd File"}; d l g . m _ o f n . l p s t r Title = lpszTi t l e ; 为设置一个在文件对话框关闭后仍然保留的文件过滤器,首先设置一个如下所示的静态 字符串: static char lpstrCustomFilter[255] = {"Previous Filter\0*.log\0"}; 然后用它的地址初始化文件对话框,如下所示: // retain the customer’s last file filter selection dlg.m_ofn.lpstrCustomFilter = lpstrCustomFilter; dlg.m_ofn.nMaxCustFilter = 255; 3) 创建通用文件对话框和检索值 用CfileDialog ::DoModal() 函数打开一个文件对话框,然后检查用户是否已经单击 I D O K, 如果是,则从对话框中获得该值: if ( dlg.DoModal() == IDOK ) { 检索用户选择的文件名,这可以使用下面任何一个函数: CString path = dlg.GetPathName(); // ex: c:\temp\temp.tmp 第 7 章第对话框和对话条第第189下载CString file = dlg.GetFileName(); // ex: temp.tmp CString title = dlg.GetFileTi t l e ( ) ; // ex: temp CString ext = dlg.GetFileExt(); // ex: tmp 可以用下面的代码确定用户选择了哪一个文件过滤器: int nFilterIndex=dlg.m_ofn.nFilterIndex; 该过滤器作为索引返回到各种可能的文件过滤器列表中。 为了获得只读(Read Only)复选框的状态,可以用下面的代码: BOOL bReadOnly =dlg.GetReadOnlyPref(); 如果用户选择了多个文件 (在上面设置了O F N _ A L L O W M U LT I S E L E C T ),那么可以用下面 的代码翻动它们: for (POSITION pos = dlg.GetStartPosition();pos;) { CString pathx = dlg.GetNextPathName(pos); // ex: c:\temp\temp.tmp } } 注意,当选择多个文件时,必须分析文件名以获得文件的标题和扩展名等等。 4. 注意 这个实例演示了一种修改文件浏览器风格的 File Open 对话框的方法,用这种风格,用户 不但可以打开需要的文件,而且可以执行与 Windows Explorer一样的文件维护功能,包括创建 新的目录,删除旧的文件等。另一个旧的 File Open对话框版本也可以用,旧风格的优点在于 每个对话框控件都比较容易使用,因为用户接触到编辑实际的对话框模板,这个模板称为 F I L E O P E N . D L G,可以在\ V C \ I N C L U D E文件夹找到。如果用户愿意使用旧版本的 File Open对 话框,可参考实例 2 4,该实例说明了如何使用 \VC\INCLUDE 中的模板重载通用文件对话框。 当然,用户也可以用 F I L E O P E N . D L G,不管怎样,必须在使用下面代码构造文件对话框类时 设置O F N _ E X P L O R E R标志,因为在构造C F i l e D i a l o g类时这个标志将自动设置。 d l g . m _ o f n . f l a g s & = ~ O F N _ E X P L O R E R ; 另外,尽量不要使用 C F i l e D i a l o g提供的H e l p,用户应该通过添加和运行 H e l p来得到更好 的使用效果。 5. 使用光盘时注意 运行随书附带光盘上的工程时,单击 Te s t按钮和W z d打开通用文件对话框,注意到这个对 话框除了在对话框底部增加了两个新控件以外,与在 Wi n d o w s中遇到的大多数文件对话框都 类似。 7.4 实例24:重载通用颜色对话框 1. 目标 在通用颜色对话框上增加一些自己的特色,如图 7 - 3所示。 2. 策略 将从Common Dialog类中派生出新类C C o l o r D i a l o g,而且为对话框增加一些控件。同时使 用其构造函数中的参数以及设置其 C H O O S E C O L O R结构来定制该对话框。 190第第第二部分第用户界面实例 下载3. 步骤 1) 创建新的通用颜色对话框类 注意 如果用户不希望为颜色对话 框增加自己的控件,可以不需要创 建新的类,因此可以略过这一部分。 使用C l a s s Wi z a r d从C C o l o r D i a l o g 中派生出新的类。 这一步必须剪切标准颜色对话框模 板并粘贴到应用程序的源代码中,该标准模板可以在 \ V C \ I N C K U D E目录下的C O L O R . D L G中 找到,用文本编辑器打开它并把它粘贴到 . r c文件的对话框部分中。 同时必须将 C O L O R D L G . H的资源包含文件添加到自己的源文件中,这可以通过单击 Developer Studio中的Vi e w和Resource Includes菜单项来打开Resource Includes对话框,然后进 入C O L O R D L G . H来实现。 这一步使用Dialog Editor来编辑对话框,可以移动、删除和添加这些控件— 只是不能改 变现有控件的I D号,颜色对话框将会寻找它们的 I D号。 2) 初始化通用颜色对话框类 为使用C C o l o r D i a l o g对话框类或自己的派生类,首先构造通用颜色对话框类如下: CWzdColorDialog dlg( r g b , // initial color selection // CC_FULLOPEN| // intially opens both sides // of color dialog // CC_PREVENTFULLOPEN| // prevent user from opening customize // side of color dialog C C _ S H O W H E L P | // creates help button // CC_SOLIDCOLOR| // display only non-dithered colors C C _ E N A B L E T E M P L AT E | // use a custom dialog template 0 , N U L L // parent window ) ; 如果使用自己的对话框模板,那么同时必须在 C o l o r D i a l o g的 C H O O S E C O L O R结构中定义该模板: d l g . m _ c c . l p Te m p l a t e N a m e = M A K E I N T R E S O U R C E ( I D D _ W Z N _ C H O O S E C O L O R ) ; 当构造上面的类时,必须设置 C C _ E N A B L E T E M P L ATE 标志。 为在C C o l o r D i a l o g的调用之间保存用户所定制的颜色,必须提供指向 1 6个C O L O R R E F值 的静态数组的指针,代码如下: COLORREF lpCustColors[16]; : : : dlg.m_cc.lpCustColors = lpCustColors; 3) 创建通用颜色对话框 用CColorDialog ::DoModal()函数打开一个通用颜色对话框,然后检查用户是否已经单击 I D O K,如果单击,那么从对话框中获取其值: 第 7 章第对话框和对话条第第191下载 图7-3 定制颜色对话框 用户自定义的 控件被添加到 普通颜色对话 框的底部if ( dlg.DoModal() == IDOK ) { rgb = dlg.GetColor(); } 4. 注意 ■ 除了以上介绍的文件对话框和颜色对话框之外,还有用于字体设置、查找和替换文 本、打开文件、页面设置和文件打印的通用对话框,打开文件的通用对话框将在最后的实 例中演示。 ■ 颜色对话框通常用于选择散射 ( d i ff u s e d )颜色,但使用C C _ S O L I D C O L O R风格来创建颜 色对话框时,可用它来选择非散射颜色。不幸的是,在一个只有 2 5 6种颜色的系统上颜色的选 择可能变得非常少。为在一个只有2 5 6种颜色的系统提供给用户更广泛的非散射颜色选择范围, 请参考实例3 1。 5. 使用光盘时注意 运行随书附带光盘上的工程时,单击 Te s t按钮和W z d打开通用颜色对话框,用户将会注意 到这个对话框除了在对话框底部增加了两个新控件以外,与在 Wi n d o w s中遇到的大多数颜色 对话框都类似。 7.5 实例25:获得目录名 1. 目标 创建一个可显示系统驱动器的子目 录结构的树形控件来显示系统磁盘的目 录,如图7 - 4所示。 2. 策略 用户可能认为通用文件对话框可以 处理所有的文件提示,但某一天可能忽 然意识到自己应该具有一个只用于提示 文件目录的应用程序,同时也意识到没 有捷径来使用通用文件对话框为用户提 示目录信息。虽然可以采用使控件不可 见以及使编辑控件在用户单击目录时返回这种方式,但是这会弄得一团糟,而且采用这种方 式的通用文件对话框,当用于提示目录信息时,给用户看起来的外观和用起来的感觉与其他 微软应用程序不匹配,那么该用什么呢?答案:使用 M F C文档中的一个小小的 : : S H B r o w s e - ForFolder() API调用。 由于: : S H B r o w s e F o r F o l d e r ( )需要一些特殊的处理,本例将在自己的 D i r e c t o r y类中封装其功 能。 3. 步骤 1) 创建目录类 用C l a s s Wi z a r d创建新的类,且不从任何类中派生。本例要使用的 A P I不需要从任何 M F C 类中继承功能。 向该类中添加G e t D i r e c t o r y ( )函数,: : S H B r o w s e F o r F o l d e r ( )在创建B R O W S E I N F O结构时将 192第第第二部分第用户界面实例 下载 图7-4 目录提示对话框 一个可为用户 提示文件夹(目 录)名的对话框启动该函数。 CString CWzdDirDlg::GetDirectory( CWnd *pParent,LPCSTR lpszRoot, LPCSTR lpszTitle ) { CString str; BROWSEINFO bi; bi.hwndOwner = pParent -> m_hWnd; // owner of created dialog box bi.pidlRoot = 0; // unused bi.pszDisplayName = 0; // buffer to receive name // displayed by folder // (not a valid path) b i . l p s z Title = lpszTi t l e ; // title is "Browse for // Folder", this is // an instruction bi.lpfn = BrowseCallbackProc; // callback routine called // when dialog has been // initialized bi.lParam = 0; // passed to callback routine bi.ulFlags = B I F _ R E T U R N O N LYFSDIRS | // only allow user to select // a directory B I F _ S TATUSTEXT | // create status text field // we will be writing to // in callback // BIF_BROWSEFORCOMPUTER| // only allow user to select // a computer // BIF_BROWSEFORPRINTER | // only allow user to select // a printer // BIF_BROWSEINCLUDEFILES| // displays files too which // user can pick // BIF_DONTGOBELOWDOMAIN| // when user is exploring the // "Entire Network" they // are not allowed into // any domain 0 ; m_sRootDir = lpszRoot; // save for callback routine 下一步,可以调用 : S H B r o w s e F o r F o l d e r ( )提示用户输入目录名,随后可以使用 : : S H G e t P a t h F r o m I D L i s t ( )获取该目录名: LPITEMIDLIST lpItemId = ::SHBrowseForFolder( &bi ); if ( lpItemId ) { LPTSTR szBuf = str. G e t B u ffer( MAX_PATH ); ::SHGetPathFromIDList( lpItemId, szBuf ); ::GlobalFree( lpItemId ); s t r. R e l e a s e B u ff e r ( ) ; } return str; } 第 7 章第对话框和对话条第第193下载在第1步中,我们指定了一个回调例程。只有在需要进一步更改 ::SHBrowse ForFolder()行 为的时候才需要使用回调例程。如果指定 N U L L,则说明不需要回调例程。但是,为了在打开 提示时实现一个初始目录,则需要使用一个回调例程!所以,加入下例回调函数以修改提示 的初始目录: int CALLBACK BrowseCallbackProc( HWND hwnd,UINT msg,LPARAM lp, L PARAM pData ) { TCHAR buf[MAX_PAT H ] ; switch( msg ) { // when dialog is first initialized, change directory // to one chosen above case BFFM_INITIALIZED: strcpy( buf,CWzdDirDlg::m_sRootDir ); ::SendMessage( hwnd,BFFM_SETSELECTION,TRUE,( LPARAM) buf ); b r e a k ; 如果选择上面的B I F _ S TAT U S T E X T风格,则用户每次选择不同的文件夹时都可以在这个 回调程序中设置其状态: case BFFM_SELCHANGED: if ( ::SHGetPathFromIDList( ( LPITEMIDLIST ) lp ,buf ) ) SendMessage( hwnd,BFFM_SETSTAT U S T E X T,0,( LPARAM )buf ); b r e a k ; } return 0; } 该类的完整程序代码可参考本实例结尾处的程序清单— 目录类。 2) 使用目录类 为用户提示目录现在是本实例中的目录类调用其 G e t D i r e c t o r y ( )函数所完成的一件轻而易 举的事情: // get current working directory char buf[MAX_PAT H ] ; _getcwd( buf,MAX_PATH ); CWzdDirDlg dlg; CString dir = dlg.GetDirectory( this, // parent window buf, // root directory " Title" // optional title ) ; 4. 注意 用户可能已经注意到了 B R O W S E I N F O结构中的p i d l R o o t成员变量,也许会认为它是用来 指定一个初始的目录,而事实上它除了使用户花费几个小时试图使其工作以外好像别无用处。 5. 使用光盘时注意 运行随书附带光盘上的工程时,单击 Te s t按钮然后单击W z d菜单命令打开目录,此时将提 194第第第二部分第用户界面实例 下载示已经打开的当前工作目录。 6. 程序清单— 目录类 #if !defined( AFX_WZDDRDLG_H__D52656D3_8A25_11D2_9C53_00AA003D8695__INCLUDED_ ) #define AFX_WZDDRDLG_H__D52656D3_8A25_11 D 2 _ 9 C 5 3 _ 0 0 A A 0 0 3 D 8 6 9 5 _ _ I N C L U D E D _ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 class CWzdDirDlg { p u b l i c : C W z d D i r D l g ( ) ; virtual ~CWzdDirDlg(); CString GetDirectory( CWnd *pParent = NULL, LPCSTR lpszRoot = "c:\\", LPCSTR lpszTitle = "Please Pick a Directory" ); static CString m_sRootDir; } ; int CALLBACK BrowseCallbackProc( HWND hwnd,UINT uMsg,LPARAM lp, LPARAM pData ); # e n d i f // !defined( AFX_WZDDRDLG_H__D52656D3_8A25_11D2_9C53_00AA003D8695__INCLUDED_ ) // WzdDrDlg.cpp: implementation of the CWzdDirDlg class. / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #include "stdafx.h" #include "WzdDrDlg.h" #include "Shlobj.h" CString CWzdDirDlg::m_sRootDir; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Construction/Destruction / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / C W z d D i r D l g : : C W z d D i r D l g ( ) { } C W z d D i r D l g : : ~ C W z d D i r D l g ( ) { } CString CWzdDirDlg::GetDirectory( CWnd *pParent,LPCSTR lpszRoot,LPCSTR lpszTitle ) { CString str; 第 7 章第对话框和对话条第第195下载BROWSEINFO bi; bi.hwndOwner = pParent -> m_hWnd; // owner of created dialog box bi.pidlRoot = 0; // unused bi.pszDisplayName = 0; // buffer to receive name displayed by // folder (not a valid path) b i . l p s z Title = lpszTi t l e ; // title is “Browse for Folder”, // this is an instruction bi.lpfn = BrowseCallbackProc; // callback routine called when dialog // has been initialized bi.lParam = 0; // passed to callback routine bi.ulFlags = B I F _ R E T U R N O N LYFSDIRS | // only allow user to select // a directory B I F _ S TATUSTEXT | // create status text field we will be // writing to in callback // BIF_BROWSEFORCOMPUTER| // only allow user to select a computer // BIF_BROWSEFORPRINTER | // only allow user to select a printer // BIF_BROWSEINCLUDEFILES| // displays files too which user // can pick // BIF_DONTGOBELOWDOMAIN| // when user is exploring the “Entire // Network” they are not allowed // into any domain 0; m_sRootDir = lpszRoot; LPITEMIDLIST lpItemId = ::SHBrowseForFolder( &bi ); if ( lpItemId ) { LPTSTR szBuf = str. G e t B u ffer( MAX_PATH ); ::SHGetPathFromIDList( lpItemId, szBuf ); ::GlobalFree( lpItemId ); s t r. R e l e a s e B u ff e r ( ) ; } return str; } int CALLBACK BrowseCallbackProc( HWND hwnd,UINT msg,LPARAM lp, LPARAM pData ) { TCHAR buf[MAX_PAT H ] ; s w i t c h ( m s g ) { // when dialog is first initialized, change directory to one chosen above case BFFM_INITIALIZED: strcpy( buf,CWzdDirDlg::m_sRootDir ); ::SendMessage( hwnd,BFFM_SETSELECTION,TRUE,( LPARAM )buf ); b r e a k ; // if you picked BIF_STATUSTEXT above, you can fill status here case BFFM_SELCHANGED: if ( ::SHGetPathFromIDList( ( LPITEMIDLIST ) lp ,buf ) ) 196第第第二部分第用户界面实例 下载SendMessage( hwnd,BFFM_SETSTAT U S T E X T,0,( LPARAM )buf ); b r e a k ; } return 0; } 7.6 实例26:子对话框 1. 目标 使用一个就像是另一个对话框中通用控件一样的对话框,如图 7 - 5所示。 图7-5 另一个对话框中的子对话框 2. 策略 考虑到对话框自身的典型用途是作为一个弹出窗口,很难想到它也可以用做一个父窗口 甚至另一个对话框的子窗口,因此本例只是创建一个作为另一个对话框的子窗口的对话框。 3. 步骤 为一个弹出的对话框添加一个子对话框 用对话框编辑器为每一个子对话框创建一个对话框模板,设置它们的属性为 C h i l d,而且 无边界。 使用C l a s s Wi z a r d为这些模板创建一个对话框类。 在父对话框类中嵌入每一个子对话框类: CWzdDlg1 m_wzd1; CWzdDlg2 m_wzd2; : : 用Dialog Editor 为父对话框模板增加一个具有一定尺寸的静态控件,其位置和子对话框 所希望出现的位置相同,这将作为一个占位符 ( P l a c e h o l d e r )。 用C l a s s Wi z a r d为父对话框增加一个WM_INITDIALOG 消息处理函数,并获得这个静态控 件的矩形区域: CRect rect; m _ c t r l S t a t i c . G e t WindowRect( &rect ); S c r e e n ToClient( &rect ); 接下来在同一个消息处理函数中,创建子对话框并且将它们全部添加在静态控件上,确 保只有其中一个可见: m_dlg1.Create( IDD_DIALOG2,this ); m _ d l g 1 . S h o w Window( SW_SHOW ); 第 7 章第对话框和对话条第第197下载 所看到的控件依赖 于是Button 1还是 Button 2按下m _ d l g 1 . M o v e Window( &rect ); m_dlg2.Create( IDD_DIALOG3,this ); m _ d l g 2 . S h o w Window( SW_HIDE ); m _ d l g 2 . M o v e Window( &rect ); 根据其他一些控件,例如按钮和标签控件,来改变可见的子对话框。 在子对话框自己的类中处理来自这些子对话框的命令。 4. 注意 ■ 可以用标签控件来控制哪个对话框可见,然而 可以使用 CPropertySheet 和 CPropertyPage 类来获得更好的效果,这些类提供给用户按照这种思路来创建的其他功能。事 实上,如果这个对话框上不是所有的控件都根据条件而改变,那么用户应该只用这种方法。 ■ A p p Wizard 实际上就是用这种方法来显示它的页码而不是使用具有向导选项的属性表。 5. 使用光盘时注意 运行随书附带光盘上的工程时,单击 Te s t按钮然后单击W z d菜单命令打开一个对话框,然 后单击Button 1或Button 2按钮来显示特定的控件系列。 7.7 实例27:子属性表 1. 目标 使用一个就像是另一个对话框中通用控件一样的属性表,如图 7 - 6所示。 2. 策略 尽管Dilog Editor允许为一个对话 框模板添加标签控件,然而它创建的 控件除了告诉用户什么标签被按下以 外,将不会具有任何其他的功能。这 时就只有用户自己显示一个子对话框, 如前一个实例所示。但本例并不按照 这种方法,而是使用 Property Sheet控 件并尽量避免使用其提供的功能。本 例将使用C P r o p e r t y S h e e t来创建该Property Sheet,创建时采用无模式风格然后将它添加到对话 框上。 3. 步骤 为对话框增加属性表 用D i a l o g E d i t o r为每一个属性表页创建对话框模板,设置每一个模板为 C h i l d属性并且没有 边界。 用C l a s s Wi z a r d为每一个模板创建属性页类并且都从 CPropertyPage 中派生。 在对话框类中嵌入一个C P r o p e r t y S h e e t类。 用Dialog Editor为对话框模板增加一个带尺寸的静态控件,放在属性表所希望出现的位 置。 用C l a s s Wi z a r d为对话框增加一个WM_INITDIALOG 消息处理函数,并获得这个静态控件 的尺寸和位置: 198第第第二部分第用户界面实例 下载 图7-6 属性表用做对话框控件 该属性表单 就像其他控 件一样被加 入对话框CRect rect; m _ c t r l S t a t i c . G e t WindowRect( &rect ); S c r e e n ToClient( &rect ); 在这个O n I n i t D i a l o g函数中,为每一个属性表创建并添加页面: m_pFirstPage = new CFirstPage; m_pSecondPage = new CSecondPage; m_sheet.AddPage( m_pFirstPage ); m_sheet.AddPage( m_pSecondPage ); 最后,创建属性表并且把它添加在静态控件上: m_sheet.Create( this,WS_VISIBLE|WS_CHILD ); m _ s h e e t . M o v e Window( &rect ); 4. 注意 用户也可以用这种方法在一个对话框中嵌入一个属性表向导,由于属性表向导的无模式 窗体的标题栏中没有关闭或最小化按钮,因此可以用一个简单的方法来增加这些按扭:创建 一个带关闭和最小化按扭的对话框,并且将一个具有 Wi z a r d风格的属性表控件嵌入其上。 5. 使用光盘时注意 运行随书附带光盘上的工程时,单击 Te s t按钮然后单击W z d菜单命令打开一个对话框,这 个对话框中包含有其他一些控件 ,其中就有属性表控件。 第 7 章第对话框和对话条第第199下载下载下载 第 8 章 控 件 窗 口 控件窗口是允许用户与应用程序进行交互的按钮、列表框和滚动条。当创建对话框时, 使用对话框编辑器定义的控件窗口也随之创建。然而,有时某个控件的外观和位置并不尽如 人意,这时便可以对控件进行处理,本章的实例包括: 实例28 自己绘制的控件,将看到在使用与控件相关的其他额外开销时如何绘制自己的控件。 实例29 在窗口标题中添加按钮,将为一个窗口的标题添加一个按扭。 实例30 添加热键控件,将为用户的应用程序添加热键。 8.1 实例28:自己绘制的控件 1. 目标 自己绘制一个控件窗口。在这个实例中,将绘制一个列表框,选择的列表项目将产生缩 进,如图8 - 1所示。 2. 策略 首先使用属主绘制风格 ( o w n e r- d r a w n s t y l e )创建用户自己的控件,然后从 M F C 控件类中派生自己的控件类,接下来重载 D r a w I t e m ( )成员函数,在此可以用 M F C的 CDC 类和它的成员函数来绘制控件,本实 例中将绘制一个列表控件窗口。 3. 步骤 1) 创建一个新的列表框类 用C l a s s Wi z a r d从C L i s t B o x中派生出一个新的类。 2) 绘制列表框控件 用 C l a s s Wizard 重载C L i s t B o x类的D r a w I t e m ( )函数。 首先在D r a w I t e m ( )函数中封装( w r a p )从C D C类对象的D R AW I T E M S T R U C T结构中所获得 的设备环境的句柄: void CWzdListBox::DrawItem( LPDRAWITEMSTRUCT lpDIS ) { // get our device context and rectangle to draw to CDC dc; dc.Attach( lpDIS -> hDC ); CRect rect( lpDIS -> rcItem ); 下一步如何处理取决于怎样绘制这个控件。这可以有很多种情况。对于这个实例,只是 简单地采用C D C : : D r a w I t e m ( )来绘制列表框上的项目。但如果某个列表项被鼠标选取,那么将 用系统的高亮颜色来绘制该项目,同时它还将缩进 1 0个像素。 根据是否被选择来确定并设置列表项的前景色和背景颜色: 图8-1 自己绘制的列表框 该自绘制列表框 将使被选择的列 表项所在行缩进第 8 章第控 件 窗 口第第201下载 // if our item is selected, then set colors accordingly // and fill in background COLORREF bk = dc.GetBkColor(); COLORREF fg = dc.GetTe x t C o l o r ( ) ; if ( lpDIS -> itemState & ODS_SELECTED ) { bk = ::GetSysColor( COLOR_HIGHLIGHT ); fg = ::GetSysColor( COLOR_HIGHLIGHTTEXT ); } d c . S e t TextColor( fg ); 这一步为这个列表项绘制背景颜色: CBrush brush( bk ); dc.FillRect( &rect, &brush ); 接下来用C D C : : D r a w Te x t ( )函数来绘制这个列表项的文本,如果选择这行,那么缩进文本 的开始1 0个像素: if ( lpDIS -> itemState & ODS_SELECTED ) rect.left += 10; int nBkMode = dc.SetBkMode( TRANSPARENT ); CString str; G e t Text( lpDIS -> itemID,str ); d c . D r a w Text( str, &rect, DT_LEFT|DT_VCENTER ); 注意是从列表框控件本身获取要绘制的文本,一般来说,列表框并不能为一个属主绘制 控件维持该列表。但可以将这个控件设置为 Has String属性,它将在内部继续维护该列表,这 将允许继续使用这个控件类中的 A d d S t r i n g ( )函数。 最后需要在D r a w I t e m ( )函数中进行清除工作: d c . S e t TextColor( fg ); dc.SetBkMode( nBkMode ); d c . D e t a c h ( ) ; 此列表框控件类的完整程序代码可参考本实例结尾的程序清单— 列表框类。 3) 使用新的列表框类 为使用这个新的控件类,首先用对话框编辑器为对话框模板添加列表框,确保设置 O w n e r-Drawn Fixed和Has Strings属性。 如果还没有这样做,则使用 C l a s s Wi z a r d为该模板创建一个对话框。 使用C l a s s Wi z a r d为对话框类添加控件成员变量,然后使用用户的新类名替代 Class Wi z a r d 自动添加的C l i s t B o x类名,同时将该定义设置在 { { } }括号之外,使得Class Wi z a r d不会被它弄 糊涂,这个列表框将在下次调用 U p d a t e D a t a ( FA L S E )时被用户的列表框类子类化。 4. 注意 ■ 除Wi n d o w s列表框控件之外,其他可以属主绘制的控件包括:组合框、扩展的组合框、 列表控件、按钮、标签控件。列表控件和组合框控件都允许用户设置其中每一个项目的尺寸, 这可以通过Dialog Editor设置O w n e r-Drawn Va r i a b l e风格来实现。 ■ 当使用O w n e r-Drawn Va r i a b l e风格时,必须重载用户新控件类的 M e a s u r e I t e m ( )成员函 数并返回每一个项目所希望的尺寸。重载 M e a s u r e I t e m ( )成员函数的例子可参考实例 7。■ 如果使用Dialog Editor还将该控件设置为 S o r t风格,则必须同时在新的控件类中重载 S o r t I t e m ( )成员函数。 ■ 实际上,在这里“属主绘制”这个词有一点用词不当,从技术上来说,本实例中的控 件是自己绘制的控件,“属主绘制”最初的含义是某个控件的属主 ( o w n e r ),例如一个对话框, 可以绘制它所拥有的那个控件。然而,在这个实例中,控件发出的用于通知它的属主绘制 ( W M _ D R AW I T E M )时间已到的消息,将会反射回到该控件的 M F C类并在此进行处理,因此从 技术上说,这个正被 M F C类绘制的控件拥有这个控件。然而,由于在 M F C世界中这两种情况 被认为是同一个术语,本例中的控件应该被认为是它自己绘制自己或自绘制 ( s e l f - d r a w n )的。 关于消息反射的详细内容,请参阅第 1章。 ■ 当只是编写一个W M _ PA I N T消息处理函数来绘制控件时,为什么要费心考虑控件的属 主绘制风格呢?原因是想使绘制工作和控件的其他功能同步。例如,一个属主绘制组合框将 会告诉用户何时绘制它的列表框,而且更重要的是,它将会告诉用户绘制列表框的位置,使 得鼠标在屏幕上单击该位置时,将会与该控件上相应的项目相关联。甚至一个属主绘制的按 钮控件将通知用户何时绘制一个已按下的或未按下的按钮,使得与当单击它时所创建的命令 消息相符。 5. 使用光盘时注意 运行随书附带光盘上的工程时,单击 Te s t按钮,然后再单击W z d菜单命令,打开一个带有 列表框的对话框。注意到在单击列表框中的任何一个列表项时都将会导致文本框中的相应一 行缩进。 6. 程序清单— 列表框类 #if !defined( AFX_WZDLISTBOX_H__41500055_E450_11D1_9B7D_00AA003D8695__INCLUDED_ ) #define AFX_WZDLISTBOX_H__41500055_E450_11 D 1 _ 9 B 7 D _ 0 0 A A 0 0 3 D 8 6 9 5 _ _ I N C L U D E D _ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 // WzdListBox.h : header file / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdListBox window class CWzdListBox : public CListBox { // Construction p u b l i c : C W z d L i s t B o x ( ) ; // Attributes p u b l i c : // Operations p u b l i c : // Overrides // ClassWizard generated virtual function overrides 202第第第二部分第用户界面实例 下载// {{AFX_VIRTUAL( CWzdListBox ) p u b l i c : virtual void DrawItem( LPDRAWITEMSTRUCT lpDrawItemStruct ); // }}AFX_VIRT U A L // Implementation p u b l i c : virtual ~CWzdListBox(); // Generated message map functions p r o t e c t e d : // {{AFX_MSG( CWzdListBox ) // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Developer Studio will insert additional declarations immediately // before the previous line. # e n d i f // !defined( AFX_WZDLISTBOX_H__41500055_E450_11D1_9B7D_00AA003D8695__INCLUDED_ ) // WzdListBox.cpp : implementation file / / #include "stdafx.h" #include "wzd.h" #include "WzdListBox.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdListBox C W z d L i s t B o x : : C W z d L i s t B o x ( ) { } C W z d L i s t B o x : : ~ C W z d L i s t B o x ( ) { } BEGIN_MESSAGE_MAP( CWzdListBox, CListBox ) // {{AFX_MSG_MAP( CWzdListBox ) // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) 第 8 章第控 件 窗 口第第203下载/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdListBox message handlers void CWzdListBox::DrawItem( LPDRAWITEMSTRUCT lpDIS ) { // get our device context and rectangle to draw to CDC dc; dc.Attach( lpDIS -> hDC ); CRect rect( lpDIS -> rcItem ); // if our item is selected, then set colors accordingly // and fill in background COLORREF bk = dc.GetBkColor(); COLORREF fg = dc.GetTe x t C o l o r ( ) ; if ( lpDIS -> itemState & ODS_SELECTED ) { bk = ::GetSysColor( COLOR_HIGHLIGHT ); fg = ::GetSysColor( COLOR_HIGHLIGHTTEXT ); } d c . S e t TextColor( fg ); CBrush brush( bk ); dc.FillRect( &rect, &brush ); // draw text if ( lpDIS -> itemState & ODS_SELECTED ) rect.left += 10; int nBkMode = dc.SetBkMode( TRANSPARENT ); CString str; G e t Text( lpDIS -> itemID,str) ; d c . D r a w Te x t ( s t r, &rect, DT_LEFT|DT_VCENTER); // cleanup d c . S e t TextColor( fg ); dc.SetBkMode( nBkMode ); d c . D e t a c h ( ) ; } 8.2 实例29:在窗口标题中添加按钮 1. 目标 在一个窗口的标题栏中添加一个按钮,如图 8 - 2所示。 2. 策略 从技术上来说,本例将要创建的按钮不是一个控件窗口。 实际上要创建的是一个“假的”按钮,该按钮将由用户自己 绘制并且处理鼠标消息。系统通过响应两个消息 ( W M _ N C PA I N T和W M _ A C T I VAT E )来绘制窗口标题,在每个消息 使用C D C类的绘制功能之后,用户将对本例中的按钮进行绘 制,同时还将处理另一个消息 ( W M _ N C L B U T TO N D O W N ) 204第第第二部分第用户界面实例 下载 图8-2 标题栏中的一个按钮 在对话框的标题中 放置一个按钮来确定用户是否按下本例中的“按钮”。 3. 步骤 1) 为标题按钮设置位图 用位图编辑器(Bitmap Editor)创建两个位图:一个用于按钮弹起时,另一个用于按钮按下 时。可以在位图边界使用高亮度和阴影颜色来达到这一效果,背景颜色采用灰色— 装载位 图时代替这种颜色。对于一个用于通常窗口标题中常规尺寸的按钮,可以设置其尺寸为 1 6× 1 6像素,对于一个工具栏窗口的小尺寸的按钮,可设置其尺寸为 1 2×1 2像素。 在自己的对话框类中的构造函数中,将这两个位图装载到嵌入的 C B i t m a p成员变量中。利 用随书附带光盘上为该实例提供的 C W z d B i t m a p类,可以将当前用于按钮的系统颜色替代为灰 色背景色,还可以将按钮的状态初始化为未按下 ( u n p r e s s e d )。 CWzdDialog::CWzdDialog( CWnd* pParent /* = NULL*/ ) : CDialog( CWzdDialog::IDD, pParent ) { // {{AFX_DATA_INIT( CWzdDialog ) // }}AFX_DATA _ I N I T m_bPressed = FA L S E ; m_bitmapPressed.LoadBitmapEx ( IDB_PRESSED_BITMAP, TRUE ); m_bitmapUnpressed.LoadBitmapEx ( IDB_UNPRESSED_BITMAP, TRUE ); } 注意 这个位图类( C W z d B i t m a p )在本系列丛书中的第一本书《Visual C++ MFC编程实 例》中创建。 2) 绘制标题按钮 用C l a s s Wi z a r d为该类添加W M _ N C PA I N T和W M _ A C T I VAT E消息处理函数,从这两个消息 处理函数中调用一个D r a w B u t t o n ( )函数,该函数将用于绘制按钮。接下来将编写 DrawButton () 函数的代码: void CWzdDialog::OnNcPaint() { // draw caption first C D i a l o g : : O n N c P a i n t ( ) ; // then draw button on top D r a w B u t t o n ( ) ; } void CWzdDialog::OnActivate( UINT nState, CWnd* pWndOther, BOOL bMinimized ) { CDialog::OnActivate( nState, pWndOther, bMinimized ); D r a w B u t t o n ( ) ; } 创建 DrawButton() 函数,这个函数将决定绘制哪个按钮位图并且使用如下所示的 S t r e t c h B l t ( )函数绘制它: void CWzdDialog::DrawButton() 第 8 章第控 件 窗 口第第205下载206第第第二部分第用户界面实例 下载 { // if window isn’t visible or is minimized, skip if ( !IsWi n d o w Visible() || IsIconic() ) r e t u r n ; // get appropriate bitmap CDC memDC; CDC* pDC = GetWi n d o w D C ( ) ; memDC.CreateCompatibleDC( pDC ); memDC.SelectObject( m_bPressed ? &m_bitmapPressed : &m_bitmapUnpressed ); // get button rect and convert into non-client area coordinates CRect rect, rectWnd; G e t B u t t o n R e c t ( r e c t ) ; G e t Wi n d o w R e c t ( r e c t W n d ) ; r e c t . O ffsetRect( -rectWnd.left, -rectWnd.top ); // draw it pDC -> StretchBlt( rect.left, rect.top, rect.Wi d t h ( ) , rect.Height(), &memDC, 0, 0, m_bitmapPressed.m_Wi d t h , m_bitmapPressed.m_Height, SRCCOPY ); m e m D C . D e l e t e D C ( ) ; ReleaseDC( pDC ); } 注意到从G e t Wi n d o w D C ( )中获得了设备环境,这样设备环境就可以绘制到整个窗口,包 括标题所在的非客户区。这里使用 S t r e t c h B l t ( )是因为用户并不能确定使用何种分辨率来绘制 对话框。同时,本例使用了另一个称为 G e t B u t t o n R e c t ( )局部辅助函数来确定按钮的尺寸,该 函数将在下一步创建。 创建G e t B u t t o n R e c t ( )函数以确定按钮的尺寸,这将依靠系统告知自己的按钮的相应尺寸, 并使用G e t S y s t e m M e t r i c s ( )函数来获得这些尺寸: void CWzdDialog::GetButtonRect( CRect& rect ) { G e t Wi n d o w R e c t ( & r e c t ) ; // for small caption use SM_CYDLGFRAME rect.top += GetSystemMetrics(SM_CYFRAME)+1; // for small caption use SM_CYSMSIZE rect.bottom = rect.top + GetSystemMetrics( SM_CYSIZE )-4; // for small caption use SM_CXDLGFRAME rect.left = rect.right - GetSystemMetrics( SM_CXFRAME ) - ( 2 * // set to number of buttons already in caption + 1 GetSystemMetrics( SM_CXSIZE ) )-1; // for small caption use SM_CXSMSIZE // for small caption use SM_CXSMSIZE rect.right = rect.left + GetSystemMetrics(SM_CXSIZE)-3; }3) 允许用户单击按钮 为 了确 定 用 户 是 否 单击 本 例 中 的 “ 按钮 ”, 使 用 C l a s s Wi z a r d为 这 个 类 添加 W M _ N C L B U T TO N D O W N消息处理函数,它可以用来检查用户是否已经单击了标题栏,如果 是,则进一步检查是否单击其中的按钮所在之处。注意在这里又用到了 G e t B u t t o n R e c t ( )函数: void CWzdDialog::OnNcLButtonDown( UINT nHitTest, CPoint point ) { if ( nHitTest == HTCAPTION ) { // see if in area we reserved for button CRect rect; GetButtonRect( rect ); if ( rect.PtInRect( point ) ) { m_bPressed = !m_bPressed; D r a w B u t t o n ( ) ; } } CDialog::OnNcLButtonDown( nHitTest, point ); } 此对话框类的完整程序代码可参考本实例结尾的程序清单— 对话框类。 4. 注意 ■ 本 例 中 不 仅 需 要 处 理 W M _ N C PAINT 消 息 来 绘 制 按 钮 , 而 且 还 需 要 处 理 W M _ A C T I VAT E消息。这是因为当激活窗口时, W M _ A C T I VAT E消息将导致系统重绘标题颜 色(当窗口激活时,窗口标题的颜色从暗灰色变为亮色 )。 ■ 确保在系统绘制标题然后绘制用户自己的“按钮”,本例中所完成的一切就是在系统所 绘制的内容之上再绘制一个位图。 ■ 本实例演示了一个保持被按下状态的按钮,为了在单击该按钮时进行切换,应该在处 理W M _ N C L B U T TO N D O W N时一直绘制表示按钮“按下”的位图,然后添加一个新的 W M _ N C L B U T TO N U P消息处理函数来绘制表示按钮“弹起”的位图。 ■ 同样的方法可以用于任何一种控件,只要不介意为每一种控件添加各自的逻辑代码。 5. 使用光盘时注意 运行随书附带光盘上的工程时,单击 Te s t按钮,然后再单击W z d菜单命令,打开一个在标 题中有按钮的对话框。单击该按钮,使之保持按下或未按下状态。 6. 程序清单— 对话框类 #if !defined( AFX_WZDDIALOG_H__A76FC804_D3BD_11D1_9B67_00AA003D8695__INCLUDED_ ) #define AFX_WZDDIALOG_H__A76FC804_D3BD_11 D 1 _ 9 B 6 7 _ 0 0 A A 0 0 3 D 8 6 9 5 _ _ I N C L U D E D _ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 // WzdDialog.h : header file / / #include "WzdBitmap.h" 第 8 章第控 件 窗 口第第207下载208第第第二部分第用户界面实例 下载 / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdDialog dialog class CWzdDialog : public CDialog { // Construction p u b l i c : CWzdDialog( CWnd* pParent = NULL ); // standard constructor // Dialog Data // {{AFX_DATA( CWzdDialog ) enum { IDD = IDD_WZD_DIALOG }; // NOTE: the ClassWizard will add data members here // }}AFX_DATA // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CWzdDialog ) p r o t e c t e d : virtual void DoDataExchange( CDataExchange* pDX ); // DDX/DDV support // }}AFX_VIRT U A L // Implementation p r o t e c t e d : // Generated message map functions // {{AFX_MSG( CWzdDialog ) afx_msg void OnNcPaint(); afx_msg void OnNcLButtonDown( UINT nHitTest, CPoint point ); afx_msg void OnActivate( UINT nState, CWnd* pWndOther, BOOL bMinimized ); // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : BOOL m_bPressed; CWzdBitmap m_bitmapPressed; CWzdBitmap m_bitmapUnpressed; void DrawButton(); void GetButtonRect( CRect &rect ); } ; // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Developer Studio will insert additional declarations immediately // before the previous line. # e n d i f // !defined( AFX_WZDDIALOG_H__A76FC804_D3BD_11D1_9B67_00AA003D8695__INCLUDED_ ) // WzdDialog.cpp : implementation file / / #include "stdafx.h" #include "wzd.h" #include "WzdDialog.h"#ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f #define HTBUTTON 32 / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdDialog dialog CWzdDialog::CWzdDialog( CWnd* pParent /* = NULL*/ ) : CDialog( CWzdDialog::IDD, pParent ) { // {{AFX_DATA_INIT( CWzdDialog) // NOTE: the ClassWizard will add member initialization here // }}AFX_DATA _ I N I T m_bPressed = FA L S E ; m_bitmapPressed.LoadBitmapEx( IDB_PRESSED_BITMAP,TRUE ); m_bitmapUnpressed.LoadBitmapEx( IDB_UNPRESSED_BITMAP,TRUE ); } void CWzdDialog::DoDataExchange( CDataExchange* pDX ) { CDialog::DoDataExchange( pDX ); // {{AFX_DATA_MAP( CWzdDialog ) // NOTE: the ClassWizard will add DDX and DDV calls here // }}AFX_DATA _ M A P } BEGIN_MESSAGE_MAP( CWzdDialog, CDialog ) // {{AFX_MSG_MAP( CWzdDialog ) O N _ W M _ N C PA I N T ( ) O N _ W M _ N C L B U T TO N D O W N ( ) O N _ W M _ A C T I VAT E ( ) // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdDialog message handlers void CWzdDialog::OnNcPaint() { // draw caption first C D i a l o g : : O n N c P a i n t ( ) ; // then draw button on top D r a w B u t t o n ( ) ; } // caption is also redrawn with WM_ACTIVATE message for color change void CWzdDialog::OnActivate( UINT nState, CWnd* pWndOther, BOOL bMinimized ) 第 8 章第控 件 窗 口第第209下载{ CDialog::OnActivate( nState, pWndOther, bMinimized ); D r a w B u t t o n ( ) ; } void CWzdDialog::OnNcLButtonDown( UINT nHitTest, CPoint point ) { if ( nHitTest == HTCAPTION ) { // see if in area we reserved for button CRect rect; GetButtonRect( rect ); if ( rect.PtInRect( point ) ) { m_bPressed = !m_bPressed; D r a w B u t t o n ( ) ; } } CDialog::OnNcLButtonDown( nHitTest, point ); } void CWzdDialog::GetButtonRect( CRect& rect ) { G e t WindowRect( &rect ); // for small caption use SM_CYDLGFRAME rect.top += GetSystemMetrics( SM_CYFRAME )+1; // for small caption use SM_CYSMSIZE rect.bottom = rect.top + GetSystemMetrics( SM_CYSIZE )-4; // for small caption use SM_CXDLGFRAME rect.left = rect.right - GetSystemMetrics( SM_CXFRAME ) - // set to number of buttons already in caption + 1 ( 2 * // for small caption use SM_CXSMSIZE GetSystemMetrics( SM_CXSIZE ) )-1; // for small caption use SM_CXSMSIZE rect.right = rect.left + GetSystemMetrics( SM_CXSIZE )-3; } void CWzdDialog::DrawButton() { // if window isn’t visible or is minimized, skip if ( !IsWi n d o w Visible() || IsIconic() ) r e t u r n ; // get appropriate bitmap CDC memDC; CDC* pDC = GetWi n d o w D C ( ) ; memDC.CreateCompatibleDC( pDC ); 210第第第二部分第用户界面实例 下载第 8 章第控 件 窗 口第第211下载 memDC.SelectObject( m_bPressed ? &m_bitmapPressed : &m_bitmapUnpressed ); // get button rect and convert into non-client area coordinates CRect rect, rectWnd; GetButtonRect( rect ); G e t WindowRect( rectWnd ); r e c t . O ffsetRect( -rectWnd.left, -rectWnd.top ); // draw it pDC -> StretchBlt( rect.left, rect.top, rect.Width(), rect.Height(), &memDC, 0, 0, m_bitmapPressed.m_Width, m_bitmapPressed.m_Height, SRCCOPY ); m e m D C . D e l e t e D C ( ) ; ReleaseDC( pDC ); } 8.3 实例30:添加热键控件 1. 目标 在自己的应用程序中添加热键。 2. 策略 热键是一种小有名气的功能,它允许用户与当前处于非激活状态的应用程序进行交互。 当用户按下一个热键时,经常是 S h i f t、C o n t r o l、A l t和其他一些字母键的组合,则用户自己的 应用程序可以被激发或可以接收一个 W M _ H O T K E Y消息来处理。 Dialog Editor允许在对话框模板上添加一个热键控件,但这并不是一个真正的热键控件。 相反,它实际上更像是一个热键编辑框,将各种键 (例如S h i f t / A l t / C o n t r o l )的组合转换成它们 的等效文本(当这个控件具有当前焦点时,按下 S h i f t键和F键将会使S h i f t + F在控件上出现)。而 这个控件的输出将可以直接用于 Windows API中并注册一个热键。 必须在系统中注册热键,这样当按下热键时才会激活自己的应用程序,这里将使用 W M _ S E T H O T K E Y完成注册。当按下一个热键组合时,将会把 W M _ S E T H O T K E Y消息发送给 用户的应用程序,这里将使用 : : R e g i s t e r H o t K e y ( )函数。 3. 步骤 1) 为用户提示热键 用Dialog Editor在用户自己的对话框模板中添加一个热键编辑框。 用Class Wi z a r d为这个对话框模板类添加热键控件成员变量。 用下面的代码来从H o t k e y控件中获得热键的键码: WORD m_wVkCode; WORD m_wModifier; m _ c t r l H o t K e y.GetHotKey( m_wVkCode, m_wModifier ); 2) 注册用于激活应用程序的热键 发送一个带有以上所返回的热键值的 W M _ R E T H O T K E Y消息到主窗口: AfxGetMainWnd() -> SendMessage(WM_SETHOTKEY, ( WPARAM )MAKEWORD( m_wVkCode,m_wModifier ) ); 3) 注册发送W M _ H O T K E Y消息的热键为通过R e g i s t e r H o t K e y ( )函数来使用热键编辑框控件的输出,要求首先将由 G e t H o t K e y ( )函 数返回的组合键( m o d i f i e r )转换成R e g i s t e r H o t K e y ( )函数可以使用的值: // must translate hot key control’s returned modifier to // RegisterHotKey format UINT mod = 0; if ( m_wModifier&HOTKEYF_ALT ) mod| = MOD_ALT; if ( m_wModifier&HOTKEYF_CONTROL ) mod| = MOD_CONTROL; if ( m_wModifier&HOTKEYF_SHIFT ) mod| = MOD_SHIFT; if ( m_wModifier&HOTKEYF_EXT ) mod| = MOD_WIN; m_wModifier = mod; 注册热键: : : R e g i s t e r H o t K e y ( AfxGetMainWnd() -> m_hWnd, // window to receive hot-key // notification 1 2 3 4 , // identifier of hot key m _ w M o d i f i e r, // key-modifier flags m _ w V k C o d e // virtual-key code ) ; 通过在相应的非子窗口 ( M a i n f r a m e和Child Frame是唯一合适的窗口)中手工添加代码处理 W M _ H O T K E Y消息: // to the message map ON_MESSAGE( WM_HOTKEY,OnHotKey ) L R E S U LT CMainFrame::OnHotKey( WPARAM wParam, LPARAM lParam ) { w P a r a m ; // id specified in RegisterHotKey() // (in this example: 1234) l P a r a m ; // virtual key code and modifiers of the hotkey return 0L; } 4. 注意 ■ 用户在允许输入哪些键作为热键上必须小心,因为组合键将取代任何其他应用程序的 快捷键的优先权。例如,如果用户指定 A LT + S组合键作为一个热键,即使该热键组合可能是 当前使用的应用程序用于保存工作的快捷键,它也不会发挥其快捷键作用,而只是以热键方 式或者激发用户自己的应用程序或者发出一个 W M _ H O T K E Y消息。 ■ 热键和快捷键的区别在于热键将允许用户和自己的应用程序之间进行交互,甚至于在 应用程序处于非激活时也可以进行交互。 5. 使用光盘时注意 运行随书附带光盘上的工程时,单击 Te s t按钮,然后再单击W z d菜单命令,打开一个允许 输入热键组合的对话框。单击 Make Active Application和O K,然后激活另一个应用程序并且 再此输入热键组合,则这个应用程序将会被再次激活。单击 Send WM_HOTKEY Message并在 C M a i n F r a m e的O n H o t K e y ( )函数中设置一个断点,然后按下热键组合可看到应用程序在这一断 点终止。 212第第第二部分第用户界面实例 下载下载下载 第 9 章 绘 图 在应用程序中,允许对位图和图标添加颜色和风格。由于所有的 Wi n d o w s界面看起来基 本上都相似,所以要使应用程序的外观看起来与他人的不同的唯一途径就是使用标记 ( l o g o )和 飞溅屏幕(splash screen)。在创建自己的控件和在 C A D应用程序中显示图形时,绘图显然也是 很重要的。 可以用本章中的一些实例给应用程序赋予一些非同寻常的特征,本章实例包括: 实例31 使用非散射( n o n d i ff u s e d )颜色,在本例中将看到如何用非散射的颜色绘图。在第 1 章中可以找到散射颜色的“悲伤经历” (sad story),但结论是对于图形应用程序,必须用非散 射颜色来绘图。 实例32 伸展位图,在本例中将伸展 ( s t r e t c h )一个现有的位图来填充某一特定区域。 实例33 抓取屏幕,在这里将看到从屏幕的某个区域抓取的图像来装载一个位图。 实例34 输出一个D I B位图文件,本例将输出一个位图到一台设备无关位图 ( D I B )文件。 9.1 实例31:使用非散射颜色 1. 目标 用非散射的颜色绘图,希望用户设置并保存他们自己的非散射的颜色选择方案,如图 9 - 1 所示。 2. 策略 为了在自己的应用程序中使用非散射 颜色绘图,需要创建、保存并使用自己的 调色板。为了做到这一点,本例将用 M F C 的C P a l e t t e类。为了提示用户进行颜色选择, 将根据用户的选择创建一个新的属性页, 该属性页对于每一种基本颜色 (红色、绿色 和蓝色)都有个滑动条,这样可以允许用户 通过拖动滑动条到所选择颜色密度来挑选 颜色。本例将用 A n i m a t e P a l e t t e ( )成员函数 来动态改变视窗中被选择的颜色。最后使 用G e t P r o f i l e B i n a r y ( )函数和WriteProfileBinary ()函数保存用户的颜色选择。 3. 步骤 1) 创建一个应用程序调色板 在C M a i n F r a m e类中定义一个颜色数组,并且在类的构造函数中将它们设置为一些缺省颜 色: COLORREF m_rgbColors[NUM_COLORS]; : : : C M a i n F r a m e : : C M a i n F r a m e ( ) 图9-1 非散射颜色选项{ m_rgbColors[COLOR1_COLOR] = RGB( 200,20,150 ); m_rgbColors[COLOR2_COLOR] = RGB( 0,200,100 ); } 接下来在C M a i n F r a m e中,添加一个用于从这些颜色中创建调色板的函数。这意味着需要 创建一个 C P a l e t t e 对象并使用 C P a l e t t e 中的 C r e a t e P a l e t t e ( ) 函数创建一个调色板。 C P a l e t t e : : C r e a t e P a l e t t e ( )需要一个L O G PA L E T T E结构作为参数,该结构中包含了所有希望加入 到调色板中的颜色,并使用以下代码初始化: void CMainFrame::CreatePalette() { L O G PALETTE *lp = ( LOGPALETTE * )new BYTE[sizeof( LOGPALETTE ) + ( NUM_COLORS * sizeof( PA L E T T E E N T RY ) )]; lp -> palVersion = 0x300; lp -> palNumEntries = NUM_COLORS; for ( int i = 0; i palPalEntry[i].peRed = GetRValue( m_rgbColors[i] ); lp -> palPalEntry[i].peGreen = GetGValue( m_rgbColors[i] ); lp -> palPalEntry[i].peBlue = GetBValue( m_rgbColors[i] ); // reserve for animation lp -> palPalEntry[i].peFlags = PC_RESERV E D ; } if ( m_pPalette ) delete m_pPalette; m_pPalette = new CPalette; m_pPalette -> CreatePalette( lp ); delete []lp; } 在系统注册表中加载任何程序选项后,从 C M a i n F r a m e的O n C r e a t e ( )函数中调用这个新的 C M a i n F r a m e: :C r e a t e P a l e t t e ( )函数,这些程序选项中很快就会包含颜色选项。 2) 用应用程序的调色板绘图 为使用自己的调色板绘图,必须首先将调色板选入自己设备环境中,然后再在这个调色 板中实现颜色。实现颜色 (realizing color)仅仅意味着Wi n d o w s试图将所有的应用程序调色板中 的颜色都加入到可用的系统调色板 (有关这方面的详细知识请参阅第 1章): // select palette into device context and realize colors CPalette *pOPalette = pDC -> SelectPalette( ( ( CMainFrame* )AfxGetMainWnd() ) -> GetPalette(),FALSE ); pDC -> RealizePalette(); 注意同时为C M a i n F r a m e添加一个称为G e t P a l e t t e ( )的封装函数,这个函数允许其他的类来 访问这个应用程序的调色板。 无论何时使用非散射的颜色绘图,都用 PA L E T T E I N D E X ( )宏来定义颜色,代码如下: CPen pen( PS_SOLID,3,PALETTEINDEX( COLOR1_COLOR ) ); CPen *pOPen = pDC -> SelectObject( &pen ); CBrush brush( PALETTEINDEX( COLOR2_COLOR ) ); CBrush *pOBrush = pDC -> SelectObject( &brush ); 214第第第二部分第用户界面实例 下载C O L O R 1 _ C O L O R和C O L O R 2 _ C O L O R两个值只是应用程序调色板的索引。 当用画笔和画刷绘制直线和图形时,可使用合适的非散射颜色。 在绘图完成之后,重新选择原来的画笔、画刷和调色板: pDC -> SelectObject( pOPen ); pDC -> SelectObject( pOBrush ); pDC -> SelectPalette( pOPalette,FALSE ); 3) 创建一个颜色选项属性页 用Developer Studio创建一个新的对话框模板。设置这个模板为具有细框架的子风格。这 个模板的标题将与属性页标签上出现的标题一样。 用Dialog Editor添加如图9 - 1所示的一个图像控件和三个滑动条,也可以只用本书附带光 盘上提供给该实例的模板。 用C l a s s Wi z a r d为从C P r o p e r t y P a g e中派生的模板添加一个新的属性页。 添加该属性页到应用程序并作为首选项: void CMainFrame::OnOptionsPreferences() { CPropertySheet sheet( _T( "Preferences" ),this ); m_pColorPage = new CColorPage; sheet.AddPage( m_pColorPage ); m_pColorPage -> m_rgbColors[COLOR1_COLOR] = m _ r g b C o l o r s [ C O L O R 1 _ C O L O R ] ; m_pColorPage -> m_rgbColors[COLOR2_COLOR] = m _ r g b C o l o r s [ C O L O R 2 _ C O L O R ] ; s h e e t . D o M o d a l ( ) ; delete m_pColorPage; } 在此省略如何将应用程序的当前颜色传递到该页,以及滑动条是如何改变这些值的细节, 请参考本实例结尾的程序清单— 主框架类和程序清单— 颜色属性页类,在此介绍了滑动 条如何创建一个需要加入系统调色板的新 R G B颜色值的内容。 保存系统调色板中的新R G B颜色,可以通过使用如下所示的 C P a l e t t e的A n i m a t e P a l l e t t e ( )函 数来完成: // select our palette into a device context CDC *pDC = GetDC(); CMainFrame *pFrame = ( CMainFrame * )AfxGetMainWnd(); CPalette *pPalette = pFrame -> GetPalette(); CPalette *pOPalette = pDC -> SelectPalette( pPalette,FALSE ); // add new color to palette using animation PA L E T T E E N T RY pentry; p e n t r y.peRed = GetRValue( m_rgbColors[i] ); p e n t r y.peGreen = GetGValue( m_rgbColors[i] ); p e n t r y.peBlue = GetBValue( m_rgbColors[i] ); 第 9 章第绘第第图第第215下载p e n t r y.peFlags = PC_RESERVED; pPalette -> AnimatePalette( i,1,&pentry ); // reselect old palette pDC -> SelectPalette( pOPalette,FALSE ); 注意,即使A n i m a t e P a l e t t e ( )函数中的某一个参数不使用一个设备环境,和它一起使用的 调色板必须自己被选入设备环境 (MUST ITSELF BE SELECTED INTO A DEVICE CONTEXT) 来使该函数工作。换句话说,必须将 P P a l e t t e选入到p D C中,以使得A n i m a t e P a l e t t e ( )可以正常 工作。 4) 保存并恢复颜色选项 为了保存应用程序的颜色以备下次使用,无论当前使用什么函数,都需要添加下面几行 来保存用户的颜色选项: UINT size = sizeof( m_rgbColors ); AfxGetApp() -> WriteProfileBinary( "Settings","Colors", ( BYTE* )m_rgbColors, size); 为了恢复用户的颜色选项,无论当前使用什么函数,都需要添加下面几行来恢复用户的 颜色选项: BYTE *p; UINT size; if ( AfxGetApp() -> GetProfileBinary ( "Settings","Colors", &p,&size ) ) { memcpy( m_rgbColors,p,size ); delete []p; } 4. 注意 ■ M F C应用程序的颜色缺省为散射颜色,这种颜色是由三种基本颜色组合形成的。但是 对于某些应用程序,特别对于一个 C A D应用程序,这是不可接受的,因为散射颜色容易模糊。 散射颜色和非散射颜色的区别,请参阅第 1章。 ■ 用户可能会注意到在运行光盘中的示例时, A n i m a t e P a l e t t e ( )不仅改变颜色属性页中矩 形框的颜色,而且改变视的颜色。因为这是直接对系统调色板进行修改,而应用程序则是从 系统调色板中获取其各种颜色。在用户视中的每一个像素都指向系统调色板中的一种颜色 — 如果用户改变了系统调色板中的颜色,那么也改变了像素的颜色。 ■ 用户可能会注意到当另一个应用程序激活时,在应用程序中的非散射的颜色将会发生 畸变。这是因为另一个应用程序已经接管了系统调色板并且将用户应用程序中的颜色丢弃。可 以通过处理在C M a i n F r a m e中的两个消息来消除畸变。这两个消息是: W M _ U P D AT E PA L E T T E 和W M _ O N Q U E RY N E W PA L E T T E。处理这个功能的推荐方法是使用新的系统调色板并用 UpdateColor ()函数来刷新用户应用程序的颜色。然而,U p d a t e C o l o r ( )函数较慢并且容易产生颜 色错误,因而许多图形应用程序在处于后台非激活状态时都将忽略颜色所发生的变化,或者只 是通知自己的应用程序通过调用 W M _ Q U E RY N E W PA L E T T E中的R e d r a w Wi n d o w ( )消息处理函 数重新绘制自己,并且使用新的系统调色板。该函数将导致主窗口和其他的子窗口,特别是视, 重新绘制它们。 216第第第二部分第用户界面实例 下载5. 使用光盘时注意 运行随书附带光盘上的工程时,将会注意到视由两个颜色方块所填充。单击 O p t i o n s和 P r e f e r e n c e s菜单命令打开一个首选项属性表单,然后拖动任何一个滑动条,注意到颜色方块 逐渐变成另外一种非散射的颜色。如果用户退出并重新进入应用程序,将会注意到应用程序 恢复上一次退出之前保存的颜色。 6. 程序清单— 主框架类 // MainFrm.h : interface of the CMainFrame class / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #if !defined( AFX_MAINFRM_H__CA9038EA_BODF_11D1_A18C_DCB3C85EBD34__INCLUDED_ ) #define AFX_MAINFRM_H__CA9038EA_BODF_11 D 1 _ A 1 8 C _ D C B 3 C 8 5 E B D 3 4 _ _ I N C L U D E D _ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 #include "colorpage.h" class CMainFrame : public CMDIFrameWnd { D E C L A R E _ D Y N A M I C ( C M a i n F r a m e ) p u b l i c : C M a i n F r a m e ( ) ; // Attributes p u b l i c : // Operations p u b l i c : void LoadOptions(); void SaveOptions(); void SetColorRef( int id,COLORREF rgb ){m_rgbColors[id] = rgb;}; CPalette *GetPalette(){return m_pPalette;}; // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CMainFrame ) virtual BOOL PreCreateWindow( CREATESTRUCT& cs ); // }}AFX_VIRT U A L // Implementation p u b l i c : virtual ~CMainFrame(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump( CDumpContext& dc ) const; # e n d i f 第 9 章第绘第第图第第217下载protected: // control bar embedded members CStatusBar m_wndStatusBar; C ToolBar m_wndTo o l B a r ; // Generated message map functions p r o t e c t e d : // {{AFX_MSG( CMainFrame ) afx_msg int OnCreate( LPCREATESTRUCT lpCreateStruct ); afx_msg void OnClose(); afx_msg void OnOptionsPreferences(); // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : COLORREF m_rgbColors[NUM_COLORS]; CColorPage *m_pColorPage; CPalette *m_pPalette; void CreatePalette(); } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Developer Studio will insert additional declarations immediately // before the previous line. # e n d i f // !defined( AFX_MAINFRM_H__CA9038EA_BODF_11D1_A18C_DCB3C85EBD34__INCLUDED_ ) // MainFrm.cpp : implementation of the CMainFrame class / / #include "stdafx.h" #include "Wzd.h" #include "WzdProject.h" #include "MainFrm.h" #include #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CMainFrame IMPLEMENT_DYNAMIC( CMainFrame, CMDIFrameWnd ) 218第第第二部分第用户界面实例 下载BEGIN_MESSAGE_MAP( CMainFrame, CMDIFrameWnd ) // {{AFX_MSG_MAP( CMainFrame ) O N _ W M _ C R E AT E ( ) O N _ W M _ C L O S E ( ) ON_COMMAND( ID_OPTIONS_PREFERENCES, OnOptionsPreferences ) // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) static UINT indicators[] = { I D _ S E PA R ATOR, // status line indicator I D _ I N D I C ATO R _ C A P S , I D _ I N D I C ATO R _ N U M , I D _ I N D I C ATO R _ S C R L , } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CMainFrame construction/destruction C M a i n F r a m e : : C M a i n F r a m e ( ) { m_rgbColors[COLOR1_COLOR] = RGB( 200,20,150 ); m_rgbColors[COLOR2_COLOR] = RGB( 0,200,100 ); m_pPalette = NULL; } C M a i n F r a m e : : ~ C M a i n F r a m e ( ) { m_pPalette -> DeleteObject(); delete m_pPalette; m_pPalette = NULL; } int CMainFrame::OnCreate( LPCREATESTRUCT lpCreateStruct ) { if ( CMDIFrameWnd::OnCreate( lpCreateStruct ) = -1 ) return -1; L o a d O p t i o n s ( ) ; C r e a t e P a l e t t e ( ) ; if ( !m_wndTo o l B a r.Create( this ) || ! m _ w n d To o l B a r. L o a d ToolBar( IDR_MAINFRAME ) ) { TRACE0( "Failed to create toolbar\n" ); return -1; // fail to create } if ( !m_wndStatusBar.Create( this ) || ! m _ w n d S t a t u s B a r.SetIndicators( indicators, sizeof( indicators )/sizeof( UINT ) ) ) 第 9 章第绘第第图第第219下载{ TRACEO( "Failed to create status bar\n" ); return -1; // fail to create } // TODO: Remove this if you don't want tool tips or a resizeable toolbar m _ w n d To o l B a r.SetBarStyle( m_wndTo o l B a r.GetBarStyle() | C B R S _ TO O LTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC ); // TODO: Delete these three lines if you don't want the toolbar to // be dockable m _ w n d To o l B a r.EnableDocking( CBRS_ALIGN_ANY ); EnableDocking( CBRS_ALIGN_ANY ); DockControlBar( &m_wndToolBar ); return 0; } BOOL CMainFrame::PreCreateWindow( CREATESTRUCT& cs ) { // TODO: Modify the Window class or styles here by modifying // the CREATESTRUCT cs return CMDIFrameWnd::PreCreateWindow( cs ); } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CMainFrame diagnostics #ifdef _DEBUG void CMainFrame::AssertValid() const { C M D I F r a m e W n d : : A s s e r t Va l i d ( ) ; } void CMainFrame::Dump( CDumpContext& dc ) const { CMDIFrameWnd::Dump( dc ); } #endif //_DEBUG / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CMainFrame message handlers void CMainFrame::OnClose() { S a v e O p t i o n s ( ) ; C M D I F r a m e W n d : : O n C l o s e ( ) ; } 220第第第二部分第用户界面实例 下载void CMainFrame::LoadOptions() { BYTE *p; UINT size; if ( AfxGetApp() -> GetProfileBinary( SETTINGS_KEY, C O L O R S _ K E Y,&p,&size ) ) { memcpy( m_rgbColors,p,size ); delete []p; } } void CMainFrame::SaveOptions() { UINT size = sizeof( m_rgbColors ); AfxGetApp() -> WriteProfileBinary( SETTINGS_KEY, C O L O R S _ K E Y,( BYTE* )m_rgbColors,size ); } void CMainFrame::OnOptionsPreferences() { CPropertySheet sheet( _T( "Preferences" ),this ); m_pColorPage = new CColorPage; sheet.AddPage( m_pColorPage ); m_pColorPage -> m_rgbColors[COLOR1_COLOR] = m_rgbColors[COLOR1_COLOR]; m_pColorPage -> m_rgbColors[COLOR2_COLOR] = m_rgbColors[COLOR2_COLOR]; s h e e t . D o M o d a l ( ) ; delete m_pColorPage; } void CMainFrame::CreatePalette() { L O G PALETTE *lp = ( LOGPALETTE * )calloc( 1, sizeof( LOGPALETTE ) + ( NUM_COLORS * sizeof( PA L E T T E E N T RY ) ) ); lp -> palVersion = 0x300; lp -> palNumEntries = NUM_COLORS; for ( int i = 0; i < NUM_COLORS; i++ ) { lp -> palPalEntry[i].peRed = GetRValue( m_rgbColors[i] ); lp -> palPalEntry[i].peGreen = GetGValue( m_rgbColors[i] ); lp -> palPalEntry[i].peBlue = GetBValue( m_rgbColors[i] ); // reserve for animation lp -> palPalEntry[i].peFlags = PC_RESERV E D ; } if ( m_pPalette ) delete m_pPalette; m_pPalette = new CPalette; m_pPalette -> CreatePalette( lp ); free( lp ); } 第 9 章第绘第第图第第221下载7. 程序清单— 颜色属性页类 #if !defined COLORPA G E _ H #define COLORPA G E _ H // ColorPage.h : header file / / #include "WzdProject.h" / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CColorPage dialog class CColorPage : public CPropertyPage { D E C L A R E _ D Y N C R E ATE( CColorPage ) // Construction p u b l i c : C C o l o r P a g e ( ) ; ~ C C o l o r P a g e ( ) ; // Dialog Data // {{AFX_DATA( CColorPage ) enum { IDD = IDD_COLOR_PAGE }; CStatic m_ctrlColor1Display; CStatic m_ctrlColor2Display; CSliderCtrl m_ctrlBlueSlider1; CSliderCtrl m_ctrlGrnSlider1; CSliderCtrl m_ctrlRedSlider1; CSliderCtrl m_ctrlBlueSlider2; CSliderCtrl m_ctrlGrnSlider2; CSliderCtrl m_ctrlRedSlider2; // }}AFX_DATA COLORREF m_rgbColors[NUM_COLORS]; // Overrides // ClassWizard generate virtual function overrides // {{AFX_VIRTUAL( CColorPage ) p r o t e c t e d : virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support // }}AFX_VIRT U A L // Implementation p r o t e c t e d : // Generated message map functions // {{AFX_MSG( CColorPage ) virtual BOOL OnInitDialog(); afx_msg void OnVScroll( UINT nSBCode, UINT nPos, CScrollBar* pScrollBar ); afx_msg void OnPaint(); 222第第第二部分第用户界面实例 下载/ / } } A F X _ M S G D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : void DrawColorRects(); } ; # e n d i f // ColorPage.cpp : implementation file / / #include "stdafx.h" #include "wzd.h" #include "ColorPage.h" #include "WzdProject.h" #include "MainFrm.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CColorPage property page I M P L E M E N T _ D Y N C R E ATE( CColorPage, CPropertyPage ) CColorPage::CColorPage() : CPropertyPage( CColorPage::IDD ) { // {{AFX_DATA_INIT( CColorPage ) // }}AFX_DATA _ I N I T } C C o l o r P a g e : : ~ C C o l o r P a g e ( ) { } void CColorPage::DoDataExchange( CDataExchange* pDX ) { CPropertyPage::DoDataExchange( pDX ); // {{AFX_DATA_MAP( CColorPage ) DDX_Control( pDX, IDC_COLOR1_DISPLAY, m_ctrlColor1Display ); DDX_Control( pDX, IDC_COLOR2_DISPLAY, m_ctrlColor2Display ); DDX_Control( pDX, IDC_BLUE_SLIDER1, m_ctrlBlueSlider1 ); DDX_Control( pDX, IDC_GRN_SLIDER1, m_ctrlGrnSlider1 ); DDX_Control( pDX, IDC_RED_SLIDER1, m_ctrlRedSlider1 ); DDX_Control( pDX, IDC_BLUE_SLIDER2, m_ctrlBlueSlider2 ); DDX_Control( pDX, IDC_GRN_SLIDER2, m_ctrlGrnSlider2 ); DDX_Control( pDX, IDC_RED_SLIDER2, m_ctrlRedSlider2 ); // }}AFX_DATA _ M A P } 第 9 章第绘第第图第第223下载BEGIN_MESSAGE_MAP( CColorPage, CPropertyPage ) // {{AFX_MSG_MAP( CColorPage ) O N _ W M _ V S C R O L L ( ) O N _ W M _ PA I N T ( ) // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CColorPage message handlers BOOL CColorPage::OnInitDialog() { C P r o p e r t y P a g e : : O n I n i t D i a l o g ( ) ; // setup slider bars m _ c t r l B l u e S l i d e r 1 . S e t TicFreq ( 15 ); m_ctrlBlueSlider1.SetRange( 0, 255, TRUE ); m_ctrlBlueSlider1.SetPos ( 255-GetBValue( m_rgbColors[COLOR1_COLOR] ) ); m _ c t r l G r n S l i d e r 1 . S e t TicFreq ( 15 ); m_ctrlGrnSlider1.SetRange ( 0, 255, TRUE ); m_ctrlGrnSlider1.SetPos ( 255-GetGValue( m_rgbColors[COLOR1_COLOR] ) ); m _ c t r l R e d S l i d e r 1 . S e t TicFreq ( 15 ); m_ctrlRedSlider1.SetRange ( 0, 255, TRUE ); m_ctrlRedSlider1.SetPos ( 255-GetRValue( m_rgbColors[COLOR1_COLOR] ) ); m _ c t r l B l u e S l i d e r 2 . S e t TicFreq ( 15 ); m_ctrlBlueSlider2.SetRange ( 0, 255, TRUE ); m_ctrlBlueSlider2.SetPos ( 255-GetBValue( m_rgbColors[COLOR2_COLOR] ) ); m _ c t r l G r n S l i d e r 2 . S e t TicFreq ( 15 ); m_ctrlGrnSlider2.SetRange ( 0, 255, TRUE ); m_ctrlGrnSlider2.SetPos ( 255-GetGValue( m_rgbColors[COLOR2_COLOR] ) ); m _ c t r l R e d S l i d e r 2 . S e t TicFreq ( 15 ); m_ctrlRedSlider2.SetRange ( 0, 255, TRUE ); m_ctrlRedSlider2.SetPos ( 255-GetRValue( m_rgbColors[COLOR2_COLOR] ) ); return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FA L S E } void CColorPage::OnPaint() { CPaintDC dc( this ); // device context for painting D r a w C o l o r R e c t s ( ) ; } void CColorPage::OnVScroll( UINT nSBCode, UINT nPos, CScrollBar* pScrollBar ) { int i; int color = 0; UINT id = pScrollBar -> GetDlgCtrlID(); 224第第第二部分第用户界面实例 下载switch ( nSBCode ) { case SB_TO P : color = 255; b r e a k ; case SB_BOTTO M : color = 0; b r e a k ; case SB_LINEDOWN: case SB_LINEUP: case SB_PA G E D O W N : case SB_PA G E U P : switch ( id ) { case IDC_RED_SLIDER1: color = m_ctrlRedSlider1.GetPos(); b r e a k ; case IDC_BLUE_SLIDER1: color = m_ctrlBlueSlider1.GetPos(); b r e a k ; case IDC_GRN_SLIDER1: color = m_ctrlGrnSlider1.GetPos(); b r e a k ; case IDC_RED_SLIDER2: color = m_ctrlRedSlider2.GetPos(); b r e a k ; case IDC_BLUE_SLIDER2: color = m_ctrlBlueSlider2.GetPos(); b r e a k ; case IDC_GRN_SLIDER2: color = m_ctrlGrnSlider2.GetPos(); b r e a k ; } b r e a k ; case SB_THUMBPOSITION: case SB_THUMBTRACK: color = nPos; b r e a k ; case SB_ENDSCROLL: b r e a k ; } if ( nSBCode! = SB_ENDSCROLL ) { color = 255-color; switch ( id ) { case IDC_RED_SLIDER1: case IDC_RED_SLIDER2: i = COLOR1_COLOR; 第 9 章第绘第第图第第225下载if ( id = IDC_RED_SLIDER2 ) i = COLOR2_COLOR; m_rgbColors[i] = RGB( color, G e t G Value( m_rgbColors[i] ), G e t B Value( m_rgbColors[i] ) ); b r e a k ; case IDC_BLUE_SLIDER1: case IDC_BLUE_SLIDER2: i = COLOR1_COLOR; if ( id = IDC_BLUE_SLIDER2 ) i = COLOR2_COLOR; m_rgbColors[i] = RGB( GetRValue( m_rgbColors[i] ), G e t G Value( m_rgbColors[i] ),color ); b r e a k ; case IDC_GRN_SLIDER1: case IDC_GRN_SLIDER2: i = COLOR1_COLOR; if ( id == IDC_GRN_SLIDER2 ) i = COLOR2_COLOR; m_rgbColors[i] = RGB(GetRValue( m_rgbColors[i] ),color, G e t B Value( m_rgbColors[i] ) ); b r e a k ; } // select palette into a device context // (NOTE: you MUST select a palette into a device context for // AnimatePalette() to work!) CDC *pDC = GetDC(); CMainFrame *pFrame = ( CMainFrame * )AfxGetMainWnd(); CPalette *pPalette = pFrame -> GetPalette(); CPalette *pOPalette = pDC -> SelectPalette( pPalette,FALSE ); // add new color to palette using animation PA L E T T E E N T RY pentry; p e n t r y.peRed = GetRValue( m_rgbColors[i] ); p e n t r y.peGreen = GetGValue( m_rgbColors[i] ); p e n t r y.peBlue = GetBValue( m_rgbColors[i] ); p e n t r y.peFlags = PC_RESERV E D ; pPalette -> AnimatePalette( i,1,&pentry ); // reselect old palette pDC -> SelectPalette( pOPalette,FALSE ); // change color in CMainFrame pFrame -> SetColorRef( i,m_rgbColors[i] ); } CPropertyPage::OnVScroll( nSBCode, nPos, pScrollBar ); } void CColorPage::DrawColorRects() { // create a solid brush using our palette CDC *pDC = GetDC(); CMainFrame *pFrame = ( CMainFrame * )AfxGetMainWnd(); CPalette *pPalette = pFrame -> GetPalette(); 226第第第二部分第用户界面实例 下载CPalette *pOPalette = pDC -> SelectPalette( pPalette,FALSE ); CBrush brush1( PALETTEINDEX( COLOR1_COLOR ) ); CBrush brush2( PALETTEINDEX( COLOR2_COLOR ) ); // draw the rectangle CRect rect; m _ c t r l C o l o r 1 D i s p l a y. G e t WindowRect( &rect ); S c r e e n ToClient( &rect ); rect.DeflateRect( 2,2,2,2 ); CBrush *pOBrush = pDC -> SelectObject( &brush1 ); pDC -> Rectangle( &rect ); m _ c t r l C o l o r 2 D i s p l a y. G e t WindowRect( &rect ); S c r e e n ToClient( &rect ); rect.DeflateRect( 2,2,2,2 ); pDC -> SelectObject( &brush2 ); pDC -> Rectangle( &rect ); // unselect everything pDC -> SelectObject( pOBrush ); pDC -> SelectPalette( pOPalette,FALSE ); ReleaseDC( pDC ); } 9.2 实例32:伸展位图 1. 目标 伸展位图以匹配屏幕上的特定区域,如图 9 - 2所示。 图9-2 伸展位图 2. 策略 使用C D C : : S e t S t r e t c h M o d e ( )函数设置伸展模式,并使用 C D C : : S t r e t c h B l t ( )函数完成实际的 伸展。本例将该功能封装到自己的位图类中。 3. 步骤 1) 创建新的位图类 第 9 章第绘第第图第第227下载 一个伸展位图可以自动匹配 其所在的绘制空间的大小用C l a s s Wi z a r d创建从C b i t m a p派生的新位图类。为该新类添加一个新成员函数: S t r e t c h ( )。 2) 创建伸展位图对象 为S t r e t c h ( )函数赋三个参数,前两个参数是即将创建的已伸展位图的宽度和高度,最后一 个参数是下面将讨论的伸展模式: CBitmap *CWzdBitmap::Stretch( int nWidth, int nHeight, int nMode ) { 创建一个设备环境。由于不必为该函数传递设备环境,因而可从桌面获得设备环境: CDC dcTo, dcFrom, dcScreen; dcScreen.Attach( ::GetDC( NULL ) ); 从该屏幕设备环境创建两个内存设备环境,一个用于未伸展位图,另一个用于已伸展位 图: // create "from" device context and select the loaded bitmap into it dcFrom.CreateCompatibleDC( &dcScreen ); dcFrom.SelectObject( this ); // create a "to" device context select a memory bitmap into it d c To.CreateCompatibleDC( &dcScreen ); 在内存中创建一个空白位图,该空白位图具有与所要创建的位图相同的高度和宽度,同 时,需要将该空白位图选入 To设备环境: CBitmap *pBitmap = new CBitmap; pBitmap -> CreateCompatibleBitmap( &dcScreen, nWidth, nHeight ); d c To.SelectObject( pBitmap ); 假设在S t r e t c h ( )函数中已经使用L o a d B i t m a p ( )函数或者其他方式装载了一个位图对象。现 在便可以获得该位图的尺寸大小,如下所示: // get original bitmap size BITMAP bmInfo; GetObject( sizeof( bmInfo ),&bmInfo ); 设置伸展模式,然后进行伸展。注意将从该类内部的位图伸展到刚才创建的空白位图对 象: // set the stretching mode d c To.SetStretchBltMode( nMode ); // stretch loaded bitmap into memory bitmap d c To.StretchBlt( 0, 0, nWidth, nHeight, &dcFrom, 0, 0, bmInfo.bmWidth, bmInfo.bmHeight, SRCCOPY ); S t r e t c h ( )要做的最后工作是清除设备环境,并返回一个指针到这个被伸展的新位图类对象。 完成以后,调用程序负责删除该位图对象: // delete and release device contexts d c To . D e l e t e D C ( ) ; d c F r o m . D e l e t e D C ( ) ; ::ReleaseDC( NULL, dcScreen.Detach() ); // it's up to the caller to delete this new bitmap 228第第第二部分第用户界面实例 下载return pBitmap; } 3) 使用新的位图类 为了使用新位图类,先装载一个位图到该类中: CWzdBitmap m_bitmap; : : : m_bitmap.LoadBitmap( IDB_WZD_BITMAP ); 然后创建该位图的伸展版本: // get a bitmap stretched to size of client area of view CRect rect; GetClientRect( &rect ); CBitmap *pBitmap = m_bitmap.Stretch( rect.Width(), rect.Height(), COLORONCOLOR ); // also HALFTONE - - slower but attempts to average colors // BLACKONWHITE - - monotone - - scacrifices white for black pixels // WHITEONBLACK - - monotone - - scacrifices black for white pixels // if you use HALFTONE, must also call the following next to // realign the brush ::SetBrushOrgEx( pDC -> m_hDC, 0, 0, NULL ); 接下来可以按通常方式在屏幕上绘制该伸展位图: // get device context to select bitmap into CDC dcComp; dcComp.CreateCompatibleDC( pDC ); dcComp.SelectObject( pBitmap ); // draw bitmap pDC -> BitBlt( 0, 0, rect.Width(), rect.Height(), &dcComp, 0, 0, SRCCOPY ); delete pBitmap; 4. 注意 ■ C D C : : S t r e t c h ( )函数对于大位图工作得非常好,这样的位图有成百上千的像素需要处理。 在较小的分辨率下,例如 1 6×1 6像素大小的标准工具栏按钮,该函数必须采取某些折衷方式 是很显然的。线条模糊、图像变形对专业级的外观显示都是不可接受的。在这样的分辨率下 最好避免使用S t r e t c h ( )函数,而应当对不同的分辨率采用不同的位图绘制函数。对于大小适中 的位图,此时伸展模式就比较重要了。在使用 H A L F TO N E模式伸展或者压缩一个位图的时候, S t r e t c h B l t ( )函数可以进行颜色平均。即如果每个像素所用的颜色可在多种颜色之间进行选择, 则该函数使用平均颜色值。这样做可能较慢,但比起不能进行颜色平均的 C O L O R O N C O L O R 模式而言,其效果更容易使人接受。对于单色位图 (仅有黑白色 ),可以为伸展模式选择 B L A C K O N W H I T E或者W H I T E O N B L A C K。在该函数处于B L A C K O N W H I T E的模式之下,或 对新图上的像素点所用颜色需要在黑色或者白色之间进行选择时,总是选择黑色,而 W H I T E O N B L A C K模式则相反。 ■ 最好伸展基准位图 (reference bitmap)而不要持续伸展已经伸展过的位图,特别是在伸 第 9 章第绘第第图第第229下载展了几次的情况下更是如此。换句话说不要通过伸展位图 A来创建位图B,然后再通过伸展 B 来创建位图C。最好分别伸展A以得到B和C。这是因为每次使用S t r e t c h B l t ( )函数都会导致一些 图像变形,如果每次都用同一位图进行伸展,则这些图像变形就会逐渐累积起来。 5. 使用光盘时注意 执行随书附带光盘上的工程的时候,你会注意到视由一个位图所填充,该位图的大小随 用户缩小或者扩大视时而改变。 6. 程序清单— 位图类 #ifndef WZDBITMAP_H #define WZDBITMAP_H class CWzdBitmap : public CBitmap { p u b l i c : DECLARE_DYNAMIC( CWzdBitmap ) // Constructors C W z d B i t m a p ( ) ; CBitmap *Stretch( int nWidth, int nHeight, int nMode ); // Implementation p u b l i c : virtual ~CWzdBitmap(); // Attributes // Operations } ; # e n d i f // WzdBitmap.cpp : implementation of the CWzdBitmap class / / #include "stdafx.h" #include "WzdBitmap.h" #include "resource.h" / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdBitmap IMPLEMENT_DYNAMIC( CWzdBitmap, CBitmap ) C W z d B i t m a p : : C W z d B i t m a p ( ) { } C W z d B i t m a p : : ~ C W z d B i t m a p ( ) { } CBitmap *CWzdBitmap::Stretch( int nWidth, int nHeight, int nMode ) { CDC dcTo , d c F r o m , d c S c r e e n ; 230第第第二部分第用户界面实例 下载dcScreen.Attach( ::GetDC( NULL ) ); // create "from" device context and select the loaded bitmap into it dcFrom.CreateCompatibleDC( &dcScreen ); dcFrom.SelectObject( this ); // create a "to" device context select a memory bitmap into it d c To.CreateCompatibleDC( &dcScreen ); CBitmap *pBitmap = new CBitmap; pBitmap -> CreateCompatibleBitmap( &dcScreen, nWidth, nHeight ); d c To.SelectObject( pBitmap ); // get original bitmap size BITMAP bmInfo; GetObject( sizeof( bmInfo ),&bmInfo ); // set the stretching mode d c To.SetStretchBltMode( nMode ); // stretch loaded bitmap into memory bitmap d c To.StretchBlt( 0, 0, nWidth, nHeight, &dcFrom, 0, 0, bmInfo.bmWidth, bmInfo.bmHeight, SRCCOPY ); // delete and release device contexts d c To . D e l e t e D C ( ) ; d c F r o m . D e l e t e D C ( ) ; ::ReleaseDC( NULL, dcScreen.Detach() ); // it’s up to the caller to delete this new bitmap return pBitmap; } 9.3 实例33:抓取屏幕 1. 目标 将屏幕上的某块区域抓进位图。 2. 策略 也许读者已经知道可以用 B i t B l t ( )函数在屏幕上绘制位图。但是否了解还可以将该过程反 过来,即在屏幕上的某个区域抓进位图。在没有当前系统调色板的情况下抓取的内容可能什 么也没有,因为位图不过是指向颜色表的指针数组,所以还需要抓取当前的系统调色板。由 于该功能与位图密切相关,这里将它封装进一个自己的位图类中。 3. 步骤 1) 抓取屏幕 用C l a s s Wi z a r d创建派生自C b i t m a p的新类。 用Text Editor为该类添加新函数 C a p t u r e ( ),该函数以一个R E C T结构作为其调用参数。该 R E C T结构将包含所抓取屏幕区域的屏幕坐标。在该函数中首先删除保存在该类中的上一次的 调色板和位图对象: void CWzdBitmap::Capture( CRect &rect ) 第 9 章第绘第第图第第231下载{ // cleanup old captures if (m_pPalette) { D e l e t e O b j e c t ( ) ; delete m_pPalette; } 接下来,由于调用程序提供了该位图最终所具备的坐标尺寸,可以将其作为属性值保 存: // save width and height m _ n Width = rect.Wi d t h ( ) ; m_nHeight = rect.Height(); 为该屏幕创建一个设备环境。一般情况下该设备环境设置为写入,现在则将其设置为读 取: CDC dcScreen; dcScreen.CreateDC( "DISPLAY", NULL, NULL, NULL ); 创建一个空白的位图对象并将其选入一个内存设备环境: CDC dcMem; dcMem.CreateCompatibleDC( &dcScreen ); CreateCompatibleBitmap( &dcScreen, m_nWidth, m_nHeight ); dcMem.SelectObject( this ); 现在用BitBlt ()函数拷贝屏幕内容到空白位图: dcMem.BitBlt( 0, 0, m_nWidth, m_nHeight, &dcScreen, rect.left, rect.top, SRCCOPY ); 2) 抓取系统调色板 正如以上提到的,位图在没有当前系统调色板的情况下是毫无意义的。需要通过该调色 板来再次绘制该位图并获得相同的颜色。这里使用 C P a l e t t e的C r e a t e P a l e t t e ( )成员函数来创建该 调色板,其唯一的调用参数是一个 L O G PA L E T T L E结构,需要在该结构中填充当前系统调色 板。 创建一个空的足够大的L O G PA L E T T E结构以包含屏幕的颜色: int nColors = ( 1 << ( dcScreen.GetDeviceCaps( BITSPIXEL ) * dcScreen.GetDeviceCaps( PLANES ) ) ); L O G PALETTE *pLogPal = ( LOGPALETTE * )new BYTE[ s i z e o f ( L O G PALETTE) + ( nColors * sizeof( PA L E T T E E N T RY ) )]; 用颜色方案和颜色号初始化该结构的头部: pLogPal -> palVersion = 0x300; pLogPal -> palNumEntries = nColors; 用: : G e t S y s t e m P a l e t t e E n t r i e s ( )捕获当前系统颜色: ::GetSystemPaletteEntries( dcScreen.m_hDC, 0, nColors, ( LPPA L E T T E E N T RY )( pLogPal -> palPalEntry ) ); 创建调色板并完成设备环境清除工作: m_pPalette = new CPalette; 232第第第二部分第用户界面实例 下载m_pPalette -> CreatePalette( pLogPal ); // clean up delete []pLogPal; d c M e m . D e l e t e D C ( ) ; d c S c r e e n . D e l e t e D C ( ) ; 了解该新类可以参考本实例结尾的程序清单— 位图类。 3) 使用新的位图类 在本例中将抓取整个桌面的图像放入位图: CWnd *pWnd = GetDesktopWi n d o w ( ) ; pWnd -> GetWindowRect( &rect ); CWzdBitmap bitmap; bitmap.Capture( rect ); 如下显示该位图: // select bitmap palette CPalette *pOldPal = pDC -> SelectPalette( m_bitmap.GetPalette(), FALSE ); pDC -> RealizePalette(); // get device context to select bitmap into CDC dcComp; dcComp.CreateCompatibleDC( pDC ); dcComp.SelectObject( &m_bitmap ); // draw bitmap pDC -> BitBlt( 0,0,m_bitmap.m_nWidth, m_bitmap.m_nHeight, &dcComp, 0,0,SRCCOPY ); // reselect old palette pDC -> SelectPalette( pOldPal,FALSE ); 4. 注意 在实例1 8中曾使用本实例打印应用程序的视图。首先抓取视图,然后将其转变为设备无 关位图( D I B ),最后将其拷贝到打印机。 5. 使用光盘时注意 执行随书附带的光盘上的工程,单击 Te s t / W z d菜单项,则当前的桌面内容将在视中显示。 6. 程序清单— 位图类 #ifndef WZDBITMAP_H #define WZDBITMAP_H class CWzdBitmap : public CBitmap { p u b l i c : DECLARE_DYNAMIC( CWzdBitmap ) // Constructors 第 9 章第绘第第图第第233下载C W z d B i t m a p ( ) ; void Capture( CRect &rect ); CPalette *GetPalette(){return m_pPalette;}; // Implementation p u b l i c : virtual ~CWzdBitmap(); // Attributes int m_nWi d t h ; int m_nHeight; // Operations p r i v a t e : CPalette *m_pPalette; } ; # e n d i f // WzdBitmap.cpp : implementation of the CWzdBitmap class / / #include "stdafx.h" #include "WzdBtmap.h" / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdBitmap IMPLEMENT_DYNAMIC( CWzdBitmap, CBitmap ) C W z d B i t m a p : : C W z d B i t m a p ( ) { m_pPalette = NULL; } C W z d B i t m a p : : ~ C W z d B i t m a p ( ) { if ( m_pPalette ) { delete m_pPalette; } } void CWzdBitmap::Capture( CRect &rect ) { // cleanup old captures if ( m_pPalette ) { D e l e t e O b j e c t ( ) ; delete m_pPalette; } 234第第第二部分第用户界面实例 下载// save width and height m _ n Width = rect.Wi d t h ( ) ; m_nHeight = rect.Height(); / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // copy screen image into a bitmap object / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // create a device context that accesses the whole screen CDC dcScreen; dcScreen.CreateDC( "DISPLAY", NULL, NULL, NULL ); // create an empty bitmap in memory CDC dcMem; dcMem.CreateCompatibleDC( &dcScreen ); CreateCompatibleBitmap( &dcScreen, m_nWidth, m_nHeight ); dcMem.SelectObject( this ); // copy screen into empty bitmap d c M e m . B i t B l t ( 0 , 0 , m _ n Wi d t h , m _ n H e i g h t , & d c S c r e e n , r e c t . l e f t , r e c t . t o p , S R C C O P Y ) ; // this bitmap is worthless without the current system palette, so... / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // save system palette in this bitmap's palette / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // create an empty logical palette that’s big enough to hold all the colors int nColors = ( 1 << ( dcScreen.GetDeviceCaps( BITSPIXEL ) * dcScreen.GetDeviceCaps( PLANES ) ) ); L O G PALETTE *pLogPal = ( LOGPALETTE * )new BYTE[ sizeof( LOGPALETTE ) + ( nColors * sizeof( PA L E T T E E N T RY ) )]; // initialize this empty palette’s header pLogPal -> palVersion = 0x300; pLogPal -> palNumEntries = nColors; // load this empty palette with the system palette's colors ::GetSystemPaletteEntries( dcScreen.m_hDC, 0, nColors, ( LPPA L E T T E E N T RY )( pLogPal -> palPalEntry ) ); // create the palette with this logical palette m_pPalette = new CPalette; m_pPalette -> CreatePalette( pLogPal ); // clean up delete []pLogPal; d c M e m . D e l e t e D C ( ) ; d c S c r e e n . D e l e t e D C ( ) ; } 第 9 章第绘第第图第第235下载9.4 实例34:输出DIB位图文件 1. 目标 输出位图到设备无关位图( D I B )文件。 2. 策略 所有文件类型的位图都是设备无关的,也就是说它们包含其本身的颜色表。当位图被装 载到一个应用程序使用时,分解该位图为头、颜色表和图像数据 (指向颜色表的指针)。颜色表 被放到一个调色板中。该调色板在绘制位图之前必须选入一个设备环境,而头则转变为一个 b i t m a p头。 但是在本例中,将该过程颠倒过来,从其 b i t m a p头、调色板和图像数据来创建一个 D I B位 图。: : G e t D I B i t s ( ) A P I则完成的最后组装工作,然后将该对象与任何其他二进制文件一样写入 磁盘。这里将所有的这些功能全部封装到新的位图类中。 3. 步骤 1) 创建D I B对象 用C l a s s Wi z a r d从C B i t m a p创建新类。 为该新类添加C r e a t e D I B ( )函数。该函数将从当前装载到 (如L o a d B i t m a p )该位图类的位图 创建一个D I B。首先创建一个空的D I B头,可以用该位图头中的信息填充它: HANDLE CWzdBitmap::CreateDIB( int *pbmData ) { BITMAPINFOHEADER bi; memset( &bi, 0, sizeof( bi ) ); bi.biSize = sizeof( BITMAPINFOHEADER ); bi.biPlanes = 1; bi.biCompression = BI_RGB; 从B I T M A P头获取并保存该位图的尺寸: BITMAP bm; GetObject(sizeof( bm ),( LPSTR )&bm ); b i . b i Width = bm.bmWi d t h ; bi.biHeight = bm.bmHeight; 获取每个像素所需要的位数。这就是每个图像数据指针的大小。该数据指针越大,在颜 色表中被定义的颜色也越多,但同时位图也越大: int bits = bm.bmPlanes * bm.bmBitsPixel; if ( bits <= 1 ) bi.biBitCount = 1; else if ( bits <= 4 ) bi.biBitCount = 4; else if ( bits <= 8 ) bi.biBitCount = 8; e l s e bi.biBitCount = 24; 这里用G e t D I B t s ( )函数来填充颜色表和图像数据。但是首先要创建足够大的内存区域来装 载该位图,因此必须确定颜色表和图像数据要多大。 将每一种颜色定义 ( 4字节)所指定大小与各种可能颜色的数目相乘,以此计算颜色表的大 236第第第二部分第用户界面实例 下载小: int biColorSize = 0; if ( bi.biBitCount! = 24 ) biColorSize = ( 1 << bi.biBitCount ); biColorSize* = sizeof( RGBQUAD ); 将每一个颜色指针的大小与位图尺寸 (大小为宽度乘以高度)相乘,以此计算图像数据的大 小,: : G e t D I B i t s ( )将根据D W O R D基准来输出每一行,因此必须确保计算的图像数据大小必须 正确: bi.biSizeImage = ( DWORD )bm.bmWidth * bi.biBitCount; // bits per row bi.biSizeImage = ( ( ( bi.biSizeImage ) + 31 ) / 32 ) * 4; // DWORD aligned bi.biSizeImage* = bm.bmHeight; // bytes required for whole bitmap 这一步分配大小足以装载该位图头、颜色表和图像数据的全局内存区域,并将到目前为 止所创建的位图头复制到其中: HANDLE hDIB = ::GlobalAlloc( GHND, bi.biSize + biColorSize + bi.biSizeImage ); LPBITMAPINFOHEADER lpbi = ( LPBITMAPINFOHEADER )::GlobalLock( hDIB ); *lpbi = bi; 在使用: : G e t D I B i t s ( )之前的最后一步是用选入的位图调色板创建设备环境: CDC dc; dc.Attach( ::GetDC( NULL ) ); CPalette *pPal = dc.SelectPalette( m_pPalette,FALSE ); d c . R e a l i z e P a l e t t e ( ) ; 现在创建了: : G e t D I B i t s ( )所需要的所有部分,接下来让该函数完成其工作,如下所示: ::GetDIBits( dc.m_hDC, ( HBITMAP )m_hObject, 0, ( UINT )bi.biHeight, ( LPSTR )lpbi + ( WORD )lpbi -> biSize + biColorSize, ( LPBITMAPINFO )lpbi, DIB_RGB_COLORS ); 清除设备环境并返回D I B的句柄: ::GlobalUnlock( hDIB ); dc.SelectPalette( pPal,FALSE ); d c . R e a l i z e P a l e t t e ( ) ; // return handle to the DIB return hDIB; } 2) 将D I B对象存入磁盘 现在为该类添加另一个函数,该函数将文件名作为参数,本例将用该函数来保存位图文 件。基本上是在 C r e a t e D I B ( )函数中复制所创建的 D I B到磁盘文件中,但是首先还要创建一个 头:位图文件头。 首先为该类添加新函数,它使用 C r e a t e D I B ( )函数来得到当前位图对象的 D I B句柄: void CWzdBitmap::SaveBitmapEx( CString sFile ) { // create a DIB bitmap 第 9 章第绘第第图第第237下载int bmData; HANDLE hDIB = CreateDIB( &bmData ); 接下来锁定句柄以获取指向该 D I B的内存指针: LPBYTE lpBitmap = ( LPBYTE )::GlobalLock( hDIB ); int bmSize = ::GlobalSize( hDIB ); 注意 句柄实际上是指向指针的指针。这样就可以使得它最后所指向的对象在内存中移 动。为什么要移动对象呢?这样做的话,只要内存有了空洞( h o l e )就可以压缩内存— 空洞是由于其他应用程序释放了内存而产生的。因为句柄不直接指向内存,而是实际 上指向一个内存指针表,所有系统要做的就是更新该指针表。但是在锁定句柄的情况 下,可以通知系统停止移动对象并返回指向该对象的真实指针,这样就可以像访问其 他内存对象一样访问自己的对象了。如果是为了继续压缩未分配的内存,则在任何时 候都不应该锁定内存。 这一步创建一个空白位图文件头并用该位图的有关细节填充: BITMAPFILEHEADER bmfh; b m f h . b f Type = 'MB'; // (actually 'BM' for bitmap) bmfh.bfSize = sizeof(BITMAPFILEHEADER)+bmSize; bmfh.bfReserved1 = 0; bmfh.bfReserved2 = 0; b m f h . b f O ffBits = bmData; 首先用C F i l e创建一个二进制文件,然后写入 D I B的头: CFile file; file.Open( sFile, CFile::modeCreate|CFile::modeWrite ); f i l e . Write( &bmfh,sizeof( BITMAPFILEHEADER ) ); f i l e . Write( lpBitmap,bmSize ); f i l e . C l o s e ( ) ; 进行清除: ::GlobalUnlock( hDIB ); ::GlobalFree( hDIB ); } 3) 使用新的位图函数 使用抓取屏幕或者绘制该位图方式中的一种创建位图后,可以将其存入磁盘: b i t m a p . S a v e B i t m a p E x ( "dib.bmp" // file name and path to save to ) ; 4. 注意 本例使用了实例 1 8中的C r e a t e D I B ( )函数,它在打印位图前将位图转变成 D I B。实际上在 打印机接受之前需要将任何位图都转变成 D I B。理论上只需要在打印机的设备环境中实现位图 的调色板,然后调用 B i t B l t ( )函数即可。但是B i t B l t ( )看起来并不阻止屏幕调色板到打印机调色 板的转换。 5. 使用光盘时注意 运行随书附带光盘上的工程时,单击 Te s t / W z d菜单项。当前桌面内容将被抓取并随后写 238第第第二部分第用户界面实例 下载入文件d i b . b m p内。 6. 程序清单— 位图类 #ifndef WZDBITMAP_H #define WZDBITMAP_H class CWzdBitmap : public CBitmap { p u b l i c : DECLARE_DYNAMIC( CWzdBitmap ) // Constructors C W z d B i t m a p ( ) ; void SaveBitmapEx( CString sFile ); HANDLE CreateDIB( int *pbmData = NULL ); // Implementation p u b l i c : virtual ~CWzdBitmap(); // Attributes int m_nWi d t h ; int m_nHeight; // Operations p r i v a t e : CPalette *m_pPalette; } ; # e n d i f // WzdBitmap.cpp : implementation of the CWzdBitmap class / / #include "stdafx.h" #include "WzdBtmap.h" / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdBitmap IMPLEMENT_DYNAMIC( CWzdBitmap, CBitmap ) C W z d B i t m a p : : C W z d B i t m a p ( ) { m_pPalette = NULL; } C W z d B i t m a p : : ~ C W z d B i t m a p ( ) { if ( m_pPalette ) { 第 9 章第绘第第图第第239下载delete m_pPalette; } } void CWzdBitmap::SaveBitmapEx( CString sFile ) { // create a DIB bitmap int bmData; HANDLE hDIB = CreateDIB( &bmData ); // get a memory pointer to it LPBYTE lpBitmap = ( LPBYTE )::GlobalLock( hDIB ); int bmSize = ::GlobalSize( hDIB ); // create file CFile file; file.Open( sFile, CFile::modeCreate|CFile::modeWrite ); // write the bitmap header BITMAPFILEHEADER bmfh; b m f h . b f Type = 'MB' ; // (actually 'BM' for bitmap) bmfh.bfSize = sizeof( BITMAPFILEHEADER ) + bmSize; bmfh.bfReserved1 = 0; bmfh.bfReserved2 = 0; b m f h . b f O ffBits = bmData; f i l e . Write( &bmfh,sizeof( BITMAPFILEHEADER ) ); // write the bitmap body f i l e . Write( lpBitmap,bmSize ); // cleanup f i l e . C l o s e ( ) ; ::GlobalUnlock( hDIB ); ::GlobalFree( hDIB ); } HANDLE CWzdBitmap::CreateDIB(int *pbmData) { / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // create DIB header from our BITMAP header / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / BITMAPINFOHEADER bi; memset( &bi, 0, sizeof( bi ) ); bi.biSize = sizeof( BITMAPINFOHEADER ); bi.biPlanes = 1; bi.biCompression = BI_RGB; // get and store dimensions of bitmap 240第第第二部分第用户界面实例 下载BITMAP bm; GetObject( sizeof( bm ),( LPSTR )&bm ); b i . b i Width = bm.bmWi d t h ; bi.biHeight = bm.bmHeight; // get number of bits required per pixel int bits = bm.bmPlanes * bm.bmBitsPixel; if ( bits <= 1 ) bi.biBitCount = 1; else if ( bits <= 4 ) bi.biBitCount = 4; else if ( bits <= 8 ) bi.biBitCount = 8; e l s e bi.biBitCount = 24; // calculate color table size int biColorSize = 0; if ( bi.biBitCount! = 24 ) biColorSize = ( 1 << bi.biBitCount ); biColorSize* = sizeof( RGBQUAD ); // calculate picture data size bi.biSizeImage = ( DWORD )bm.bmWidth * bi.biBitCount; // bits per row bi.biSizeImage = ( ( ( bi.biSizeImage ) + 31 ) / 32 ) * 4; // DWORD aligned bi.biSizeImage* = bm.bmHeight; // bytes required for whole bitmap // return size to caler in case they want to save to file if ( pbmData ) *pbmData = bi.biSize + biColorSize; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // get DIB color table and picture data / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // allocate a hunk of memory to hold header, color table and picture data HANDLE hDIB = ::GlobalAlloc( GHND, bi.biSize + biColorSize + bi.biSizeImage ); // get a memory pointer to this hunk by locking it LPBITMAPINFOHEADER lpbi = ( LPBITMAPINFOHEADER )::GlobalLock( hDIB ); // copy our header structure into hunk *lpbi = bi; // get a device context and select our bitmap's palette into it CDC dc; dc.Attach( ::GetDC( NULL ) ); CPalette *pPal = dc.SelectPalette( m_pPalette,FALSE ); d c . R e a l i z e P a l e t t e ( ) ; 第 9 章第绘第第图第第241下载// load our memory hunk with the color table and picture data ::GetDIBits( dc.m_hDC, ( HBITMAP )m_hObject, 0, ( UINT )bi.biHeight, ( LPSTR )lpbi + ( WORD )lpbi -> biSize + biColorSize, ( LPBITMAPINFO )lpbi, DIB_RGB_COLORS ); // clean up ::GlobalUnlock( hDIB ); dc.SelectPalette( pPal,FALSE ); d c . R e a l i z e P a l e t t e ( ) ; // return handle to the DIB return hDIB; } 242第第第二部分第用户界面实例 下载下载下载 第10章 帮 助 联机帮助可以极大地减少用户花费在学习应用程序上的时间。用户可以直接查询每个控 件本身,而不用在用户手册中寻找合适的命令。联机帮助还可以使应用程序立即对整个联机 手册中的内容进行查询,而不像普通手册那样只能按照索引去寻找用户感兴趣的东西。同时 也大大节省了打印手册的费用。 然而,只有当用户对应用程序所要做的有一个总体的了解时,联机帮助才会有用。若用 户是一名完完全全的新手,它能提供的帮助好比是用一本英文字典去教一个人讲英文。当需 要的只是一些最基本常识的时候,从使用手册或指导书中一些常用的部分即可获得帮助。 实例35 添加帮助菜单项,在本例中将向应用程序中的 H e l p菜单中添加C o n t e n t s和S e a r c h 菜单项。 实例36 添加上下文相关帮助,在本例中将向应用程序添加相关帮助。 实例37 添加气泡帮助,在本例中将向应用程序添加气泡帮助。 联机帮助的三种类型 联机帮助有以下三种类型: 1) 菜单帮助(Menu help)是用户通过检索H e l p菜单所能得到的帮助。虽然 A p p Wi z a r d在这 个菜单中仅仅加入了一个 A b o u t命令按钮,但用户自己还可以加入 I n d e x和C o n t e n t命令按钮。 这样便允许用户无缝地通过 Wi n d o w s的Wi n H e l p应用程序打开帮助文件。如何在应用程序中添 加菜单帮助请参考实例3 5。 2) 上下文相关帮助(Context Sentitive help)允许用户激活当前正在进行人机交互的任何菜 单项或是对话框控件的帮助。实际上通过上下文相关帮助允许用户转到联机帮助文件中包含 相应控件或菜单项帮助内容的页面。在上下文相关帮助的目录下有以下两种版本的帮助: ■ F 1帮助允许用户按F 1键以得到有关当前选中菜单项或者被激活对话框的帮助。 ■ “w h a t’s this(这是什么)”帮助允许用户通过单击控件或希望得到帮助的屏幕区域来 获得帮助,这种方式使用户拥有更大的选择范围。当处于 W h a t’t h i s模式时,鼠标光标将变成 一个问号箭头。 所有这些上下文相关帮助的例子均可在实例 3 6中找到。 3) 气泡( B u b b l e )帮助允许用户查询控件或视的某一区域,用户所做的仅仅是将鼠标光标移 过要获得帮助的区域。一个小窗口将打开并描述该区域。气泡帮助不像上下文相关帮助所叙 述的那么详细,但它的速度很快。气泡帮助的实例请参见实例 3 7。 10.1 实例35:添加帮助菜单项 1. 目标 将标准的C o n t e n t s和S e a r c h菜单项加入到应用程序的H e l p菜单中,如图1 0 - 1所示。 2. 策略 事实上,在一些 M F C类中已包含了许多用于提供菜单帮助的功能。因此,本例中要完成的工作是有选择地“激活”这些功能。 3. 步骤 启用菜单帮助 利用菜单编辑器在帮助菜单中加 入如下的命令: N a m e : & i n d e x I D : I D _ H E L P _ I N D E X C o m m e n t :“Display Help Index\ nHelp Index” N a m e : & To p i c s I D : I D _ H E L P _ F I N D E R C o m m e n t :“Display Help topics\nHelp To p i c s” S e p a r a t o r N a m e : &Using Help I D : I D _ H E L P _ U S I N G C o m m e n t :“Display instructions about how to use help\nHelp” S e p a r a t o r 注意 最后一个菜单命令“使用帮助”是微软的Wi n H e l p程序提供的,它允许用户查找 如何使用帮助。 使用文本编辑器,在C l a s s Wi z a r d的{ { } }标识之外,加入下列消息宏到 C M a i n F r a m e的消息 映射中: ON_COMMAND( ID_HELP_INDEX, CMDIFrameWnd::OnHelpIndex ) ON_COMMAND( ID_HELP_USING, CMDIFrameWnd::OnHelpUsing ) ON_COMMAND( ID_HELP_FINDER, CMDIFrameWnd::OnHelpFinder ) 这样就可以了。现在M F C类库就可以激活Wi n d o w s的Wi n H e l p应用程序,并在用户单击菜 单帮助的时候,将应用程序的帮助文件 ( . h l p和. c n t )加入到其中。M F C的缺省情况是假定这些 文件在应用程序的可执行文件的目录下。但如果想将这些文件放在其他位置,则要在应用程 序类的I n i t I n s t a n c e ( )函数中加入如下代码: m_pszHelpFilePath = _tcsdup( _T( “ d : \ \ s o m e d i r \ \ m y h e l p . h l p ” // the directory and name of // your help file ) ); 4. 注意 如果在第四步选择 Context Help选项,A p p Wi z a r d将会自动为这些菜单项添加一定的上下 文相关帮助功能,同时还添加 . h l p和. c n t文件。然而在大多数公司,经常使用一些帮助文件的 244第第第二部分第用户界面实例 下载 图10-1 菜单帮助 在自己的应 用程序中加 入菜单帮助编写系统诸如 R o b o H E L P来创建. h l p和. c n t文件,这样在一定程度上会减少代码编写人员和产 品经理的工作量。因而,最好亲自动手去添加这些菜单项,然后将这些固定的东西加入到工 程中去。 5. 使用光盘时注意 当执行附带光盘中的工程时,将会发现帮助文件已经扩展,其中包含了三个新的帮助命 令。 10.2 实例36:添加上下文相关帮助 1. 目标 在应用程序中添加上下文相关帮助 (即点即显帮助),如图1 0 - 2所示。 2. 策略 本例将再次激活M F C类库中的某 些功能来为应用程序提供上下文相关 帮助,同时还要在应用程序中加入 F 1 H e l p和W h a t's This Help。对于已被 选的菜单项,用户可以通过按 F 1键来 获得F1 Help;对于被打开的对话框, 用户可以通过按对话框中的 H e l p按钮 或F 1键来获得 F1 Help。用户还可以 通过按S h i f t + F 1键来获得W h a t’s This Help,或者也可以通过单击 W h a t’s this菜单项、工具 条按钮,或是在对话框中标题栏中的问号按钮三者之一来获得 W h a t’s This Help。在用户单 击一个主题之前,W h a t’s This Help将使鼠标光标显示为问号箭头。 3. 步骤 1) 在主应用程序中添加上下文相关帮助 使用文本编辑器在 C M a i n F r a m e 类的消息映射中加入下列代码,代码应添加在 C l a s s Wi z a r d { { } }之后: O N _ C O M M A N D ( I D _ H E L P, CMDIFrameWnd::OnHelp) O N _ C O M M A N D ( I D _ C O N T E X T _ H E L P, CMDIFrameWnd::OnContextHelp) O N _ C O M M A N D ( I D _ D E FA U LT _ H E L P, CMDIFrameWnd::OnHelpFinder) 如果帮助文件没有安装在应用程序的可执行文件的目录下,则在应用程序类的 I n i t I n s t a n c e ( )函数中加入如下代码: m_pszHelpFilePath = _tcsdup( _T( ] “ d : \ \ s o m e d i r \ \ m y h e l p . h l p ” // the directory and name of // your help file ) ); 使用Menu Editor(菜单编辑器)在帮助菜单中添加W h a t’s this命令按钮: N a m e : & W h a t’s this I D : I D _ C O N T E X T _ H E L P Comment: "Display help for clicked on buttons,menus and windows\nHelp" 也可以选择使用Toolbar Editor(工具条编辑器)在工具条中加入W h a t's this按钮。设置其I D 第10章第帮第第助第第245下载 图10-2 上下文相关帮助 在自己的应用 程序中加入上 下文相关帮助号为I D _ C O N T E X T _ H E L P。使用A p p Wi z a r d创建一个临时工程并且在第四步指定 Context help, 可以为这个按钮获得一个位图 (一个带有箭头的问号 )。然后从这个新工程的工具条中剪切下 W h a t’s this位图并且将其粘贴到当前工程中。 利用Accelerator Editor(加速键编辑器)实现F 1和S h i f t+F 1快捷键,并使用下列I D: V K _ F 1 I D _ C O N T E X T _ H E L P V I RT K E Y, S H I F T V K _ F 1 I D _ H E L P V I RT K E Y 使用String Table Editor(字符串编辑器)在字符串中加入两条空闲消息。其中一条提示用户 可以按F 1键以得到帮助,另一条则提示用户可以单击某一主题以得到帮助: ■ 改变A F X _ I D S _ I D L E M E S S A G E的消息为For Help,press F1。 ■ 添加A F X _ I D S _ H E L P M O D E M E S S A G E的消息为: Select an object on which to get Help。 2) 在对话框中添加上下文相关帮助 使用Dialog Editor,打开模板自身的属性框,并且指定 Context help风格,这将在创建标 题条时产生一个“?”按钮。对于每一个对话框模板重复该操作。 同样可以在每一个模板中添加 H e l p按钮。设置该按钮的I D号为I D _ H E L P。 对于对话框模板中的每一个控件,选择 Help ID选择项。这样就为该控件加入了一个帮助 标识号,当调用Wi n H e l p时通过帮助标识号来访问该控件。该标识号通过 Dialog Editor还将自 动加入到一个r e s o u r c e . h m文件中。如果r e s o u r c e . h m文件不存在,Dialog Editor将自动创建一 个。以后要编写帮助文件时将使用这个 . h m文件。 注意 帮助标识号是在控件的I D C号的基础上由对话框编辑器自动创建的。换句话说, 如果控件的标识号是: #define IDC_BUTTO N 1 1 0 0 0 帮助标识号将是: #define HIDC_BUTTO N 1 0 x 8 0 8 2 0 3 e 8 这里标识号结尾处的“3 e 8”是I D C _ B U T TO N 1中的1 000在十六进制时的等效值。这意 味着如果要在另一个对话框中创建标识号同样为1 000的另一控件时,帮助系统将会激 活原有的相同的帮助信息。因而,注意跟踪控件的标识号并保证标识号不重复是非常重 要的,有时候应当手工分配控件标识号。这样,两个不同控件的帮助才不会互相冲突。 在对话框中支持上下文相关帮助需要在此对话框中处理两个消息: W M _ H E L P I N F O和 W M _ H E L P H I T T E S T。在W h a t's this模式中,当用户单击对话框中的某一项时,系统发出第一 条消息。第二条和第一条大致相同,不同之处仅在对话框当前没有激活,处于无模式对话框 或对话条两种状态之一。在这两种情况下,需要自行确定用户单击的内容是什么,并且如果 对话框具有帮助标识号时或者返回该标识号或者自行调用帮助系统。 在每个对话框类中加入 W M _ H E L P I N F O 和 W M _ H E L P H I T T E S T 消息处理函数 ( W M _ H E L P H I T T E S T必须手工添加, WM_HELPINFO可用C l a s s Wi z a r d添加): BEGIN_MESSAGE_MAP( CWzdDialog, CDialog ) // {{AFX_MSG_MAP( CWzdDialog ) O N _ W M _ H E L P I N F O ( ) // }}AFX_MSG_MAP ON_MESSAGE( WM_HELPHITTEST, O n H e l p H i t Test ) 246第第第二部分第用户界面实例 下载E N D _ M E S S A G E _ M A P ( ) 对于 O n H e l p I N f o ( )处理函数,确定哪一种控件被选择并且查询其标识号。然后使用 C Wi n H e l p : : Wi n H e l p ( )建立合适的上下文相关帮助: BOOL CWzdDialog::OnHelpInfo( HELPINFO* pHelpInfo ) { if ( pHelpInfo -> iContextType == HELPINFO_WINDOW ) { DWORD helpId = -1; CWnd* pWnd = GetDlgItem( pHelpInfo -> iCtrlId ); if ( pWnd ) { helpId = pWnd -> GetWi n d o w C o n t e x t H e l p I d ( ) ; } AfxGetApp() -> WinHelp( helpId, HELP_CONTEXTPOPUP ); } // return CDialog::OnHelpInfo( pHelpInfo ); return TRUE; } 使用O n H e l p H i t Te s t ( )时,在可以查询其帮助标识号之前,必须确定用户单击了哪一个控 件。不过这时可以返回该标识号到 M F C类,由它激活帮助系统: L R E S U LT CWzdDialog::OnHelpHitTest( WPARAM wParam, LPARAM lParam ) { DWORD helpId = -1; // find the window that was clicked CWnd* pWnd = ChildWindowFromPoint( CPoint( LOWORD( lParam ), HIWORD( lParam ) ), CWP_SKIPINVISIBLE|CWP_SKIPTRANSPARENT ); if ( pWnd ) { helpId = pWnd -> GetWi n d o w C o n t e x t H e l p I d ( ) ; } return helpId; } 4. 注意 A p p Wi z a r也将自动添加这些功能中的一部分。请参考前面实例中的“注意”部分。 5. 使用光盘时注意 当执行附带光盘中的工程时,将会发现帮助菜单已经扩展,包含 W h a t’s this项,工具条 中也有一个附加的W h a t’s this按钮。单击Te s t / W z d菜单命令打开对话框,其中包括一个 H e l p 按钮,标题栏中有一个“?”按钮。 10.3 实例37:添加气泡帮助 1. 目标 在应用程序中加入气泡帮助,如图 1 0 - 3所示。 2. 策略 本例使用C To o l Ti p C t r l类来提供气泡帮助。这个类所做的工作仅仅是当用户将鼠标移过某 第10章第帮第第助第第247下载一控件或是视中的某一区域时,能打开一个气泡似的小窗口。在这个窗口中将显示出一段短 小的文本信息,该文本信息在 C To o l Ti p C t r l中定义。使用C To o l Ti p C t r l的方法是将鼠标消息截 获给它,由于它所要监控的 W M _ M O U S E M O V E鼠标消息实际上是进入鼠标所在窗口的窗口 过程( Window process)。例如,当鼠标移过一个按钮控件时,该按钮控件将获取所有这些鼠标 消息。所幸的是在 C To o l Ti p C t r l封装的工具提示(tool tip)控件中提供了一个子类化功能,它允 许截取这些消息。但在另一方面,因为某些原因, C To o l Ti p C t r l封装的工具提示控件的功能实 现起来并不是十分简单。因而将在 C To o l Ti p C t r l派生类中添加自己的函数以直接访问工具提示 控件。 图10-3 气泡帮助 3. 步骤 1) 派生一个新的C To o l Ti p C t r l类 使用C l a s s Wi z a r d创建一个C To o l Ti p C t r l子类。 使用文本编辑器添加一个新函数 A d d To o l ( )。这个函数允许在对话框类中嵌入一个单个的 工具提示控件,使用它可支持对话框中的所有控件。首先可以创建一个空的 TO O L I N F O结构, 以发送到工具提示控件: BOOL CWzdTo o l Ti p C t r l : : A d d Tool( UINT nID, LPCTSTR lpszText ) { TOOLINFO ti; memset( &ti, 0, sizeof( TOOLINFO ) ); ti.cbSize = sizeof( TOOLINFO ); 接下来保存用户传送的文本信息: t i . l p s z Text = ( LPSTR )lpszTe x t ; 接下来要求工具提示控件对来自指定控件标识号的信息进行分类: ti.uFlags = TTF_IDISHWND|TTF_SUBCLASS; ti.uId = ( UINT )GetParent() -> GetDlgItem( nID ) -> m_hWnd; 接下来确定这个工具提示控件的属主: ti.hwnd = GetOwner() -> GetSafeHwnd(); 最后将下面的消息发送给控件: return ( BOOL )SendMessage( TTM_ADDTOOL, 0, ( LPARAM )&ti ); } 2) 在对话框中实现新的C To o l Ti p C t r l类 248第第第二部分第用户界面实例 下载 在对话框和视中加入 气泡帮助在对话框中嵌入下面一个新类: C W z d To o l TipCtrl m_tooltips; 如果希望在某些静态控件上面显示气泡帮助,则需要使用对话框编辑器将它们的风格修 改为Parent notify。在此不需要对除了静态控件以外的任何控件进行任何修改,因为所有这些 控件的风格将自动通知它们的父窗口。 使用C l a s s Wi z a r d添加一个W M _ I N I T D I A L O G消息处理函数到这个对话框类中,并且创建 气泡帮助窗口: m_tooltips.Create( this ); 注意 尽管创建了一个窗口,但该窗口在工具提示控件为某一控件显示气泡帮助前一直 是不可见的。同时注意在这个对话框中,该窗口将为所有这些控件显示气泡帮助。 在W M _ I N I T D I A L O G处理函数中,使用前面添加的 A d d To o l ( )函数,为需要显示气泡帮助 的所有控件指定各自的帮助信息: m _ t o o l t i p s . A d d To o l ( I D C _ R A D I O 1 , // control id “Radio Control” // bubble help message ) ; 最后,激活工具提示: m_tooltips.Activate( TRUE ); 如果要在其他地方修改控件的帮助文本信息,使用以下代码: m _ t o o l t i p s . U p d a t e Ti p Te x t ( “Checked Box”, // new text G e t D l g I t e m ( I D C _ C H E C K 1 // the control’s id ) ) ; 以上便为对话框中的控件加入了气泡帮助。当在一个父窗口中有许多子窗口而又不想为 每一个子窗口实现C To o l Ti p C t r l控件时,同样可以使用这种解决办法。接下来将创建一个新的 C To o l Ti p C t r l派生类,它仅在其他窗口的特定区域特别是视中显示气泡帮助。 3) 添加一个新的C To o l Ti p C t r l派生类 使用文本编辑器,在新工具提示类中加入一个新的函数 A d d A r e a ( )。同样可以先创建一个 空的TO O L I N F O结构,以发送到工具提示控件: BOOL CWzdTo o l TipCtrl::AddArea( UINT nID, LPRECT lpRect, LPCTSTR lpszText ) { TOOLINFO ti; memset( &ti, 0, sizeof( TOOLINFO ) ); ti.cbSize = sizeof( TOOLINFO ); 指定要显示的文本: t i . l p s z Text = ( LPSTR )lpszTe x t ; 告诉工具提示控件应监视的窗口 (例如视)和区域: ti.hwnd = GetOwner() -> GetSafeHwnd(); 第10章第帮第第助第第249下载memcpy( &ti.rect, lpRect, sizeof( RECT ) ); 同时告诉工具提示控件为这个窗口分类,这样控件可以截获自己鼠标消息: ti.uFlags = TTF_SUBCLASS; 接下来指定一个自行定义的标识号,它将允许在以后自行修改该工具提示,该标识号可 以是任何未分配的整型数: ti.uId = nID; 现在将该定义发送给工具条,然后返回: return ( BOOL )SendMessage( TTM_ADDTOOL, 0, ( LPARAM )&ti ); } 4) 在视中实现一个新的C To o l Ti p C t r l类 在视类中嵌入新的工具提示类: C W z d To o l TipCtrl m_tooltips; 在视的W M _ C R E AT E消息处理函数中创建它: m_tooltips.Create( this ); 在该消息处理函数或其他位置,定义希望显示气泡帮助信息的视中的区域: m _ t o o l t i p s . A d d A r e a ( 1 , // user defined id & r e c t 1 , // a CRect value defining the area in view // coordinates “ m e s s a g e ” , // the bubble help message ) ; 在退出该消息处理函数之前,用下面的代码激活工具提示控件: m_tooltips.Activate( TRUE ); 为更新帮助文本信息,使用以下代码: m _ t o o l t i p s . U p d a t e Ti p Te x t ( “new text”, // the new text message t h i s , // pointer to view class 1 // the id defined above ) ; 为改变这个工具提示所指向的区域,可以使用以下代码: m _ t o o l t i p s . S e t To o l R e c t ( t h i s , // pointer to view class 1 , // the id defined above & r e c t // a new CRect value defining // the area in view coordinates ) ; 参考本实例结尾的程序清单— 工具提示类来查看工具提示类的完整代码列表。 4. 注意 ■ 另一个解决方案是为每一个对话框控件分别加入各自的工具提示控件。然而,这将涉 及到为每一个控件创建自己的派生类,只有这样才可以在其中嵌入工具提示类。 ■ 工具提示类显示单行的文本。对于特别长的信息或是某些特殊类型的信息,也许想为 250第第第二部分第用户界面实例 下载文本进行多行显示,但仅仅在文本中加入一个 \ n换行符是不行的。必须在工具提示控件的派 生类中加入 W M _ PA I N T和W M _ N C PA I N T消息处理函数并且自行绘制窗口。使用函数 G e t Wi n d o w Te x t ( )可获取要显示的帮助文本信息。 5. 使用光盘时注意 ■ 当执行附带光盘中的工程时,将会发现鼠标在视的左上角移动时,气泡帮助窗口将被 打开。然后单击 Te s t / W z d菜单命令打开对话框,这允许光标移过该对话框的任何控件时,将 显示气泡帮助信息。 ■ 该工程文件中,同时提供了 W z d T T 2 . c p p和. h文件,它们是创建用户自己的工具提示帮 助的基础。 6. 程序清单— 工具提示类 #if !defined( AFX_WZDTLT I P _ H _ _ 1 E 2 2 2 D A 3 _ 9 7 E F _ 11D2_A18D_C32FFDBA4686__INCLUDED_ ) #define AFX_WZDTLT I P _ H _ _ 1 E 2 2 2 D A 3 _ 9 7 E F _ 11 D 2 _ A 1 8 D _ C 3 2 F F D B A 4 6 8 6 _ _ I N C L U D E D _ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 // WzdTlTip.h : header file / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTo o l TipCtrl window class CWzdTo o l TipCtrl : public CTo o l Ti p C t r l { // Construction p u b l i c : C W z d To o l Ti p C t r l ( ) ; // Attributes p u b l i c : // Operations p u b l i c : BOOL AddTool( UINT nID, LPCTSTR lpszText = LPSTR_TEXTCALLBACK ); BOOL AddTool( UINT nID, UINT nIDText ); BOOL AddArea( UINT nID, LPRECT lpRect, LPCTSTR lpszText = LPSTR_TEXTCALLBACK ); BOOL AddArea( UINT nID, LPRECT lpRect, UINT nIDText ); // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CWzdTo o l TipCtrl ) // }}AFX_VIRT U A L // Implementation p u b l i c : virtual ~CWzdTo o l Ti p C t r l ( ) ; // Generated message map functions p r o t e c t e d : 第10章第帮第第助第第251下载// {{AFX_MSG( CWzdTo o l TipCtrl ) // NOTE - the ClassWizard will add and remove member functions here. // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Visual C++ will insert additional declarations immediately before // the previous line. # e n d i f // !defined( AFX_WZDTLT I P _ H _ _ 1 E 2 2 2 D A 3 _ 9 7 E F _ 11D2_A18D_C32FFDBA4686__INCLUDED_ ) // WzdTlTip.cpp : implementation file / / #include "stdafx.h" #include "wzd.h" #include "WzdTlTi p . h " #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTo o l Ti p C t r l C W z d To o l Ti p C t r l : : C W z d To o l Ti p C t r l ( ) { } C W z d To o l Ti p C t r l : : ~ C W z d To o l Ti p C t r l ( ) { } BEGIN_MESSAGE_MAP( CWzdTo o l TipCtrl, CTo o l TipCtrl ) // {{AFX_MSG_MAP( CWzdTo o l TipCtrl ) // NOTE - the ClassWizard will add and remove mapping macros here. // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTo o l TipCtrl message handlers BOOL CWzdTo o l Ti p C t r l : : A d d Tool( UINT nID, LPCTSTR lpszText ) { TOOLINFO ti; 252第第第二部分第用户界面实例 下载memset( &ti, 0, sizeof( TOOLINFO ) ); ti.cbSize = sizeof( TOOLINFO ); ti.hwnd = GetOwner() -> GetSafeHwnd(); ti.uFlags = TTF_IDISHWND|TTF_SUBCLASS; ti.uId = ( UINT )GetParent() -> GetDlgItem( nID ) -> m_hWnd; t i . l p s z Text = ( LPSTR )lpszTe x t ; return ( BOOL )SendMessage( TTM_ADDTOOL, 0, ( LPARAM )&ti ); } BOOL CWzdTo o l Ti p C t r l : : A d d Tool( UINT nID, UINT nIDText ) { CString str; VERIFY( str.LoadString( nIDText ) ); return AddTool( nID,str ); } BOOL CWzdTo o l TipCtrl::AddArea( UINT nID, LPRECT lpRect, LPCTSTR lpszText ) { TOOLINFO ti; memset( &ti, 0, sizeof( TOOLINFO ) ); ti.cbSize = sizeof( TOOLINFO ); ti.hwnd = GetOwner() -> GetSafeHwnd(); ti.uFlags = TTF_SUBCLASS; ti.uId = nID; memcpy( &ti.rect, lpRect, sizeof( RECT ) ); t i . l p s z Text = ( LPSTR )lpszTe x t ; return ( BOOL )SendMessage( TTM_ADDTOOL, 0, ( LPARAM )&ti ); } BOOL CWzdTo o l TipCtrl::AddArea( UINT nID, LPRECT lpRect, UINT nIDText ) { CString str; VERIFY( str.LoadString( nIDText ) ); return AddArea( nID,lpRect,str ); } 第10章第帮第第助第第253下载下载下载 第11章 普 通 窗 口 M F C应用程序中几乎所有的窗口都是特殊的— 要么是对话框,要么是视窗口或是控件 窗口。但它们都是同一种窗口即普通窗口的不同变化。本章的实例将讲解如何创建一个通用 窗口,它适用于任何情况。 Wi n d o w s应用程序的用户界面完全是由单个的窗口所组成的,这些窗口具有不同的尺寸 和风格。多数情况下,使用 Developer Studio中的编辑器和向导自动地将窗口加入到应用程序 中去,纵观全书可以找到许多这方面的例子。不幸的是,向导只能创建一些特定种类的窗口, 除此之外就需要自己动手去创建自己的窗口了。 本章中包含在应用程序用户界面的任何位置创手工建各种窗口的实例。它们包括: 实例38 创建普通窗口。本例将演示如何只用 M F C的一般窗口过程来创建窗口。 实例39 创建一个窗口类— 短调用形式,本例将演示如何使用 M F C的A f x R e g i s t e r W n d - Class ()函数创建自定义窗口类,该函数自动填充了很多空白。 实例40 创建一个窗口类— 长调用形式,本例将演示如何使用 M F C的A f x R e g i s t e r C l a s s ( ) 函数创建一个窗口类,该函数可以由用户完全控制窗口的类创建过程。 11.1 实例38:创建普通窗口 1. 目标 创建一个普通窗口,如图11 - 1所示。 2. 策略 本例将使用M F C的通用窗口类C W n d 来创建这些窗口。同时还将用 Wi n d o w s A P I来直接创建窗口。然后使用一种将这 个C W n d对象连接到已存在窗口上的方 法。 3. 步骤 1) 使用C W n d创建一个普通窗口 使用M F C创建一个普通窗口,可以 使用: CWnd wnd; HMENU hMenu = ::LoadMenu( NULL,MAKEINTRESOURCE( IDR_WZD_MENU ) ); w n d . C r e a t e E x ( 0 , // extended window style _T( "AfxWnd" ), // MFC window class name " C a p t i o n " , // window caption W S _ C H I L D | W S _ V I S I B L E , // window style 1 0 , 1 0 , // x,y position 1 0 0 , 7 5 , // width and height 图11-1 四个普通窗口 使用“ B U T TO N”窗口类 和C W n d类创建的按钮 使用Windows API创建并用一 个C W n d类封装的重叠窗口 使用C B u t t o n类创 建的按钮 使用C W n d和自己的窗 口类创建的重叠窗口m _ h W n d , // parent window handle h M e n u // menu handle, // or if child window, a window id ) ; 上面使用的的窗口类名是通用 M F C窗口。为了创建一个按钮控件,只需要使用 A f x W n d来 代替B U T TO N: CWnd wndButton; wndButton.CreateEx( 0, // extended window style _T( "BUTTON" ), // window class name "My Button", // window caption W S _ C H I L D | W S _ V I S I B L E , // window style 1 0 , 1 0 , // x,y position 1 0 0 , 7 5 , // width and height h W n d , // parent window handle ( HMENU )IDC_WZD_BUTTO N // in this case, a button id ) ; 为使用MFC CButton类来创建完全相同的按钮,可使用以下代码, C B u t t o n中的C r e a t e ( )成 员函数只完成上面我们所完成的工作: CRect rect( 200, 200, 300, 275 ); CButton button; b u t t o n . C r e a t e ( " B u t t o n " , // window caption W S _ C H I L D | W S _ V I S I B L E , // window style r e c t , // position and dimensions t h i s , // parent window class I D C _ W Z D _ B U T TO N // button id ) ; 2) 使用Windows API创建一个普通窗口 为了直接用Windows API创建一个普通窗口,可以直接使用下面的代码。这个例子中将创 建一个重叠窗口。请参照后面的注意以了解重叠窗口、弹出窗口和子窗口的不同之处。 HWND hWnd = ::CreateWi n d o w E x ( W S _ E X _ C L I E N T E D G E , // extended window style " A f x W n d " , // windows class name " O v e r l a p p e d " , // window caption W S _ C A P T I O N | W S _ S Y S M E N U | W S _ O V E R L A P P E D | W S _ V I S I B L E | W S _ D L G F R A M E , // window style 220, 220, 200, 100, // position and dimensions N U L L , // owner window handle—NULL is Desktop h M e n u , // for popup and overlapped windows A f x G e t I n s t a n c e H a n d l e ( ) , // handle to application instance N U L L // pointer to window-creation data ) ; 3) 使用C W n d对象封装Wi n d o w s对象 可以使用下面的代码,使用 W n d类来封装刚才创建的窗口: CWnd wndWr a p p e r ; w n d Wr a p p e r.Attach( hWnd ); 第11章第普 通 窗 口第第255下载一旦封装该窗口,当C W n d类被析构时,C W n d将会销毁这个窗口。 为了在析构C W n d类或其派生类(例如:C b u t t o n )时,C W n d附属的窗口不被销毁,可以在 开始时用下面的代码将进行分离: HWND hWnd = wndWr a p p e r. D e t a c h ( ) ; : : D e s t r o y Window( hWnd ); 如果希望窗口销毁该窗口所附属的 M F C类,可使用C l a s s Wi z a r d加入W M _ N C D E S T R O Y消 息处理函数到这个类中。然后在消息处理函数中加入如下的代码: void CWzdWnd::OnNcDestroy() { C W n d : : O n N c D e s t r o y ( ) ; delete this; } 4. 注意 ■ 普通窗口是在 Wi n d o w s界面中所看到的所有窗口的基础。共有三种普通类型的窗口: 重叠窗口、弹出窗口以及子窗口。它们是主窗口、对话框和消息框、控件窗口 (例如控件)的基 础。Windows API通常在重叠窗口或弹出窗口所谓的非客户区进行大量的绘制工作。另一方 面,子窗口是在窗口类中定义的,它通常是在窗口过程中绘制。控件窗口都是具有用于绘制 各自控件的唯一窗口过程的子窗口。关于窗口的更多的细节,请参阅第 1章。 ■ 在调用了C r e a t e E x ( )和C r e a t e Wi n d o w ( )函数之后, 使用一系列从Windows API到该窗口 的窗口过程的消息便创建了一个 M F C窗口。关于该事件发生序列,请参阅附录 A。 ■ 添加一个W M _ N C D E S T R O Y消息处理函数来析构一个M F C类在大多数情况下是不必要 的。这是因为该类要么是嵌入到另一个类中,而后者在应用程序结束时将自动析构前者,要 么是被分配到某一函数的堆栈中,当函数返回时,这个类也将被析构。但是有一种情况需要 这样做,就是无模式对话框。只要用户单击 C l o s e按钮,无模式对话框窗口将会被销毁。而该 窗口附属的类将被剩下,它没有窗口可访问也无法析构,结果将导致内存泄漏。 ■ 本例使用的类由Windows 操作系统和M F C所支持。为创建自己的窗口类,请参考实例 3 9和实例4 0。 5. 使用光盘时注意 执行附带光盘上的工程时,会发现视将由不同方式创建的四个基本窗口所填充。 11.2 实例39:创建短调用形式窗口类 1. 目标 创建一个通用窗口类以便于创建窗口时所使用。 2. 策略 M F C框架为创建和注册一个窗口类提供了两个函数,本例将使用短版本的调用: A f x R e g i s t e r W n d C l a s s ( )。为了在创建窗口类的过程具有更多的控制,请参考实例 4 0。 3. 步骤 1) 用A f x R e g i s t e r W n d C l a s s ( )函数创建新窗口类 为创建窗口类,可以使用: 256第第第二部分第用户界面实例 下载lpszClass = AfxRegisterWndClass( // window class styles CS_DBLCLKS | // convert two mouse clicks into // a double click to this // window’s process C S _ H R E D R AW | // send WM_PAINT to window // if horizontal size changes C S _ V R E D R AW // send WM_PAINT to window // if vertical size changes // CS_OWNDC | // every window created from // this class gets its very // own device context // CS_PARENTDC | // device context created for // this window allows drawing // in parent window too // CS_NOCLOSE | // disable the close command // on the System menu : : L o a d C u r s o r ( N U L L , I D C _ C R O S S ) , // window class cursor // or NULL for default arrow // cursor (this cursor is // displayed the when mouse // cursor is over a window // created with this class) ( H B R U S H ) ( C O L O R _ B A C K G R O U N D + 1 ) , // background color // or NULL for no background // erase (if NULL, window // will not erase // background for you) AfxGetApp() -> LoadIcon(IDI_WZD_ICON) // window icon or // NULL for default // icon (icon // displayed in // window caption // or in minimized // window) ) ; A f x R e g i s t e r W n d C l a s s ( )自动生成一个新的窗口类名。为了使用这个新的窗口类创建窗口, 只需使用这个已生成的名字来创建即可。 2) 使用由A f x R e g i s t e r W n d C l a s s ( )创建的窗口类 为使用新的窗口类,将A f x R e g i s t e r W n d C l a s s ( )创建的类名加入到C W n d : : C r e a t e E x ( )函数中, 如下所示: CWnd wnd; wnd.CreateEx( 0,lpszClass," ",WS_OVERLAPPEDWINDOW|WS_VISIBLE, 100, 100, 200, 100, NULL, NULL ); 3) 创建最简单的窗口类 为使用A f x R e g i s t e r W n d C l a s s ( )创建最简单的窗口类,可以使用如下代码: lpszClass = AfxRegisterWndClass( 0 ); 第11章第普 通 窗 口第第257下载通过该窗口类创建的窗口将有一个箭头光标和一个缺省图标,其背景不能擦除。 4. 注意 ■ 窗口类名只是一个文本字符串,用于标识已在系统中注册的窗口类结构。窗口类结构 用于维护用户窗口类的风格、背景色以及在窗口创建时进行初始化的窗口过程。关于窗口类 的更多信息,请参阅第1章。 ■ A f x R e g i s t e r W n d C l a s s ( )自动创建和初始化一个窗口结构。对于窗口过程,它使用一个 名为A f x W n d P r o c的普通M F C窗口过程。A f x R e g i s t e r W n d C l a s s ( )还可以根据所传递的参数自动 创建窗口类名。然而这种方式的一个缺点是如果使用完全相同的参数分两次调用该函数时, 将只能创建一个窗口类。一般情况下这将无伤大雅,除非使用不连续的 C S _ C L A S S D C。但即 使在这种情况下,也只是在两个由该窗口类创建的窗口试图同时进行绘制时才会出现问题。 关于这方面问题的详细信息,请参阅第 1章。 ■ 如果希望在创建窗口类具有更多的自主控制,包括给自行确定窗口类名等等,请参考 实例4 0。 5. 使用光盘时注意 执行附送光盘上的工程时,在 W z d Vi e w中的O n Te s t W z d 1 ( )函数中设置一个断点。单击 Te s t和W z d 1菜单命令,在应用程序中创建两个窗口类及用这两个窗口类创建两个窗口时,跟 踪该过程。 11.3 实例40:创建长调用形式窗口类 1. 目标 创建一个特定的窗口类并将能够自行确定窗口类名。 2. 策略 M F C框架中提供了两个函数以创建和注册一个窗口类。在本例中将使用长形式的调用: A f x R e g i s t e r C l a s s ( )。它允许在窗口类创建过程中用户具有更多的控制权。为创建一个快速、 通用的窗口类,请参考实例 3 9。 3. 步骤 1) 用A f x R e g i s t e r C l a s s ( )创建一个窗口类 为创建一个窗口类,必须首先如下所示初始化 W N D C L A S S结构: WNDCLASS wndclass = { // window class styles CS_DBLCLKS | // convert two mouse clicks into a double // click to this window’s process C S _ H R E D R AW | // send WM_PAINT to window if horizontal // size changes C S _ V R E D R AW // send WM_PAINT to window if vertical // size changes // CS_GLOBALCLASS | // class is available to all process threads // CS_OWNDC | // every window created from this class // gets its very own device context // CS_PARENTDC | // device context created for this window // allows drawing in parent window too 258第第第二部分第用户界面实例 下载// CS_NOCLOSE | // disable the close command on the // System menu A f x W n d P r o c , // window process for every window created // from this class 0 , 0 , // extra window and class bytes unused // in MFC AfxGetInstanceHandle(), // handle of this application’s instance AfxGetApp() -> LoadIcon( IDI_WZD_ICON ), // window icon or NULL ::LoadCursor( NULL,IDC_CROSS ), // window class cursor or // NULL for default // arrow cursor ( HBRUSH )( COLOR_BACKGROUND + 1 ), // background color or NULL // for no background // erase MAKEINTRESOURCE( IDR_WZD_MENU ), // menu to be used when // creating windows // using this class " M y C l a s s N a m e " // a class name you are // assigning this // windows class } ; 然后可以在系统中注册窗口类,使用以下代码: AfxRegisterClass( &wndclass ); 2) 使用由A f x R e g i s t e r C l a s s ( )创建的窗口类 为使用这个新的窗口类,将其名字加入到 C W n d : : C r e a t e E x ( )函数中,如下所示: CWnd wnd; wnd.CreateEx( 0, "MyClassName", <<< new class name " ",WS_OVERLAPPEDWINDOW|WS_VISIBLE, 100, 100, 200, 100, NULL, NULL ); 4. 注意 ■ 所有由M F C创建的窗口都使用 A f x W n d P r o c窗口过程。这允许发送到 M F C窗口的消息 以同样的方式进行处理。该窗口过程确定了窗口的外观和使用感觉。一个 B U T TO N窗口类则 具有一个特殊的窗口过程,该过程将绘制一个按钮以作为对发送给该过程的 W M _ PA I N T消息 的响应。关于这方面的详细信息,请参阅第 1章。 ■ 本例中定义的图标将在具有标题的窗口或是最小化窗口的左上角上显示。缺省的图标 由系统提供,并随操作系统的不同而不同。 ■ 当鼠标在该窗口类创建的窗口的客户区移动时,这里定义的光标将显示。若未定义, 则使用缺省的箭头光标。 ■ 如果将背景画刷设置成 N U L L,则系统不能擦除窗口的背景。这意味着当窗口创建时, 窗口的所有其他非客户区仍将被绘制,而客户区内容将保留。即窗口下面的内容仍将显示 在客户区中。为了自行擦除客户区的背景,可使用 C l a s s Wi z a r d添加W M _ E R A S E B K G R N D 消息处理函数到 M F C类中。在其中可以绘制一个矩形以填充背景,或提供一些其他的有趣 的背景。 ■ 这里提供的菜单句柄将被分配到由该窗口类创建的每一个窗口中。在创建窗口时,只 第11章第普 通 窗 口第第259下载需为C W n d : : C r e a t e E x ( )提供一个新的菜单句柄就可以替代缺省菜单。如果菜单句柄在窗口类和 C W n d : : C r e a t e E x ( )中被省略,那么窗口在创建时就没有菜单。由于子窗口不能有菜单— 因此 在窗口类没有使用菜单句柄,但在 C W n d : : C r e a t e E x ( )中它被用作一个控件标识号。 ■ 这里提供的类名可以是任何的文本名,包括现有的窗口类名,例如 B U T TO N。但是, 使用现有的类名来命名新的窗口类将意味着以后所有的窗口都将从这个新类中创建。例如, 如果将新的窗口类命名为 B U T TO N,则将来所有的按钮控件都将使用该窗口类创建,而不是 从系统缺省的B U T TO N窗口类创建。有关超类化的信息请参阅第 1章。 ■ 应用程序注册的窗口类只对该应用程序才可用的。如果使用 C S _ G L O B A L C L A S S类风 格,那么窗口类对每一个该应用程序中创建的线程则都将是可用的,但是对系统中其他的应 用程序是不可用的,至少在 Windows 3.1是这样。当应用程序结束时,所创建的窗口类都将被 注销和销毁。 260第第第二部分第用户界面实例 下载下载下载 第12章 特定的应用程序 本章的实例相同之处很少,但它们的创建过程一般较简单或者经常被创建,或是二者皆 有。M F C已经准备了两个精心制作的编辑器的示例 (记事本( N o t e p a d )和写字板( Wo r d p a d ) ),但 正由于这两个实例制作太细致,很难看出如果创建一个不加修饰的实例时会是多么的简单。 资源管理器( E x p l o r e r )界面也是一个像 S D I和M D I界面那样广泛使用的界面,但创建起来并不 是很容易。本章的实例包括: 实例41 创建简单的文本编辑器。本例将创建一个十分完整的文本编辑器,但并不需要太 多的工作。 实例42 生成简单的RT F编辑器。本例将创建一个具有操纵字体和图片功能的编辑器,类 似于Wo r d处理函数。 实例43 创建资源管理器界面。本例将创建一个类似资源管理器的应用程序,左边具有一 个树形控件,右边则具有一个列表控件。 实例44 创建简单的O D B C数据库编辑器。本例将创建一个简单的数据库编辑器,它可以 操作符合O D B C标准的数据库。 实例45 创建简单的D A O数据库编辑器。和实例4 4一样,但具有操作D A O数据库功能。 实例46 创建简单的向导,本例将用属性表生成一个简单的向导。 12.1 实例41:创建简单的文本编辑器 1. 目标 如图1 2 - 1所示,使用编辑控件创建一个简单的文本编辑器。 2. 策略 本例将通过A p p Wi z a r d使用M F C的C E d i t Vi e w, 创建一个简单的文本编辑器。还将提供利用 C l a s s Wi z a r d为现有应用程序创建文本编辑器的方法。 3. 步骤 1) 使用A p p Wi z a r d生成文本编辑器 使用A p p Wi z a r d,生成一个 S D I或是M D I应用 程序。 当仍处于 A p p Wi z a r d中时,使用 A d v a n c e d O p t i o n s,为该* . t x t工程文件生成“文件扩展名 ( F i l e E x t e n s i o n )”。 在A p p Wi z a r d最后一步将会看到一个为这个工程生成类的详细清单。选择 C X x x Vi e w类, 其中X x x是自己定义的工程文件名。然后将注意到下面的基类组合框被启用,此时应当从该组 合框中选择C E d i t Vi e w。 这样便可以了。 A p p Wi z a r d此时将创建合适的视类和文档类。视类支持剪切和粘贴功能, 图12-1 简单文本编辑器文档类将被修改以装载和保存文本文件。普通的文件对话框将用于提醒用户使用前面指定的 * . t x t扩展名以装载和保存文件。 2) 使用C l a s s Wi z a r d生成一个文本编辑器视 在一个现有的应用程序中,使用 C l a s s Wi z a r d生成一个由C E d i t Vi e w派生而来的新视类。 使用C l a s s Wi z a r d生成一个由C D o c u m e n t派生而来的新文档类。 在这个新的文档类中,加入下面的代码到序列化函数中,以允许装载和保存文本文件: void CWzdDoc::Serialize(CArchive& ar) { POSITION pos = GetFirstVi e w P o s i t i o n ( ) ; ( ( CWzdEditView * )GetNextView( pos ) ) -> SerializeRaw( ar ); } 如果正在创建S D I应用程序,当新的文件被装载时,需要取消视类。为此需要添加下面的 代码到文档类的O n N e w D o c u m e n t ( )函数中: BOOL CWzdDoc::OnNewDocument() { if ( !CDocument::OnNewDocument() ) return FA L S E ; POSITION pos = GetFirstVi e w P o s i t i o n ( ) ; <<< add ( ( CWzdEditView * )GetNextView( pos ) ) -> S e t Wi n d o w Te x t ( N U L L ) ; <<< add return TRUE; } 使用这两个新类就可以在应用程序类中创建新的文件模板类。 4. 注意 ■ C E d i t Vi e w类是C Vi e w类和C E d i t控件类的混合物。 C E d i t Vi e w类同时也包含成员函数 S e r i a l i z e R a w ( ),该函数使装载和保存文件变得更容易。 ■ 在MFC C++软件包中随之附带了几个优秀的文本编辑器实例。然而,因为它们都包含 了一些用起来比较花哨的功能,结果代码看上去有些复杂,这就使得用户对使用 M F C的内置 特征创建文本编辑器实际上非常简单这一事实产生误解。 5. 使用光盘时注意 当运行随书附送光盘上的工程时,将会注意到它具备了一个简单文本编辑器的所有功能。 12.2 实例42:生成简单的RTF编辑器 1. 目标 如图4 - 2所示,使用丰富编辑控件 (rich edit control)生成一个简单的RT F 编辑器。 2. 策略 本例使用M F C的C R i c h E d i t Vi e w和 C R i c h E d i t D o c,用A p p Wi z a r d制作一 262第第第二部分第用户界面实例 下载 图12-2 简单RTF编辑器 丰富编 辑文本个简单的多信息文本格式 ( RT F )的文本编辑器。同时还将看到如何使用 C l a s s Wi z a r d为一个现有 的应用程序添加RT F文本编辑器。 3. 步骤 1) 使用A p p Wi z a r d生成一个文本编辑器 使用A p p Wi z a r d,创建一个S D I或是M D I应用程序。 当仍处于A p p Wi z a r d时,使用Advanced Options为该* . r t f工程文件生成“文件扩展名 ( F i l e E x t e n s i o n )”。 在A p p Wi z a r d的最后一步,将会看到一个将为这个工程而生成的类详细清单。选择 C X x x Vi e w类,其中X x x是自己的工程文件名。然后将会注意到在下面基类的组合框被启动。 此时应当从这个组合框中选择 C R i c h E d i t Vi e w。 这样便可以了。此时 A p p Wi z a r d将生成合适的视类和文档类。前者支持剪切和粘贴功能, 后者将被修改以装载和保存 RT F文件。普通的文件对话框将用于提醒用户使用前面上面定义 的* . r t f扩展名以装载和保存文件。 2) 使用C l a s s Wi z a r d生成一个RT F编辑器视 使用C l a s s Wi z a r d生成一个由C R i c h E d i t Vi e w派生而来的新的视类。 由于当前无法使用 C l a s s Wi z a r d创建一个由C R i c h E d i t D o c派生而来的新文档类,因此使用 C l a s s Wi z a r d生成一个由 C D o c u m e n t派生而来的新文档类作为替代。然后手工编辑生成的 . h 和. c p p文件,用CRichEditDoc 代替所有在文件中出现的C D o c u m e n t。 手工添加以下代码以重载文档类。对 C R i c h E d i t D o c而言,这是一个辅助函数,它将返回 下一步将要创建的类,并允许文档中包含诸如 RT F文本的C O M对象: CRichEditCntrItem* CWzdDoc::CreateClientItem( REOBJECT* pReo ) const { return new CWzdCntrItem( pReo, ( CWzdDoc* )this ); } 从资源中手工创建一个类,详细代码可以在本例结尾的程序清单—丰富编辑容器项目类 (Rich Edit Container Item Class)中找到。 使用这两个新类就可以在应用程序类中建立新的文件模板类。 4. 注意 ■ C R i c h E d i t Vi e w类是C Vi e w类和C R i c h E d i t C t r l控件类的混合物。C R i c h E d i t D o c类使装载 和保存 RT F文件变得更容易。一个叫做 C R i c h E i d t C n t r I t e m 的第三方 M F C 类可帮助 C R i c h E d i t D o c类装载和保存RT F文件。 ■ 如果只打算使用C R i c h E d i t C t r l控件类,则必须在应用程序类的 O n I n i t I n s t a n c e ( )函数中 加入对A f x I n i t R i c h E d i t ( )函数的调用。这将初始化应用程序以使用 RT F控件。C R i c h E d i t Vi e w类 也同时为用户调用这个函数。 ■ 嵌入到丰富编辑视(Rich Edit Vi e w )中的丰富文本控件同样具有内置功能,可以设置字 体和段落格式,请参看有关 C R i c h E d i t C t r l类的M F C文档以了解如何实现这些功能。 5. 使用光盘时注意 当运行随书附送光盘上的工程时,将会注意到它可以打开和显示 . r t f文件。 6. 程序清单— 丰富编辑容器项目类(Rich Edit Container Item Class) // WzdCntrItem.h : interface of the CWzdCntrItem class 第12章第特定的应用程序第第263下载/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #if !defined WZDCNTRITEM #define WZDCNTRITEM class CWzdDoc; class CWzdRichEditVi e w ; class CWzdCntrItem : public CRichEditCntrItem { DECLARE_SERIAL( CWzdCntrItem ) // Constructors p u b l i c : CWzdCntrItem( REOBJECT* pReo = NULL, CWzdDoc* pContainer = NULL ); // Attributes p u b l i c : CWzdDoc* GetDocument() { return (CWzdDoc*)COleClientItem::GetDocument(); } C W z d R i c h E d i t View* GetActiveVi e w ( ) { return (CWzdRichEditVi e w * ) C O l e C l i e n t I t e m : : G e t A c t i v e View(); } // ClassWizard generated virtual function overrides // {{AFX_VIRT U A L ( C W z d C n t r I t e m ) p u b l i c : p r o t e c t e d : // }}AFX_VIRT U A L // Implementation p u b l i c : #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; # e n d i f } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / # e n d i f // WzdCntrItem.cpp : implementation of the CWzdCntrItem class / / #include "stdafx.h" #include "Wzd.h" #include "WzdDoc.h" #include "WzdRichEditVi e w. h " #include "WzdCntrItem.h" #ifdef _DEBUG 264第第第二部分第用户界面实例 下载#undef THIS_FILE static char BASED_CODE THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdCntrItem implementation IMPLEMENT_SERIAL( CWzdCntrItem, CRichEditCntrItem, 0 ) CWzdCntrItem::CWzdCntrItem( REOBJECT *pReo, CWzdDoc* pContainer ) : CRichEditCntrItem( pReo, pContainer ) { } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdCntrItem diagnostics #ifdef _DEBUG void CWzdCntrItem::AssertValid() const { C R i c h E d i t C n t r I t e m : : A s s e r t Va l i d ( ) ; } void CWzdCntrItem::Dump( CDumpContext& dc ) const { CRichEditCntrItem::Dump( dc ); } # e n d i f 12.3 实例43:创建资源管理器界面 1. 目标 创建一个界面类似于Wi n d o w s资源管理器的应用程序,如图 1 2 - 3所示。 2. 策略 Wi n d o w s资源管理器是一个 S D I应用程序, 它只有一个文档但是有两个视。第一个视是树形 视,第二个是列表控件视。这两个视实际上都是 在另一个名为分离窗口 (splitter window)的窗口 中,该窗口填充了整个主窗口。 Visual C++6.0版 本中的A p p Wi z a r d允许自动选择和生成这个界面 (在其第5步中选择) ,然而它不填充使这两个视同 步所需的逻辑代码. 因此,在本例中,对于没有使用 6 . 0版本的 情况将首先手工创建这个界面。方法是修改 C M a i n f r a m e类,将树形视分解为两个部分,并将列表视加入到第二个部分。然后使用标准的 C Tr e e Vi e w和C L i s t Vi e w类调用来填充这些视。同时将同步这些视使树形视中的内容与列表视 第12章第特定的应用程序第第265下载 图12-3 资源管理器界面 与Windows Explorer(tm)一样,本 实例显示如何创建这样的界面:一 边是树形控件,另一边是列表控件中的内容相关联,反之亦然。为使每一级的子目录都能被显示,将大量使用递归调用。本例 中将显示所有的文件目录,对任何与之相似的层次结构都可以使用这些方法显示。 3. 步骤 1) 创建应用程序 使用Application Wi z a r d生成一个S D I应用程序。在最后一步将得到一个为工程文件生成的 类的详细清单。选择 C X x x Vi e w类,其中 X x x是自己的工程文件名。选择 Base class下的 C Tr e e Vi e w,单击F i n i s h以完成工程文件。 使用C l a s s Wi z a r d创建一个由C L i s t Vi e w派生而来的新的视类。 在C M a i n F r a m e类中嵌入一个分离窗口类,如下所示: p r i v a t e : CSplitterWnd m_wndSplitter; 然后,使用C l a s s Wi z a r d为O n C r e a t e C l i e n t ( )函数添加并重载一个C M a i n F r a m e类。在其中首 先创建一个具有两栏的可分离窗口: BOOL CMainFrame::OnCreateClient( LPCREATESTRUCT lpcs, CCreateContext* pContext ) { // create a split window with 1 row and 2 columns if ( !m_wndSplitter.CreateStatic( this, 1, 2 ) ) { TRACE0( "Failed to CreateStaticSplitter\n" ); return FA L S E ; } 接下来在O n C r e a t e C l i e n t ( )中告诉分离窗口使用传递到这个函数的树形视作为第一栏中的 视。 CRect rect; GetClientRect( &rect ); if ( !m_wndSplitter. C r e a t e View( 0, 0, pContext -> m _ p N e w ViewClass, CSize( rect.Width()/4, 50 ), pContext ) ) { TRACE0( “Failed to create first pane\n” ); return FA L S E ; } 注意正在设置应用程序主窗口的宽度以及该栏的宽度。 接下来在O n C r e a t e C l i e n t ( )中,告诉可分离窗口第二栏是用 C l a s s Wi z a r d生成的列表视: if ( !m_wndSplitter. C r e a t e View( 0, 1, RUNTIME_CLASS( CWzdListView ), CSize( 200,50), pContext ) ) { TRACE0( "Failed to create second pane\n" ); return FA L S E ; } 最后在O n C r e a t e C l i e n t ( )中将告诉C M a i n F r a m e该列表视在初始时是被激活的视,也可以 返回T R U E而不调用初始化的 C F r a m e W n d : : O n C r e a t e C l i e n t ( ),这将取消刚才所进行的全部工 作。 266第第第二部分第用户界面实例 下载S e t A c t i v e View( ( CView* )m_wndSplitter.GetPane( 0,1 ) ); return TRUE; // CFrameWnd::OnCreateClient( lpcs, pContext ); 2) 填写树形视类 使用C l a s s Wi z a r d在树形视类中加入一个重载的 O n I n i t i a l U p d a t e ( )。在其中将创建和装载位 图,这些位图在树形控件中显示: void CWzdTr e e Vi e w : : O n I n i t i a l U p d a t e ( ) { C Tr e e Vi e w : : O n I n i t i a l U p d a t e ( ) ; m_ImageList.Create( IDB_SMALL_BITMAP, 16, 1, RGB( 0,0,0 ) ); G e t TreeCtrl().SetImageList( &m_ImageList, TVSIL_NORMAL ); } 资源管理器类型界面的最基本的操作是单击树形视的某一项目时,将会打开树形视中相 对应的文件。树形视中的这一操作在列表视中可以通过双击来实现。这里将使用文档类中的 U p d a t e A l l Vi e w s ( )来同步这两个视。因此这两个视必须重载 O n U p d a t e ( )函数,使用提供的l H i n t 来确定其操作,如下面所示。 使用C l a s s Wi z a r d重载树形视类的O n U p d a t e ( )函数。然后检查是否这一修改只是修改了列 表视,如果是则立即返回: void CWzdTr e e View::OnUpdate( CView* pSender, LPARAM lHint, CObject* pHint ) { if ( lHint&LIST_VIEW_ONLY ) { r e t u r n ; } 接下来在这个 O n U p d a t e ( )中,使用一个 C a s e语句来确定如何去修改这个视。缺省的情况 下只是使用一个辅助函数并用文档来填充该视,如下所示 : switch ( lHint ) { d e f a u l t : G e t Tr e e C t r l ( ) . D e l e t e A l l I t e m s ( ) ; AddBranch( TVI_ROOT, GetDocument() -> GetWzdList() ); b r e a k ; 该辅助函数A d d B r a n c h ( )可以在本例结尾的程序清单—树形视类中找到。它使用了许多的 递归调用来填充该视,其中不仅包括名字,而且还包括在将来进一步标识列表项目的数据对 象指针。其中的数据对象包含有关名字、目录 ( F I L E、D I R C TO RY等)、注释、还有项目的对 象标识号的信息。对象标识号在试图从位于其他视中的视中定位某一项目时显得非常重要, 如下面所示。 如果视的这种修改是由用户在列表视中选择了新的项目引起的,在此便需要在树形视中 选择相同的项目。为此首先在树形视中定位这个相同的项目。然后如下所示选择它: case TREE_VIEW_SELECT: 第12章第特定的应用程序第第267下载pInfo = GetDocument() -> GetSelection(); if ( (hitem = F i n d TreeItem( GetTreeCtrl().GetChildItem( GetTreeCtrl().GetRootItem() ), pInfo -> m_nObjectID ) ) != NULL ) { G e t TreeCtrl().SelectItem( hitem ); } b r e a k ; 这里可以看到,通过使用列表视项目的对象标识号来定位树形视中与之相同的项目。 同样也可以处理这种情况,即用户希望沿树形层次结构向上 (从该子目录到其上一层目录 )。 在这个例子中,用户通过一个工具栏按钮完成此操作。 case TREE_VIEW_UP_LEVEL: hitem = GetTr e e C t r l ( ) . G e t S e l e c t e d I t e m ( ) ; hitem = GetTreeCtrl().GetParentItem( hitem ); G e t TreeCtrl().SelectItem( hitem ); b r e a k ; } 为确定用户何时选择树形视中一个新的项目,可以使用 C l a s s Wi z a r d 添加 T V N _ C H A N G E D控件通知消息处理函数,以通知文档类用户的选择已经改变,文档类然后使用 U p d a t e A l l Vi e w ( )函数去通知列表视需要进行修改。 void CWzdTr e e View::OnSelchanged( NMHDR* pNMHDR, LRESULT* pResult ) { NM_TREEVIEW* pNMTr e e View = ( NM_TREEVIEW* )pNMHDR; GetDocument() -> SaveSelection( ( CWzdInfo * )pNMTr e e View -> i t e m N e w. l P a r a m , L I S T _ V I E W _ O N LY ); *pResult = 0; } 参考本实例结尾的程序清单— 树形视类,查看树形视类的完整代码列表。 3) 填写列表视类 使用C l a s s Wi z a r d重载O n I n i t i a l U p d a t e ( )。在此应当装载一些大小不一的位图,以便在列表 视中使用。在用户使用报表风格时还应当初始化列表视的各栏目以及对应的栏目名称: void CWzdListVi e w : : O n I n i t i a l U p d a t e ( ) { C L i s t Vi e w : : O n I n i t i a l U p d a t e ( ) ; // attach image lists to list control m_ImageList.Create( IDB_NORMAL_BITMAP, 32, 1, RGB( 0,0,0 ) ); GetListCtrl().SetImageList( &m_ImageList, LVSIL_NORMAL ); m_ImageSmallList.Create( IDB_SMALL_BITMAP, 16, 1, RGB( 0,0,0 ) ); GetListCtrl().SetImageList( &m_ImageSmallList, LVSIL_SMALL ); // initialize list control CClientDC dc( this ); TEXTMETRIC tm; 268第第第二部分第用户界面实例 下载d c . G e t TextMetrics( &tm ); GetListCtrl().ModifyStyle( 0,LV S _ R E P O RT | LV S _ S H O W S E L A LWAYS ); GetListCtrl().SendMessage( LV M _ S E T E X T E N D E D L I S T V I E W S T Y L E , 0 , LVS_EX_FULLROWSELECT ); GetListCtrl().InsertColumn( 0,"Name",LV C F M T _ L E F T, 2 0 * t m . t m Av e C h a r Width,0 ); GetListCtrl().InsertColumn( 1,"Comment", LV C F M T _ L E F T, 3 0 * t m . t m Av e C h a r Width,1 ); } 接下来,使用 C l a s s Wi z a r d重载O n U p d a t e ( )函数。如果这只是一次树形视的修改则返回, 并且再次使用C a s e语句来处理修改操作: void CWzdListView::OnUpdate( CView* pSender, LPARAM lHint, CObject* pHint ) { if ( lHint&TREE_VIEW_ONLY ) { r e t u r n ; } G e t L i s t C t r l ( ) . D e l e t e A l l I t e m s ( ) ; switch ( lHint ) { 列表视有四种风格:大图标、小图标、列表以及报表。在本例中,用户可以通过工具栏中 的按钮选择其中的一种,同时文档类在此发送用户请求,并相应地修改该列表视中的控件: case ICON_LIST_VIEW: GetListCtrl().ModifyStyle( LV S _ L I S T | LV S _ S M A L L I C O N | LV S _ R E P O RT, LVS_ICON ); b r e a k ; case LIST_LIST_VIEW: GetListCtrl().ModifyStyle( LV S _ I C O N | LV S _ S M A L L I C O N | LV S _ R E P O RT, LVS_LIST ); b r e a k ; case SMALL_ICON_LIST_VIEW: GetListCtrl().ModifyStyle( LV S _ L I S T | LV S _ I C O N | LV S _ R E P O RT, LVS_SMALLICON ); b r e a k ; case REPORT _ L I S T _ V I E W: GetListCtrl().ModifyStyle( LV S _ L I S T | LV S _ S M A L L I C O N | LV S _ I C O N , LV S _ R E P O RT ); b r e a k ; } 接下来在O n U p d a t e ( )中用文档数据填充列表视。再次使用辅助函数 A d d L i n e ( ),它将再次 被多次递归调用。在程序清单— 列表视类中可以发现其中有一个 A d d L i n e ( )函数的列表。其 第12章第特定的应用程序第第269下载中首先列出子目录中的所有文件夹,然后列出其中所有的文件: // list all folders in selection POSITION pos; CWzdInfo *pSelInfo = GetDocument() -> GetSelection(); for ( pos = pSelInfo -> m_list.GetHeadPosition(); pos; ) { CWzdInfo *pInfo = pSelInfo -> m_list.GetNext( pos ); if ( pInfo -> m_nCategory == CWzdInfo::FOLDER ) { AddLine( pInfo ); } } // list all files in selection for ( pos = pSelInfo -> m_list.GetHeadPosition(); pos; ) { CWzdInfo *pInfo = pSelInfo -> m_list.GetNext(pos); if (pInfo -> m_nCategory == CWzdInfo::FILE) { A d d L i n e ( p I n f o ) ; } } } 如上面所提到的,用户可以用双击列表视项目的方法来改变树形视的相应选择项目。使 用C l a s s Wi z a r d添加W M _ L B U T TO N D B L C L K消息处理函数到列表视类中。在其中查找用户所 选择的项目,并且在发现时通知文档类。文档类将把这个消息传递给树形视类: void CWzdListView::OnLButtonDblClk(UINT nFlags, CPoint point) { int ndx; if ( GetListCtrl().GetSelectedCount() == 1 && ( ndx = GetListCtrl().GetNextItem( -1, LVIS_SELECTED ) ) != -1 ) { GetDocument() -> SaveSelection( (CWzdInfo *)GetListCtrl().GetItemData(ndx), T R E E _ V I E W _ S E L E C T ) ; } C L i s t View::OnLButtonDblClk( nFlags, point ); } 这样就完成了列表视类的编写。关于这个类的完整的代码列表,请参考下面的程序清单 —列表视类。 文档类负责维护数据项,同时还负责同步两个视类 (在本例中使用的数据项可以在下面的 程序清单—数据类中找到 )。改变列表视风格的工具栏按钮和使树形视向上一层的按钮都在文 档类中处理。如前面所述, U p d a t e A l l Vi e w s ( )用来与视通信。 4) 填充文档类 使用工具栏编辑器( Toolbar Editor)为列表视中的四种风格:大图标、小图标、列表和报表 270第第第二部分第用户界面实例 下载添加按钮。 使用C l a s s Wi z a r d为文档类中的这些新工具栏按钮加入命令处理函数。 在这些命令处理函数中,使用 U p d a t e A l l Vi e w s ( )函数通知列表视哪种风格将被使用,如下 所示: void CWzdDoc::OnIconMode() { U p d a t e A l l Views( NULL,ICON_LIST_VIEW ); } void CWzdDoc::OnListMode() { U p d a t e A l l Views( NULL,LIST_LIST_VIEW ); } void CWzdDoc::OnReportMode() { U p d a t e A l l Views( NULL,REPORT_LIST_VIEW ); } void CWzdDoc::OnSmalliconMode() { U p d a t e A l l Views( NULL,SMALL_ICON_LIST_VIEW ); } 在此使用的命令(例如:S M A L L _ I C O N _ V I E W )是在文档类包含文件中定义的。 使用工具栏编辑器和 C l a s s Wi z a r d添加一个按钮控件,它将允许用户转向上一层目录,如 下所示填充有关按钮的代码: void CWzdDoc::OnUplevel() { U p d a t e A l l Views( NULL,TREE_VIEW_UP_LEVEL ); } 使用文本编辑器在文档类中加入一个新的函数,该函数可以被两个视中的任一个调用, 这样便可以修改当前被选择的数据项。该函数应当如下所示: void CWzdDoc::SaveSelection( CWzdInfo *pSelectionInfo,int nMode ) { m_pSelectionInfo = pSelectionInfo; U p d a t e A l l Views( NULL,nMode ); } 这样便可以了。单击任何一个视中的项目都将导致在另一个视中自动选择相同的项目, 以此来产生响应。 4. 注意 ■ Wi n d o w s资源管理器是一个S D I应用程序,当然可以把它创建为一个 M D I应用程序—但 它不能通过A p p Wi z a r d自动创建。按照本实例开始,使用 A p p Wi z a r d来创建一个M D I应用程序 而不是 S D I应用程序。然后不重载 C M a i n F r a m e 的O n C r e a t e C l i e n t ( )函数,而是重载 C C h i l d F r a m e的O n C r e a t e C l i e n t ( )函数来创建一个分离窗口,其他所有操作基本上都是一样的。 也许用户考虑在分离窗口中创建四个视而不是两个。这样便将显示两组树形视和列表视。但 第12章第特定的应用程序第第271下载这需要在文档类中进行说明,因为所有这四个视将使用同一个文档。 ■ 为将一个实际的目录信息填充到视中,可以使用 Windows API调用: : F i n d F i r s t F i l e ( ) 和: : F i n d N e x t F i l e ( )。 ■ 如上面所述,资源管理器类型的界面除了维护文件目录以外,还可以用于其他许多应 用程序。例如用于在 C A D系统中访问窗体,其中的窗体作为文件排列在被称作模型 ( m o d e l )的 目录下面。另一个使用是维护网络,这里每一个网络节点是“目录”,而内部的网卡是“文 件”。 5. 使用光盘时注意 当执行随书附带光盘上的工程时,将注意到它和 Wi n d o w s资源管理器在外观上和感觉上 都非常相似,尽管功能上有些差异。 6. 程序清单— 数据类 #ifndef WZDINFO_H #define WZDINFO_H #include "afxtempl.h" #include "WzdInfo.h" class CWzdInfo : public CObject { p u b l i c : CWzdInfo( CString sName,CString sComment,int nCategory,int nObjectID ); ~ C W z d I n f o ( ) ; enum { D E V I C E , F O L D E R , F I L E } ; // misc info CString m_sName; CString m_sComment; int m_nCategory; int m_nObjectID; CList m_list; } ; # e n d i f // WzdInfo.cpp : implementation of the CWzdInfo class / / #include "stdafx.h" #include "WzdInfo.h" / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdInfo 272第第第二部分第用户界面实例 下载CWzdInfo::CWzdInfo( CString sName,CString sComment,int nCategory, int nObjectID ) : m_sName( sName ), m_sComment( sComment ),m_nCategory( nCategory ), m_nObjectID( nObjectID ) { } C W z d I n f o : : ~ C W z d I n f o ( ) { while (!m_list.IsEmpty()) { delete m_list.RemoveHead(); } } 7. 程序清单— 树形视类 // WzdTr e e Vi e w.h : interface of the CWzdTr e e View class / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #if !defined WZDTREEVIEW_H #define WZDTREEVIEW_H #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 #include class CWzdTr e e View : public CTr e e Vi e w { protected: // create from serialization only C W z d Tr e e Vi e w ( ) ; D E C L A R E _ D Y N C R E ATE( CWzdTr e e View ) // Attributes p u b l i c : CWzdDoc* GetDocument(); // Operations p u b l i c : // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CWzdTr e e View ) p u b l i c : virtual void OnDraw( CDC* pDC ); // overridden to draw this view virtual BOOL PreCreateWindow( CREATESTRUCT& cs ); virtual void OnInitialUpdate(); p r o t e c t e d : 第12章第特定的应用程序第第273下载virtual BOOL OnPreparePrinting( CPrintInfo* pInfo ); virtual void OnBeginPrinting( CDC* pDC, CPrintInfo* pInfo ); virtual void OnEndPrinting( CDC* pDC, CPrintInfo* pInfo ); virtual void OnUpdate( CView* pSender, LPARAM lHint, CObject* pHint ); // }}AFX_VIRT U A L // Implementation p u b l i c : virtual ~CWzdTr e e Vi e w ( ) ; #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump( CDumpContext& dc ) const; # e n d i f p r o t e c t e d : // Generated message map functions p r o t e c t e d : // {{AFX_MSG( CWzdTr e e View ) afx_msg void OnSelchanged( NMHDR* pNMHDR, LRESULT* pResult ); // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : CImageList m_ImageList; void AddBranch( HTREEITEM hTreeItem, CList *pList ); HTREEITEM AddLeaf( HTREEITEM hTreeItem, CWzdInfo *pInfo ); HTREEITEM FindTreeItem( HTREEITEM hTreeItem, long nObjectID ); } ; #ifndef _DEBUG // debug version in WzdTr e e Vi e w. c p p inline CWzdDoc* CWzdTr e e Vi e w : : G e t D o c u m e n t ( ) { return (CWzdDoc*)m_pDocument; } # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Developer Studio will insert additional declarations immediately // before the previous line. # e n d i f // WzdTr e e Vi e w.cpp : implementation of the CWzdTr e e View class / / #include "stdafx.h" #include "Wzd.h" #include "WzdDoc.h" #include "WzdTr e e Vi e w. h " #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE 274第第第二部分第用户界面实例 下载static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTr e e Vi e w I M P L E M E N T _ D Y N C R E ATE( CWzdTr e e Vi e w, CTr e e View ) BEGIN_MESSAGE_MAP( CWzdTr e e Vi e w, CTr e e View ) // {{AFX_MSG_MAP( CWzdTr e e View ) ON_NOTIFY_REFLECT( TVN_SELCHANGED, OnSelchanged ) // }}AFX_MSG_MAP // Standard printing commands ON_COMMAND( ID_FILE_PRINT, CView::OnFilePrint ) ON_COMMAND( ID_FILE_PRINT_DIRECT, CView::OnFilePrint ) ON_COMMAND( ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview ) E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTr e e View construction/destruction C W z d Tr e e Vi e w : : C W z d Tr e e Vi e w ( ) { // set the CTreeCtrl attributes m_dwDefaultStyle = WS_CHILD | WS_VISIBLE | WS_BORDER | TVS_HASLINES | T V S _ S H O W S E L A LWAYS | TVS_LINESATROOT | TVS_HASBUTTO N S ; } C W z d Tr e e Vi e w : : ~ C W z d Tr e e Vi e w ( ) { } BOOL CWzdTr e e Vi e w : : P r e C r e a t e Window( CREATESTRUCT& cs ) { // TODO: Modify the Window class or styles here by modifying // the CREATESTRUCT cs return CTr e e Vi e w : : P r e C r e a t e Wi n d o w ( c s ) ; } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTr e e View drawing void CWzdTr e e View::OnDraw( CDC* pDC ) { CWzdDoc* pDoc = GetDocument(); A S S E RT _ VA L I D ( p D o c ) ; // TODO: add draw code for native data here } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / 第12章第特定的应用程序第第275下载// CWzdTr e e View printing BOOL CWzdTr e e View::OnPreparePrinting( CPrintInfo* pInfo ) { // default preparation return DoPreparePrinting( pInfo ); } void CWzdTr e e View::OnBeginPrinting( CDC* /*pDC*/, CPrintInfo* /*pInfo*/ ) { // TODO: add extra initialization before printing } void CWzdTr e e View::OnEndPrinting( CDC* /*pDC*/, CPrintInfo* /*pInfo*/ { // TODO: add cleanup after printing } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTr e e View diagnostics #ifdef _DEBUG void CWzdTr e e Vi e w : : A s s e r t Valid() const { C Vi e w : : A s s e r t Va l i d ( ) ; } void CWzdTr e e View::Dump( CDumpContext& dc ) const { C View::Dump( dc ); } CWzdDoc* CWzdTr e e View::GetDocument() // non-debug version is inline { A S S E RT( m_pDocument -> IsKindOf( RUNTIME_CLASS( CWzdDoc ) ) ); return ( CWzdDoc* )m_pDocument; } #endif //_DEBUG / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdTr e e View message handlers void CWzdTr e e Vi e w : : O n I n i t i a l U p d a t e ( ) { C Tr e e Vi e w : : O n I n i t i a l U p d a t e ( ) ; // set icons m_ImageList.Create( IDB_SMALL_BITMAP, 16, 1, RGB( 0,0,0 ) ); G e t TreeCtrl().SetImageList( &m_ImageList, TVSIL_NORMAL ); } 276第第第二部分第用户界面实例 下载void CWzdTr e e View::OnUpdate( CView* pSender, LPARAM lHint, CObject* pHint ) { if ( lHint&LIST_VIEW_ONLY ) { r e t u r n ; } HTREEITEM hitem; CWzdInfo *pInfo; switch ( lHint ) { case TREE_VIEW_SELECT: pInfo = GetDocument() -> GetSelection(); if ( ( hitem = FindTreeItem( GetTr e e C t r l ( ) . G e t C h i l d I t e m ( G e t TreeCtrl().GetRootItem() ), pInfo -> m_nObjectID ) ) != NULL ) { G e t Tr e e C t r l ( ) . S e l e c t I t e m ( h i t e m ) ; } b r e a k ; case TREE_VIEW_UP_LEVEL: hitem = GetTr e e C t r l ( ) . G e t S e l e c t e d I t e m ( ) ; hitem = GetTreeCtrl().GetParentItem( hitem ); G e t Tr e e C t r l ( ) . S e l e c t I t e m ( h i t e m ) ; b r e a k ; d e f a u l t : G e t Tr e e C t r l ( ) . D e l e t e A l l I t e m s ( ) ; A d d B r a n c h ( T V I _ R O O T, GetDocument() -> GetWzdList()); b r e a k ; } } // recurse until all CWzdInfo items are in a leaf void CWzdTr e e View::AddBranch( HTREEITEM hTr e e I t e m , CList *pList ) { for ( POSITION pos = pList -> GetHeadPosition(); pos; ) { CWzdInfo *pInfo = pList -> GetNext( pos ); if ( pInfo -> m_nCategory != CWzdInfo::FILE ) { HTREEITEM hTreeItemx = AddLeaf( hTreeItem, pInfo ); if ( pList -> GetCount() ) { AddBranch( hTreeItemx, &pInfo -> m_list ); } } } } 第12章第特定的应用程序第第277下载// add leaf to tree HTREEITEM CWzdTr e e View::AddLeaf( HTREEITEM hTreeItem, CWzdInfo *pInfo ) { T V _ I N S E RTSTRUCT tvstruct; tvstruct.hParent = hTr e e I t e m ; tvstruct.hInsertAfter = TVI_LAST; tvstruct.item.iImage = tvstruct.item.iSelectedImage = pInfo -> m_nObjectID; t v s t r u c t . i t e m . p s z Text = ( char * )LPCTSTR( pInfo -> m_sName ); tvstruct.item.mask = TVIF_IMAGE |TVIF_SELECTEDIMAGE | TVIF_PARAM | TVIF_TEXT; tvstruct.item.lParam = ( DWORD )pInfo; return GetTreeCtrl().InsertItem( &tvstruct ); } // recurse until item found or items expended HTREEITEM CWzdTr e e Vi e w : : F i n d TreeItem( HTREEITEM hTreeItem, long nObjectID ) { while ( hTreeItem != NULL ) { CWzdInfo *pInfo = ( CWzdInfo * )GetTreeCtrl().GetItemData( hTreeItem ); if ( pInfo -> m_nObjectID == nObjectID ) { return hTr e e I t e m ; } else if ( GetTreeCtrl().ItemHasChildren( hTreeItem ) ) { HTREEITEM hTreeItemx = F i n d TreeItem( GetTreeCtrl().GetChildItem( hTreeItem ), nObjectID ); if ( hTreeItemx ) { return hTr e e I t e m x ; } } e l s e { h TreeItem = GetTreeCtrl().GetNextItem( hTreeItem, TVGN_NEXT ); } } return NULL; } // new selection, change listview! void CWzdTr e e View::OnSelchanged( NMHDR* pNMHDR, LRESULT* pResult ) { NM_TREEVIEW* pNMTr e e View = ( NM_TREEVIEW* )pNMHDR; GetDocument() -> SaveSelection( ( CWzdInfo * )pNMTr e e View -> i t e m N e w. l P a r a m , L I S T _ V I E W _ O N LY ); *pResult = 0; } 278第第第二部分第用户界面实例 下载8. 程序清单— 列表视 // WzdListVi e w.h : interface of the CWzdListView class / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #if !defined WZDLISTVIEW_H #define WZDLISTVIEW_H #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 #include class CWzdListView : public CListVi e w { protected: // create from serialization only C W z d L i s t Vi e w ( ) ; D E C L A R E _ D Y N C R E ATE( CWzdListView ) // Attributes p u b l i c : CWzdDoc* GetDocument(); // Operations p u b l i c : // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CWzdListView ) p u b l i c : virtual void OnDraw( CDC* pDC ); // overridden to draw this view virtual BOOL PreCreateWindow( CREATESTRUCT& cs ); virtual void OnInitialUpdate(); p r o t e c t e d : virtual BOOL OnPreparePrinting( CPrintInfo* pInfo ); virtual void OnBeginPrinting( CDC* pDC, CPrintInfo* pInfo ); virtual void OnEndPrinting( CDC* pDC, CPrintInfo* pInfo ); virtual void OnUpdate( CView* pSender, LPARAM lHint, CObject* pHint ); // }}AFX_VIRT U A L // Implementation p u b l i c : virtual ~CWzdListVi e w ( ) ; #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump( CDumpContext& dc ) const; # e n d i f p r o t e c t e d : // Generated message map functions p r o t e c t e d : 第12章第特定的应用程序第第279下载// {{AFX_MSG( CWzdListView ) afx_msg void OnLButtonDblClk( UINT nFlags, CPoint point ); // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : CImageList m_ImageList; CImageList m_ImageSmallList; void AddLine( CWzdInfo *pInfo ); } ; #ifndef _DEBUG // debug version in WzdListVi e w. c p p inline CWzdDoc* CWzdListVi e w : : G e t D o c u m e n t ( ) { return (CWzdDoc*)m_pDocument; } # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Developer Studio will insert additional declarations immediately // before the previous line. # e n d i f // !defined( AFX_WzdListVi e w _ H _ _ C A 9 0 3 8 F 0 _ B 0 D F _ 11D1_A18C_DCB3C85EBD34__INCLUDED_ ) // WzdListVi e w.cpp : implementation of the CWzdListView class / / #include "stdafx.h" #include "Wzd.h" #include "WzdDoc.h" #include "WzdListVi e w. h " #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdListVi e w I M P L E M E N T _ D Y N C R E ATE( CWzdListVi e w, CListView ) BEGIN_MESSAGE_MAP( CWzdListVi e w, CListView ) // {{AFX_MSG_MAP( CWzdListView ) O N _ W M _ L B U T TO N D B L C L K ( ) // }}AFX_MSG_MAP // Standard printing commands ON_COMMAND( ID_FILE_PRINT, CView::OnFilePrint ) ON_COMMAND( ID_FILE_PRINT_DIRECT, CView::OnFilePrint ) ON_COMMAND( ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview ) 280第第第二部分第用户界面实例 下载E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdListView construction/destruction C W z d L i s t Vi e w : : C W z d L i s t Vi e w ( ) { // TODO: add construction code here } C W z d L i s t Vi e w : : ~ C W z d L i s t Vi e w ( ) { } BOOL CWzdListVi e w : : P r e C r e a t e Wi n d o w ( C R E ATESTRUCT& cs) { // TODO: Modify the Window class or styles here by modifying // the CREATESTRUCT cs return CListVi e w : : P r e C r e a t e Wi n d o w ( c s ) ; } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdListView drawing void CWzdListView::OnDraw( CDC* pDC ) { CWzdDoc* pDoc = GetDocument(); A S S E RT _ VALID( pDoc ); // TODO: add draw code for native data here } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdListView printing BOOL CWzdListView::OnPreparePrinting( CPrintInfo* pInfo ) { // default preparation return DoPreparePrinting( pInfo ); } void CWzdListView::OnBeginPrinting( CDC* /*pDC*/, CPrintInfo* /*pInfo*/ ) { // TODO: add extra initialization before printing } void CWzdListView::OnEndPrinting( CDC* /*pDC*/, CPrintInfo* /*pInfo*/ ) { // TODO: add cleanup after printing 第12章第特定的应用程序第第281下载} / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdListView diagnostics #ifdef _DEBUG void CWzdListVi e w : : A s s e r t Valid() const { C Vi e w : : A s s e r t Va l i d ( ) ; } void CWzdListView::Dump( CDumpContext& dc ) const { C View::Dump( dc ); } CWzdDoc* CWzdListView::GetDocument() // non-debug version is inline { A S S E RT( m_pDocument -> IsKindOf( RUNTIME_CLASS( CWzdDoc ) ) ); return ( CWzdDoc* )m_pDocument; } #endif //_DEBUG / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdListView message handlers void CWzdListVi e w : : O n I n i t i a l U p d a t e ( ) { C L i s t Vi e w : : O n I n i t i a l U p d a t e ( ) ; // attach image lists to list control m_ImageList.Create( IDB_NORMAL_BITMAP, 32, 1, RGB( 0,0,0 ) ); GetListCtrl().SetImageList( &m_ImageList, LVSIL_NORMAL ); m_ImageSmallList.Create( IDB_SMALL_BITMAP, 16, 1, RGB( 0,0,0 ) ); GetListCtrl().SetImageList( &m_ImageSmallList, LVSIL_SMALL ); // initialize list control CClientDC dc( this ); TEXTMETRIC tm; d c . G e t TextMetrics( &tm ); GetListCtrl().ModifyStyle( 0,LV S _ R E P O RT | LV S _ S H O W S E L A LWAYS ); GetListCtrl().SendMessage( LV M _ S E T E X T E N D E D L I S T V I E W S T Y L E , 0 , LVS_EX_FULLROWSELECT ); GetListCtrl().InsertColumn( 0,"Name", LV C F M T _ L E F T, 2 0 * t m . t m Av e C h a r Width,0 ); GetListCtrl().InsertColumn( 1,"Comment", LV C F M T _ L E F T, 3 0 * t m . t m Av e C h a r Width,1 ); } void CWzdListView::OnUpdate( CView* pSender, LPARAM lHint, CObject* pHint ) { 282第第第二部分第用户界面实例 下载if ( lHint&TREE_VIEW_ONLY ) { r e t u r n ; } G e t L i s t C t r l ( ) . D e l e t e A l l I t e m s ( ) ; switch (lHint) { case ICON_LIST_VIEW: G e t L i s t C t r l ( ) . M o d i f y S t y l e ( LV S _ L I S T | LV S _ S M A L L I C O N | LV S _ R E P O RT, LV S _ I C O N ) ; b r e a k ; case LIST_LIST_VIEW: G e t L i s t C t r l ( ) . M o d i f y S t y l e ( LV S _ I C O N | LV S _ S M A L L I C O N | LV S _ R E P O RT, LV S _ L I S T ) ; b r e a k ; case SMALL_ICON_LIST_VIEW: G e t L i s t C t r l ( ) . M o d i f y S t y l e ( LV S _ L I S T | LV S _ I C O N | LV S _ R E P O RT, LV S _ S M A L L I C O N ) ; b r e a k ; case REPORT _ L I S T _ V I E W: G e t L i s t C t r l ( ) . M o d i f y S t y l e ( LV S _ L I S T | LV S _ S M A L L I C O N | LV S _ I C O N , LV S _ R E P O RT ) ; b r e a k ; } // list all folders in selection POSITION pos; CWzdInfo *pSelInfo = GetDocument() -> GetSelection(); for ( pos = pSelInfo -> m_list.GetHeadPosition(); pos; ) { CWzdInfo *pInfo = pSelInfo -> m_list.GetNext( pos ); if ( pInfo -> m_nCategory == CWzdInfo::FOLDER ) { AddLine( pInfo ); } } // list all files in selection for ( pos = pSelInfo -> m_list.GetHeadPosition(); pos; ) { CWzdInfo *pInfo = pSelInfo -> m_list.GetNext( pos ); if ( pInfo -> m_nCategory == CWzdInfo::FILE ) { AddLine( pInfo ); } } 第12章第特定的应用程序第第283下载} void CWzdListView::AddLine( CWzdInfo *pInfo ) { LV_ITEM item; item.mask = LVIF_TEXT | LVIF_IMAGE | LV I F _ PA R A M ; item.iItem = GetListCtrl().GetItemCount(); item.iSubItem = 0; i t e m . p s z Text = ( char * )LPCTSTR( pInfo -> m_sName ); item.iImage = pInfo -> m_nCategory; item.lParam = ( DWORD )pInfo; int ndx = GetListCtrl().InsertItem( &item ); // if report style add comment if ( GetListCtrl().GetStyle() & LV S _ R E P O RT ) { G e t L i s t C t r l ( ) . S e t I t e m Text( ndx, 1, pInfo -> m_sComment ); } } void CWzdListView::OnLButtonDblClk( UINT nFlags, CPoint point ) { int ndx; if ( GetListCtrl().GetSelectedCount() == 1 && ( ndx = GetListCtrl().GetNextItem( -1, LVIS_SELECTED ) ) != -1 ) { GetDocument() -> SaveSelection( ( CWzdInfo * )GetListCtrl().GetItemData( ndx ), TREE_VIEW_SELECT ); } C L i s t View::OnLButtonDblClk( nFlags, point ); } 12.4 实例44:创建简单的ODBC数据库编辑器 1. 目标 创建一个简单的O D B C数据库编辑器,它允许浏览遵循 O D B C格式的整个数据库并编辑其 284第第第二部分第用户界面实例 下载 图12-4 简单ODBC编辑器 数据库记录在表单里显 示并被编辑,该表单用 D i a l o g E d i t o r创建 工具条命令允许滚动数 据库,添加或删除记录记录,如图1 2 - 4所示。 2. 策略 本例中的大多数工作都将由 A p p Wi z a r d、C l a s s Wi z a r d和对话框编辑器来完成。 A p p Wi z a r d 将使用C R e c o r d Vi e w类创建一个S D I应用程序。该视类由C F o r m Vi e w派生而来并为其视使用一 个对话框。然后将使用对话框编辑器为这个视加入字段, C l a s s Wi z a r d将这些字段赋值到数据 库中的列。视类和其他A p p Wi z a r d由添加的命令提供了所有必需的功能以浏览和编辑这些数据 库的字段。但是,还将添加 3个附加的命令用于添加、删除或清空数据库记录。 3. 步骤 1) 用A p p Wi z a r d创建一个O D B C数据库编辑器 使用A p p Wi z a r d创建一个S D I应用程序。在第 2步为应用程序加入 Database view without file support,然后单击Data Source按钮,选择O D B C。使用下拉列表框,在其中选择该应用程 序将要编辑的数据库。如果 A p p Wi z a r d成功地打开这个数据库,它将提醒用户该应用程序将要 编辑数据库中的哪一个表。对此为应用程序选择缺省项即可。 注意 如果选择了Database view with file support,AppWizard将允许访问数据库并在同 一应用程序中串行化文件文档。该选项还允许用户使用“数据库视(database view)”选 项创建MDI应用程序。 A p p Wi z a r d将为应用程序创建两个对话框模板,其中一个用于视 (另一个是普遍存在的 “关于( A b o u t )”对话框模板)。使用对话框编辑器将字段加入到这些将在数据库表中代表列的 模板中。 使用C l a s s Wi z a r d并选择视类。单击Member Va r i a b l e s标签,会发现刚才加入到这个视对话 框中的字段的控件标识号在此被列出。选择第一个控件标识号,然后单击 Add Va r i a b l e按钮, 打开Add Member Va r i a b l e对话框。在该对话框的Member variable name组合框中,单击它的下 拉按钮以显示在这个数据库表中所有列的列表。选择其中的一列便将这个存放记录的列绑定 到该控件标识号所代表的控件字段。对每一个控件字段重复此过程,为它指定存放记录的列。 为使视的大小与对话框大小相匹配,使用 C l a s s Wi z a r d重载视类的O n I n i t i a l U p d a t e ( )函数, 使其如下所示: void CWzdVi e w : : O n I n i t i a l U p d a t e ( ) { m_pSet = &GetDocument() -> m_wzdSet; C R e c o r d Vi e w : : O n I n i t i a l U p d a t e ( ) ; // make frame the size of the original dialog box R e s i z e P a r e n t To F i t ( ) ; // get rid of those pesky scroll bars SetScrollSizes( MM_TEXT, CSize( 20,20 ) ); } 2) 为该编辑器加入一个A d d命令 使用菜单编辑器和工具栏编辑器,为菜单和工具栏添加 A d d命令。 使用C l a s s Wi z a r d,在视类中对这个命令进行如下处理: void CWzdVi e w : : O n R e c o r d A d d ( ) 第12章第特定的应用程序第第285下载{ m_pSet -> AddNew(); UpdateData( FALSE ); } 注意视类中的m _ p S e t成员变量是一个指针,它指向这个编辑器所存取的记录集。 3) 为该编辑器加入一个C l e a r命令 使用菜单编辑器和工具栏编辑器,为菜单和工具栏添加 C l e a r命令。 使用C l a s s Wi z a r d,在视类中对这个命令进行如下处理: void CWzdVi e w : : O n R e c o r d C a n c e l e d i t ( ) { m_pSet -> CancelUpdate(); m_pSet -> Move( 0 ); UpdateData( FALSE ); } 4) 为该编辑器加入一个D e l e t e命令 使用菜单编辑器和工具栏编辑器,为菜单和工具栏添加 D e l e t e命令。 使用C l a s s Wi z a r d,在视类中对这个命令进行如下处理: void CWzdVi e w : : O n R e c o r d D e l e t e ( ) { t r y { m_pSet -> Delete(); } catch( CDBException* e ) { AfxMessageBox( e -> m_strError ); e -> Delete(); } // Move off the record we just deleted m_pSet -> MoveNext(); // If we moved off the end of file, move back to last record if ( m_pSet -> IsEOF() ) m_pSet -> MoveLast(); // If the recordset is now empty, clear the record we just deleted if ( m_pSet -> IsBOF() ) m_pSet -> SetFieldNull( NULL ); UpdateData( FALSE ); } 4. 注意 ■ 为使用编辑器编辑一个字段,首先需要对相应的字段进行修改,然后从该记录进行向 上或向下的滚动。在下一个记录显示之前,上一个记录已经被修改。 C l e a r命令将取消对当前 记录所进行的任何修改,这样它代表的记录便没有改变。 286第第第二部分第用户界面实例 下载■ 使用A p p Wi z a r d和C l a s s Wi z a r d提供的所有自动功能的代价是所得的最终结果在某种程 度上很不灵活。如果不选择 Database View with file support,则将不得不创建一个 S D I应用程 序,并且不得不使用窗体视而不是其他类型的视。如果为数据库支持选择 Header files only则 将失去所有这些自动功能,但应用程序将会变得更加灵活。 5. 使用光盘时注意 当运行随书附送光盘上的工程时,将注意到可以浏览一个 O D B C数据库。 12.5 实例45:创建简单的DAO数据库编辑器 1. 目标 创建一个简单的D A O数据库编辑器,允许浏览整个 D A O数据库并编辑其记录,如实例 4 4 所示。 2. 策略 本例中的策略与上一个实例相同。仅有的不同之处是在处理 A d d、C l e a r和D e l e t e三个编辑 器命令时会有差异。对于D A O数据库,对这三个命令的处理与通常差别很大。 3. 步骤 1) 创建一个D A O数据库编辑器 按上一个实例进行相同的操作以创建应用程序,并在视中加入字段—除了一点不同:定 义的是D A O数据库文件而不是O D B C数据库。 2) 为该编辑器加入一个A d d命令 使用菜单编辑器和工具栏编辑器,为菜单和工具栏添加 A d d命令。 使用C l a s s Wi z a r d,在视类中对这个命令进行如下处理: void CWzdVi e w : : O n R e c o r d A d d ( ) { m_bAddingRecord = TRUE; // keep track of what mode we’re in m_pSet -> AddNew(); UpdateData( FALSE ); } 注意视类中的m _ p S e t成员函数是一个指针,它指向这个编辑器所存取的记录集。 在创建C l e a r命令之前,需要在视类中添加一个重载函数以帮助用户确定是否仍然处于记 录添加模式中。当在 O D B C数据库编辑器中清空一个记录时,可以使用 C a n c e l U p d a t e ( )取消所 进行的修改。但是在添加新记录的中途,对 D A O数据库试图进行同样的操作时, MFC DAO 类将弹出一个错误信息 ( e x c e p t i o n )。因此,在A d d命令中设置一个标志 ( f l a g )以指示当前处于 记录添加模式,然后在视类中的 O n M o v e ( )函数中清除该标志,因为 O n M o v e ( )函数将退出当前 的记录并保存用户所进行的修改。 使用 C l a s s Wi z a r d重载视类的 O n M o v e ( )函数,其中应当清除在 A d d命令中设置的 m _ b A d d i n g R e c o r d标志: BOOL CWzdView::OnMove( UINT nIDMoveCommand ) { m_bAddingRecord = FA L S E ; return CDaoRecordView::OnMove( nIDMoveCommand ); } 第12章第特定的应用程序第第287下载3) 为该编辑器加入一个C l e a r命令 使用菜单编辑器和工具栏编辑器,为菜单和工具栏添加 C l e a r命令。 使用C l a s s Wi z a r d,在视类中对这个命令进行如下处理: void CWzdVi e w : : O n R e c o r d C a n c e l e d i t ( ) { if ( m_bAddingRecord ) m_pSet -> CancelUpdate(); m_pSet -> Move( 0 ); m_bAddingRecord = FA L S E ; UpdateData( FALSE ); } 4) 为该编辑器加入一个D e l e t e命令 使用菜单编辑器和工具栏编辑器,为菜单和工具栏添加 D e l e t e命令。 使用C l a s s Wi z a r d,在视类中对这个命令进行如下处理: void CWzdVi e w : : O n R e c o r d D e l e t e ( ) { t r y { m_pSet -> Delete(); } catch( CDaoException* e ) { AfxMessageBox( e -> m_pErrorInfo -> m_strDescription ); e -> Delete(); } // Move off the record we just deleted m_pSet -> MoveNext(); // If we moved off the end of file, move back to last record if ( m_pSet -> IsEOF() ) m_pSet -> MoveLast(); // If the recordset is now empty, clear the record we just deleted if ( m_pSet -> IsBOF() ) m_pSet -> SetFieldNull( NULL ); UpdateData( FALSE ); } 4. 注意 ■ 为使用编辑器编辑一个字段,首先需要对相应的字段进行修改,然后从该记录进行向 上或向下的滚动。当下一个记录显示之前,上一个记录已经被修改。 C l e a r命令将取消对当前 记录所进行的任何修改,这样它代表的记录便没有改变。 ■ 如果希望从一个非窗体 (nonform) 视应用程序直接访问 D A O数据库,那么在使用 A p p Wi z a r d创建应用程序的时候,应当指定 Header files only。 288第第第二部分第用户界面实例 下载5. 使用光盘时注意 当执行随书附送光盘上的工程时,将注意到可以浏览一个 D A O数据库。 12.6 实例46:创建简单的向导 1. 目标 如图1 2 - 5所示,使用Wi n d o w s中大多数向导的规范创建一个简单的向导。 2. 策略 本例将使用 C P r o p e r t y S h e e t中的向导模 式来创建向导。此外,虽然向导可以是另一 个应用程序的一部分,但是本例将使向导成 为一个独立的应用程序。为此将创建一个对 话框应用程序,然后用属性表来代替有模式 对话框。 3. 步骤 1) 创建工程 使用A p p Wi z a r d来创建一个对话框应用 程序,然后删除由它生成的对话框模板和类文件。 2) 创建向导页面 使用对话框编辑器,在向导中创建页面。除了用户能连续滚动它们以外,向导页面和属 性页面是一样的。用户赋予每个对话框模板什么属性并不重要— C P r o p e r t y类将重载它。每一 个对话框模板的标题将变成在向导中看到的页标题。按照惯例,应当在每个标题后面附加 “第一步”、“第二步”等字样。 使用C l a s s Wi z a r d为每一个对话框页面创建一个属性页类,该类从 C P r o p e r t y P a g e派生。在 每一个类中可以使用 C l a s s Wi z a r d重载四个成员函数: C P r o p e r t y P a g e : : O n s e t A c t i v e ( )、 O n Wi z a r d B a c k ( )、O n Wi z a r d N e x t ( )和O n Wi z a r d F i n i s h ( )。 使用C l a s s Wi z a r d重载每一个属性页的 O n S e t A c t i v e ( )成员函数。在其中设置哪一个向导按 钮对于该页是可见的或是可用的,如下所示: BOOL CPage1::OnSetActive() { ( ( CPropertySheet* )GetOwner() ) -> SetWi z a r d B u t t o n s ( PSWIZB_NEXT | // the “Next” button is visible PSWIZB_BACK | // the “Back” button is visible PSWIZB_FINISH | // the “Finish” button is visible PSWIZB_DISABLEDFINISH ); // disabled “Finish” button // is visible return CPropertyPage::OnSetActive(); } B a c k、N e x t和F i n i s h按钮由C P r o p e r t y S h e e t类自动处理。如果需要进行控制,则使用 C la s s Wi z a r d重载O n Wi z a r d B a c k ( )、O n Wi z a r d N e x t ( )和O n Wi z a r d F i n i s h ( )函数。在每一个函数中 可以调用U p d a t e D a t a ( T R U E )函数来检索该页的任何变化。 O n Wi z a r d B a c k ( )或O n WizardNext () 第12章第特定的应用程序第第289下载 图12-5 简单的向导函数的返回值决定了向导下一次将显示哪一页。返回缺省值为零则将显示连续的上一页 或下一页。然而如果返回的是向导中其他属性页的对话框模板的标识号,向导接下来将 显示该页。 通过重载O n Wi z a r d F i n i s h ( )而后选择是否调用基类的 CPropertyPage :: OnWi z a r d F i n i s h ( )函 数可以有条件地终止向导。若不调用基类函数将允许向导继续进行。 在下面的例子中,如果用户单击 Page 3 Next复选框,则接下来将显示I D D _ PA G E 3页: L R E S U LT CPage1::OnWi z a r d N e x t ( ) { UpdateData( TRUE ); if ( m_bPage3Check ) { return IDD_PA G E 3 ; } e l s e { return CPropertyPage::OnWi z a r d N e x t ( ) ; } } 注意 CPropertyPage::OnWizardNext()和CPropertyPage::OnWizardBack()函数都只返回零 (0),这将使向导分别转向上一页或下一页。 3) 创建向导 在应用程序类中嵌入向导的属性页类: CPageOne m_Page1; C P a g e Two m_Page2; : : : 在应用程序类的 I n i t I n s t a n c e ( )函数中,在堆栈上创建 C P r o p e r t y S h e e t类的一个示例,然后 按照希望显示给用户的次序,依次将属性页加入其中。此时还可以初始化属性页类的成员变 量,然后设置一个全能的向导风格并调用 C P r o p e r t y S h e e t : : D o M o d a l ( )函数: CWzdSheet wzd( " " ); m_pMainWnd = &wzd; AddPage( &m_Page1 ); AddPage( &m_Page2 ); AddPage( &m_Page3 ); S e t Wi z a r d M o d e ( ) ; if ( wzd.DoModal() == ID_WIZFINISH ) { // process results } 如果D o M o d a l ( )函数返回I D _ W I Z F I N I S H值,既可以在在最后一页的 O n Wi z a r d F i n i s h ( )函 数中处理向导结果,也可以直接在应用程序类的 D o M o d a l ( )函数中处理向导的结果,后者可以 很容易地访问所有页的成员变量,然而此时向导已关闭且不能返回。 4. 注意 ■ 属性表的向导模式目前除在标题栏中显示标题之外,不能显示其他任何内容,换句话 290第第第二部分第用户界面实例 下载说,就是没有关闭按钮和最小化按钮。如果已经在标题栏中加入了这些东西,请参照实例 2 7 将向导嵌入到对话框应用程序的模板中。为去掉向导的标题栏,还可能需要从 C P o r p e r t y S h e e t 中派生一个类并重载W M _ N C PA I N T消息以阻止它到达属性表。 ■ 另外一种为向导的标题栏加入关闭按钮的方法是自己绘制一个关闭按钮,这可以参考 实 例 2 9 。 但 此 处 不 必 按 照 该 例 所 介 绍 的 方 式 来 创 建 自 己 的 按 钮 位 图 , 而 是 使 用 C D C : : D r a w F r a m e C o n t r o l ( )函数并指定合适的位图类型和状态参数来绘制。 ■ 为什么属性表的向导模式不能在它的标题栏中包含任何控件?毕竟 A p p Wi z a r d的标题 栏中具有一个关闭按钮和一个上下文帮助按钮。说出来原因有点令人吃惊, A p p Wi z a r d不能使 用属性表的向导模式。事实上 A p p Wi z a r d根本不使用属性表。A p p Wi z a r d实际上是一个具有单 个主对话框模板的对话框应用程序。因此,它具有所有对话框应用程序都自动具备的标题按 钮。它是如何生成一步一步地换页这样的错觉的呢?请参考实例 2 6。 ■ Developer Studio中有一个向导,它可创建自己定制的 A p p Wi z a r d。在M F C文档中可找 到十分详细的例子。 5. 使用光盘时注意 当执行随书附送光盘上的工程时,将注意到该程序的表现与一个标准的向导类似。 6. 程序清单— 属性页类 #if !defined( AFX_PA G E 1 _ H _ _ A 8 4 6 F 4 D 3 _ E C A 0 _ 11D1_A18D_DCB3C85EBD34__INCLUDED_ ) #define AFX_PA G E 1 _ H _ _ A 8 4 6 F 4 D 3 _ E C A 0 _ 11 D 1 _ A 1 8 D _ D C B 3 C 8 5 E B D 3 4 _ _ I N C L U D E D _ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 // Page1.h : header file / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CPage1 dialog class CPage1 : public CPropertyPage { D E C L A R E _ D Y N C R E ATE( CPage1 ) // Construction p u b l i c : C P a g e 1 ( ) ; ~ C P a g e 1 ( ) ; // Dialog Data // {{AFX_DATA( CPage1 ) enum { IDD = IDD_PAGE1 }; BOOL m_bPage3Check; // }}AFX_DATA // Overrides 第12章第特定的应用程序第第291下载// ClassWizard generate virtual function overrides // {{AFX_VIRTUAL( CPage1 ) p u b l i c : virtual LRESULT OnWi z a r d B a c k ( ) ; virtual LRESULT OnWi z a r d N e x t ( ) ; virtual BOOL OnWi z a r d F i n i s h ( ) ; virtual BOOL OnSetActive(); p r o t e c t e d : virtual void DoDataExchange( CDataExchange* pDX ); // DDX/DDV support // }}AFX_VIRT U A L // Implementation p r o t e c t e d : // Generated message map functions // {{AFX_MSG( CPage1 ) virtual BOOL OnInitDialog(); // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : CWzdSheet *m_pSheet; } ; // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Developer Studio will insert additional declarations immediately // before the previous line. # e n d i f // !defined( AFX_PA G E 1 _ H _ _ A 8 4 6 F 4 D 3 _ E C A 0 _ 11D1_A18D_DCB3C85EBD34__INCLUDED_ ) // Page1.cpp : implementation file / / #include "stdafx.h" #include "wzd.h" #include "WzdSheet.h" #include "Page1.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CPage1 property page I M P L E M E N T _ D Y N C R E ATE( CPage1, CPropertyPage ) CPage1::CPage1() : CPropertyPage( CPage1::IDD ) { // {{AFX_DATA_INIT( CPage1 ) m_bPage3Check = FA L S E ; 292第第第二部分第用户界面实例 下载// }}AFX_DATA _ I N I T } C P a g e 1 : : ~ C P a g e 1 ( ) { } void CPage1::DoDataExchange( CDataExchange* pDX ) { CPropertyPage::DoDataExchange( pDX ); // {{AFX_DATA_MAP( CPage1 ) DDX_Check( pDX, IDC_PAGE3_CHECK, m_bPage3Check ); // }}AFX_DATA _ M A P } BEGIN_MESSAGE_MAP( CPage1, CPropertyPage ) // {{AFX_MSG_MAP( CPage1 ) // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CPage1 message handlers BOOL CPage1::OnInitDialog() { C P r o p e r t y P a g e : : O n I n i t D i a l o g ( ) ; m_pSheet = ( CWzdSheet* )GetOwner(); return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FA L S E } BOOL CPage1::OnSetActive() { m_pSheet -> SetWizardButtons( PSWIZB_NEXT ); // PSWIZB_BACK,PSWIZB_FINISH,PSWIZB_DISABLEDFINISH return CPropertyPage::OnSetActive(); } L R E S U LT CPage1::OnWi z a r d B a c k ( ) { return CPropertyPage::OnWi z a r d B a c k ( ) ; } L R E S U LT CPage1::OnWi z a r d N e x t ( ) { UpdateData( TRUE ); if ( m_bPage3Check ) 第12章第特定的应用程序第第293下载{ return IDD_PA G E 3 ; } e l s e { return CPropertyPage::OnWi z a r d N e x t ( ) ; } } BOOL CPage1::OnWi z a r d F i n i s h ( ) { return CPropertyPage::OnWi z a r d F i n i s h ( ) ; } 294第第第二部分第用户界面实例 下载下载下载 应用程序并不仅仅等于用户界面。从读取、写入文件到定时事件、多任务,有大量的处 理工作都在后台进行着。虽然 M F C被人们熟知为界面开发系统,但是在 M F C类库中同样具备 一些类,它们可提供对应用程序中的非界面部分的支持。 这部分中的例子涉及大量应用程序内部的处理工作。这些例子包括程序内的多任务和发 送、接收信息,还包括演奏声音和制作定时器。 第13章 消息和通信 这一章中的实例涉及应用程序内部和外部发送数据和消息。通常有两种方法处理消息队 列中的消息。这一章中还给出了除使用窗口消息之外的其他三种与外部世界通信的方式,包 括Windows socket(套接字)和低级串行I / O。 第14章 多任务 这一章中的实例涉及应用程序的处理过程,包括多任务、后台处理和其他应用程序的执 行。 第15章 其他 最后的章节中收集了内部过程处理的一些其他实例。其中将介绍定时器、二进制字符串 和V C + +的宏指令等内容。 第13章 消息和通信 应用程序之间除了发送窗口消息外,还有一些与外部环境进行通信的其他方式,包括使 用串行I / O方式以及在网络上使用套接字方式通信等等。对以上主题的深入讨论在第 1章和第3 章中已涉及,而在本章中将给出这三种通信方式的实际应用的例子,这些例子包括: 实例47 等待消息,在本例中将看到如何使用消息处理函数在另一个消息到来之前暂停 应用程序的运行。 实例48 清除消息,在本例中将看到如何清空应用程序的消息队列,以便使后续消息成 为最新消息。 实例49 向其他应用程序发送消息,本例中将创建一条唯一的消息并把它发送给系统中 的其他应用程序。 实例50 与其他应用程序共享数据,在本例中将看到如何向其他应用程序发送大量的数据。 第三部分 内部处理实例实例51 使用套接字与应用程序通信,本例中将使用 Wi n d o w套接字和其他Wi n d o w s应用 程序或任何支持套接字的应用程序 (例如 UNIX 应用程序)进行通信。 实例52 使用串行I / O,本例中将实现M F C应用程序与串行设备之间的通信。 13.1 实例47:等待消息 1. 目标 在一个函数中暂停应用程序,直到用户单击鼠标或者按键为止。 2. 策略 当应用程序看起来是空闲时,实际上它在运行函数: C Wi n A p p : : R u n ( )。该函数不仅具有 查询新消息的逻辑功能,并且完成了大量包括更新用户界面状态和清除临时内存对象在内的 应用程序维护工作。因此,如果需要在应用程序中的某个地方停止运行并等待消息,也需要 进行这种应用程序维护工作。由于微软提供了 C Wi n A p p ( ) : : R u n ( )函数的源代码,因此本例将 创建该函数的一个新版本,可用于应用程序中的任何地方。 为什么要在除C Wi n A p p函数以外的其他地方停顿呢?请参阅本实例结尾有关移植的说明, 在此可发现该实例可能的用武之地。 3. 步骤 等待w i n d o w s消息 找到需要应用程序暂停并等待特定消息的位置,并添加以下代码: // wait till user clicks on status bar before proceeding MSG msg; BOOL bIdle = TRUE; LONG lIdleCount = 0; C WinApp* pApp = AfxGetApp(); AfxMessageBox( "Into wait loop." ); m _ b Wait = TRUE; while ( m_bWait ) { // idle loop waiting for messages while ( bIdle && !::PeekMessage( &msg, NULL, NULL, NULL, PM_NOREMOVE) ) { if ( !pApp -> OnIdle( lIdleCount++ ) ) bIdle = FA L S E ; } // process new messages do { // pump messages pApp -> PumpMessage(); // if we’re done, let’s go... if ( !m_bWait ) b r e a k ; // otherwise keep looping if ( pApp -> IsIdleMessage( &msg ) ) 296第第第三部分第内部处理实例 下载{ bIdle = TRUE; lIdleCount = 0; } } while ( ::PeekMessage( &msg, NULL, NULL, NULL, PM_NOREMOVE ) ); } 以上例程将在此等待处理消息和进行应用程序维护,直到 m _ b Wa i t变为T R U E为止。(如 W M _ L B U T TO N D O W N消息)。 使用C l a s s Wi z a r d给正在等待的消息添加消息处理函数。在其中需设置 m _ b Wa i t为T R U E。 4. 注意 ■ 该等待例程不仅将消息转发到它们要发送的窗口,还进行后台处理工作。如果省略后 台处理,会发现此时仍然可以改变应用程序窗口的尺寸并移动它们,但是这些窗口无法完全 重新绘制,而且还在屏幕上留下无用的信息。同时工具栏和状态栏也将停止更新其状态,这 是因为以上这些工作都是在后台处理时进行的。 ■ 除了等待 m _ b Wa i t变成 T U R E以外,还可以在等待过程中检查通过消息泵 ( m e s s a g e p u m p )的消息。要注意的是:在此只能截取已寄送的 ( p o s t e d )消息。使用SendMessage ()发送的 消息将直接到达指定的窗口。而已寄送的消息通常只包括鼠标和键盘的行为消息— 其他任 何消息都是使用发送方式。 (如果希望截取由 S e n g M e s s a g e ( )发送的消息,可试一试使用 S e t Wi n d o w s H o o k E x ( )及W H _ C A L LW N D P R O C。) ■ 有关消息和消息泵的更多内容,请参阅第 1章。 5. 使用光盘时注意 在随书附带光盘上运行工程时,首先单击 Te s t,然后单击W z d 1菜单命令。注意应用程序 将一直在O n W z d 1 Te s t ( )函数处暂停,直到单击W z d 2菜单命令才会退出该函数并继续运行。 6. 有关移植事项 该实例理想地实现了从 DOS 应用程序到Wi n d o w s的移植。Windows 应用程序是事件驱动 的。函数仅在鼠标或者键盘按下时调用,当这些操作结束时,函数就释放控制权直到下一个 事件发生。而 D O S应用程序则占用诸如键盘和鼠标的系统资源,直到它们的功能完成后才释 放这些资源。 D O S应用程序由用户启动之后,一个的典型功能是询问一系列问题,每一次都产生停顿 并等待回答,在用户做出响应之前应用程序一直控制系统。为将这种功能移植到 Wi n d o w s中, 可以使用本例中的代码,允许系统在用户回答以前继续处理消息。然后在其他的消息处理函 数中处理用户的回答,并将 m _ b Wa i t设置为Ture ,这样可以由应用程序继续下一次询问或者 终止该函数。 13.2 实例48:清除消息 1. 目标 在继续运行之前,清除特定窗口中所有的鼠标单击和键盘敲击消息队列。 2. 策略 基于上一个实例编写一个小型消息泵 ,它可以处理当前应用程序消息队列中所有的消息 第13章第消息和通信第第297下载— 除了被选定窗口的消息之外,该窗口的消息将被删除。 3. 步骤 为某一特定窗口在需要清除消息队列的地方,添加以下代码: MSG msg; C WinApp* pApp = AfxGetApp(); while ( ::PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) ) { // kill any mouse messages for this window if ( ( msg.hwnd != m_hWnd || ( msg.message < WM_MOUSEFIRST || msg.message > WM_MOUSELAST ) ) && !pApp -> P r e TranslateMessage( &msg ) ) { : : TranslateMessage( &msg ); ::DispatchMessage( &msg ); } } 在此将清除所有的鼠标消息。为了清除所有的键盘消息,还要对 WM_KEYFIRST 和 W M _ K E Y L A S T进行一次范围检查(range check)。为了清除已寄送到某一个窗口的消息,注意 不要进行任意范围的检查— 只需检查这条消息是否到该窗口即可。为了清除已寄送到所有 窗口的消息,只需删除w h i l e语句中的内容即可。 4. 注意 ■ 当使用弹出菜单时,该实例特别有用。由于当用户在它的范围之外单击鼠标时弹出菜 单将立即关闭,剩余的鼠标单击动作可能导致在弹出菜单尚未出现之前就关闭了弹出菜单。 为了消除错误的单击,可在打开弹出菜单前添加本例中的这段程序。 ■ 参阅第1章可以了解关于M F C消息发送更多的知识。 5. 使用光盘时注意 当运行随书附送的光盘时,注意在从视中弹出菜单之前,有关该视的鼠标消息从 O n TestWzd ()的消息队列中被删除。 13.3 实例49:向其他应用程序发送消息 1. 目标 向其他应用程序发送消息。 2. 策略 本例将使用Windows API::SendMessage()函数向目标应用程序的某个窗口的句柄发送消 息。其中的技巧在于获取该窗口的句柄。同时使用 Windows API ::RegisterWi n d o w M e s s a g e ( ) 函数创建一个唯一的消息,并且两个应用程序相互都了解这条消息的含义。 本例中还将看到另一个 Windows API函数:: : B r o d c a s t S y s t e m M e s s a g e ( )函数,它可以向系 统中的每个应用程序的主窗口发送消息。这样便可以避免出现获取另一个应用程序窗口句柄 的问题。 3. 步骤 首先注册自己的窗口消息。相比较于前一个例子中使用的 W M _ U S E R + 1的技术,注册窗 口消息的好处是不必费心考虑 W M _ U S E R加上某个数之后,所表示的消息标识符是否超出工 298第第第三部分第内部处理实例 下载程的允许范围。本例在两个工程中都使用文本字符串来注册消息。由于这个文本字符串在整 个系统中应当是唯一的,因此将使用一种称为 G U I D的C O M技术来命名消息。 G U I D名字生成 器程序可以在M F C的\ B I N目录下找到,其可执行文件名为 G U I D G E N . E X E。该程序将生成在 应用程序已知范围内认为是唯一的文本字符串,这对应用程序来说当然是最好不过的。 1) 注册一个唯一的窗口消息 使用G U I D G E N . E X E生成一个G U I D。 在应用程序中把G U I D定义为窗口消息文本字符串: #define HELLO_MSG “{6047CCB1-E4E7-11 d 1 - 9 B 7 E - 0 0 A A 0 0 3 D 8 6 9 5 } ” 使用: : R e g i s t e r Wi n d o w s M e s s a g e ( )注册该窗口消息文本字符串: idHelloMsg = ::RegisterWindowMessage( HELLO_MSG ); 保存消息标识符i d H e l l o M s g,便于以后使用。 2) 向其他应用程序发送消息 使用: : R e g i s t e r Wi n d o w s M e s s a g e ( )返回的消息标识符发送消息,可使用以下代码: : : S e n d M e s s a g e ( h W n d , // handle of a window belonging to destination app i d H e l l o M s g , // registered message id w P a r a m , // as usual l P a r a m // as usual ) ; 以上代码假定事先可以通过某种方式获取目标应用程序的某个窗口的句柄。一个指向 C W n d类的指针不能在程序范围之外而发挥作用。但是可以在 C W n d类中封装已获取的窗口句 柄,并如下所示来发送消息: CWnd wnd; wnd.Attach( hWnd ); wnd.SendMessage( idHelloMsg,wParam,lParam ); 3) 接收已注册的窗口消息 为接收已注册的窗口消息,需要在接收窗口类,一般为 C M a i n F r a m e中手工添加 O N _ R E G I S T E R E D _ M E S S A G E消息宏到消息映射中: BEGIN_MESSAGE_MAP( CMainFrame, CMDIFrameWnd ) // {{AFX_MSG_MAP( CMainFrame ) // }}AFX_MSG_MAP ON_REGISTERED_MESSAGE( idHelloMsg,OnHelloMsg ) E N D _ M E S S A G E _ M A P ( ) 有关已注册消息的消息处理函数的代码如下: L R E S U LT CMainFrame::OnHelloMsg( WPARAM wParam,LPARAM lParam ) { // process message return 0; } 该实例到目前为止,一直假定事先可以通过某种方式取得目标应用程序的某个窗口的句 柄。但这是一个困难的任务。简单的方法是向每个应用程序广播一条消息,并且希望目标程 序正在监听。由于在系统中注册了一条唯一的消息,因此只有目标程序会响应这条消息。应 第13章第消息和通信第第299下载用程序广播的消息可能是它自己的窗口句柄,于是接收程序可以使用 : : S e n d M e s s a g e ( )来发送 应答,也可能是用窗口句柄来结束循环。 4) 广播窗口消息 使用下面的代码广播窗口消息: W PARAM wParam = xxx; // your definition L PARAM lParam = xxx; // your definition DWORD dwRecipients = BSM_APPLICAT I O N S ; ::BroadcastSystemMessage( BSF_IGNORECURRENTTA S K , & d w R e c i p i e n t s , i d H e l l o M s g , // registered window message wParam,lParam ); // user defined parameters 4. 注意 ■ : : B r o a d c a s t S y s t e m M e s s a g e ( )函数提供了附加的标志 B S F _ L PA R A M P O I N T E R,可以将 写入参数l P a r a m的指针转化为可以被目标程序用来访问程序空间的指针,但是这个标志可能 尚未进行文档标准化。 ■ 进程间消息最广泛的使用是在外壳程序的子进程之间。一般情况下,外壳程序使用唯 一的名字创建普通且不可见的窗口,子进程将通过这个窗口通信。此时子进程可以使用 C W n d : : F i n d Wi n d o w ( )获得使用该唯一窗口名的通信窗口的句柄,然后子窗口可以使用自己的 窗口句柄向外壳程序发送消息 I’m here,如此完成连接。创建自己的普通窗口请参考实例 3 8。 5. 使用光盘时注意 运行两个工程中的实例,其中的一个处于调试模式,另一个处于发布模式。在一个应用 程序中单击Te s t和W z d菜单命令,注意到它与另一个应用程序交换信息。 13.4 实例50:与其他应用程序共享数据 1. 目标 与另一个应用程序共享数据。 2. 策略 使用::CreateFileMapping() Windows API建立一段交换文件(swap file),与其他应用程序共 享。 3. 步骤 1) 创建共享内存 为共享内存打开一段交换文件,可使用以下代码: m_hMap = ::CreateFileMapping( ( HANDLE )0xffffffff , // or can be an open file handle 0 , // security PA G E _ R E A D W R I T E , // or PA G E _ R E A D O N LY or PA G E _ W R I T E C O P Y 0 , // size - - high order // (required if no file handle) 0 x 1 0 0 0 , // size - - low order // (required if no file handle) M A P _ I D // unique id - - required if no file handle ) ; 如果希望通过自己的文件而不是交换文件来共享数据,可以提供一个已打开的文件句柄, 300第第第三部分第内部处理实例 下载该文件句柄使用与在此使用的文件属性 (只读或者可写,等等)相同的文件属性。为创建唯一的 M A P _ I D,可使用V C + +的\ B I N目录中的G U I D G E N . E X E应用程序。 注意 使用MAP_ID第一次调用::CreateFileMapping() 实际上将生成一个映射文件。所有 以后使用相同MAP_ID的调用将只返回现有文件的句柄。 使用以下的代码获得映射文件的内存指针: m_pSharedData = ::MapVi e w O f F i l e ( m _ h M a p , F I L E _ M A P _ W R I T E , // or FILE_MAP_READ, FILE_MAP_COPY // (FILE_MAP_WRITE is read/write) 0 , // offset - - high order 0 , // offset - - low order 0 // number of bytes (zero maps entire file) ) ; 2) 使用共享内存 现在可使用: : M a p Vi e w O f F i l e ( )返回的指针进行数据存取: // writing to shared memory memcpy( ( LPBYTE )m_pSharedData,pWrite,10 ); // reading from shared memory memcpy( pRead, ( LPBYTE )m_pSharedData,10 ); 如果希望使用文件函数来访问数据,可以使用 C M w m F i l e类封装该内存指针,如下所 示: CMemFile file; file.Attach( ( LPBYTE )m_pSharedData,size ); f i l e . Write( pBuff e r,100 ); // write 100 bytes to shared memory 3) 关闭共享内存 关闭共享内存时,首先取消视与它之间的映射关系,然后关闭句柄,如下所示。 : : U n m a p ViewOfFile( m_pSharedData ); ::CloseHandle( m_hMap ); 4. 注意 ■ 系统将同步所有对: : C r e a t e F i l e M a p p i n g ( )创建的共享内存的访问,这意味着不必担心应 用程序A和应用程序B同时向相同的共享内存写入数据。但是当应用程序 A和应用程序B通过 网络共享数据时,便可能发生这种问题。这是因为数据在传送到共享内存之前首先被写入本 地缓存。在这种情况下,需要提供自己的文件同步。 ■ 当在: : M a p Vi e w O f F i l e ( )中使用非零的文件偏移量时,必须是系统内存分配单位值的倍 数。为确定该单位值,可使用 : : G e t S y s t e m I n f o ( )取得S Y S T E M _ I N F O的结构。该单位值存放于 结构的d w A l l o c a t i o n G r a n u l a r i t y成员中。过去该值一般固定设置为 6 4 K,将来可能会改变。 13.5 实例51:使用套接字与任意的应用程序通信 1. 目标 使用套接字与另一个 Wi n d o w s应用程序或者与任何支持套接字的应用程序通信,例如与 第13章第消息和通信第第301下载U N I X应用程序通信。 2. 策略 本例将使用M F C的C S o c k e t类在两个或者多个应用程序之间建立通信。通过套接字,服务 器应用程序创建一个特殊的套接字,用于监听客户应用程序的连接请求,然后服务器应用程 序创建新的套接字来完成连接。从客户和服务器两端读取该连接,直到一个需要处理的报文 到来为止。请参阅第3章了解Wi n d o w s套接字的详细内容。 本例将封装这些功能,这样所有应用程序需要完成的只是创建一个套接字连接,然后处 理到来的报文。这将包括一个新的服务器 s o c k e t类、新客户端s o c k e t类和新的报文队列类。报 文队列类允许使用同步方式向报文队列 (通常为C O b l i s t )中添加消息,这样在一个多任务环境 中的两个消息不会产生冲突。之所以需要采取这种保护措施,是因为每个套接字将在它自己 的线程中进行同步读操作: 3. 步骤 1) 创建新的服务器套接字类 使用C l a s s Wi z a r d创建一个从C S o c k e t派生的新类。可以在新类名的某个位置加上“服务器 ( S e r v e r )”这个词。 首先在该类中创建一个名为 O p e n ( )的新函数,它将使用 C S o c k e t的函数C r e a t ( )来创建套接 字。本例将使用该函数而不是使用 C S o c k e t : : C r e a t ( )直接完成打开套接字 (类似于打开一个端口 或文件)的操作: BOOL CWzdServer::Open( UINT nPort ) { return Create( nPort ); } 现在目标是编写一组函数,用户监听意图连接的客户应用程序,并且通过创建一个新的 套接字来建立连接。由于一些客户应用程序可以建立连接,因此需要在对象的映射表中跟踪 这些新的套接字对象。一旦套接字建立,立刻开始从中读取内容。 在本例中,完成上述功能需要三个函数和一个结构。第一个函数 L i s t e n E x ( ) ,用于通知套接 字开始监听客户应用程序。第二个函数 O n A c c e p t ( )在接收到连接请求时被调用。在其中创建 新的套接字,并立刻设置它开始从客户应用程序读取报文。这些是通过调用第三个函数 R e c v T h r e a d ( )来完成的,该函数位于它自己的线程中。 下面首先编写函数L i s t e n E x ( )。 添加L i s t e n E x ( )函数,它通过调用 C S o c k e t的L i s t e n ( )函数监听来自客户应用程序的连接请 求。L i s t e n E x ( )同时在结构中设置其调用参数,这些参数最终被传递到 R e c v T h r e a d ( )函数以实 现读操作: void CWzdServer::ListenEx( int hdrSz, int bodyPos, CWzdQueue *pQueue, CWnd *pWnd, UINT id ) { // initialize receive data m_RecvData.hdrSz = hdrSz; m_RecvData.bodyPos = bodyPos; m_RecvData.pQueue = pQueue; m_RecvData.pWnd = pWnd; m_id = id; // starting id 302第第第三部分第内部处理实例 下载// start listening L i s t e n ( ) ; } 使用文本编辑器( Text Editor)重载C S o c k e t的O n A c c e p t ( )函数。在其中将创建新的套接字, 以此来建立与客户应用程序的连接,同时使用由用户定义的标识符作为关键字将该套接字保 存到对象映射表中。然后,设置套接字进入同步模式,并创建一个线程从套接字中读取数据 : void CWzdServer::OnAccept ( int nErrorCode ) { if ( nErrorCode == 0 ) { // create a new socket and add to map CSocket *pSocket = new CSocket; m_mapSockets[m_id] = pSocket; // use this new socket to connect to client Accept( ( CAsyncSocket& )*pSocket ); // put socket into synchronous mode DWORD arg = 0; pSocket -> AsyncSelect( 0 ); pSocket -> IOCtl( FIONBIO, &arg ); // setup this socket to listen for client messages m_RecvData.pSocket = pSocket; m_RecvData.id = m_id++; // start the thread AfxBeginThread( RecvThread,&m_RecvData ); } } 最后添加线程例程。 R e c v T h r e a d ( )使用C S o c k e t的R e c e i v e ( )函数等待,直到通过套接字接 收到新的报文。该线程假定每一个报文包含固定字长的报头和可变长度的报文体。对于每一 个新的套接字报文, R e c v T h r e a d ( )还向应用程序发送 W M _ N E W _ M E S S A G E消息,通知新的 报文等待处理。如果套接字关闭,线程将在终止前向应用程序发送 W M _ D O N E _ M E S S A G E消 息: UINT RecvThread( LPVOID pParam ) { // get data from thread creator R E C V D ATA *pRecv = ( RECVDATA * )pParam; int len = 1; int error = 0; char *pBody = NULL; char *pHdr = NULL; // while both sockets are open while (TRUE) { // read the header 第13章第消息和通信第第303下载int res; pBody = NULL; pHdr = new char[pRecv -> hdrSz]; if ( ( res = pRecv -> pSocket -> CAsyncSocket::Receive( pHdr, pRecv -> hdrSz ) ) == SOCKET_ERROR ) error = ::GetLastError(); e l s e len = res; // if closing down, exit thread if ( len == 0 || error == WSAECONNRESET || error == WSAECONNABORTED ) break; // read the body??? if ( !error && len && pRecv -> bodyPos != -1 ) { int bodyLen = *( ( short * )pHdr+pRecv -> bodyPos ); pBody = new char[bodyLen]; if ( ( res = pRecv -> pSocket -> CAsyncSocket::Receive( pBody, bodyLen ) ) == SOCKET_ERROR ) error = ::GetLastError(); e l s e len+ = res; // if closing down, exit thread if ( len == 0 || error == WSAECONNRESET || error == WSAECONNABORTED ) break; } // put message in queue pRecv -> pQueue -> Add( new CWzdMsg( pRecv -> id,pHdr, p B o d y,len,error ) ); // post message to window to process this new message pRecv -> pWnd -> PostMessage( WM_NEW_MESSAGE ); } // cleanup anything we started delete []pHdr; delete []pBody; // tell somebody we stopped pRecv -> pWnd -> SendMessage( WM_DONE_MESSAGE, ( WPARAM )pRecv -> id,( LPARAM )error ); return 0; } 接下来添加函数 S e n d E x ( )向客户应用程序发回报文,该函数将根据用户定义的标识符从 304第第第三部分第内部处理实例 下载对象映射表中取出套接字对象,然后调用线程函数向该套接字发送报文: void CWzdServer::SendEx( int id, LPSTR lpBuf, int len ) { // locate the socket for this id CSocket *pSocket = m_mapSockets[id]; if ( pSocket ) { m_SendData.pSocket = pSocket; m_SendData.lpBuf = lpBuf; m_SendData.len = len; // start the thread AfxBeginThread( SendThread,&m_SendData ); } } S e n d T h r e a d使用C S o c k e t类的S e n d ( )函数将报文数据发送出去: UINT SendThread( LPVOID pParam ) { // get data from thread creator S E N D D ATA *pSend = ( SENDDATA * )pParam; // do the write pSend -> pSocket -> Send( pSend -> lpBuf, pSend -> len ); return 0; } 该类中的最后需要创建关闭函数,这个函数不仅将关闭监听套接字,而且将关闭创建的 所有与客户端连接的套接字: void CWzdServer::CloseEx() { int id; CSocket *pSocket; for ( POSITION pos = m_mapSockets.GetStartPosition(); pos; ) { m_mapSockets.GetNextAssoc( pos,id,pSocket ); pSocket -> Close(); } C l o s e ( ) ; } 2) 创建客户套接字类 本例实际上将把该类添加到刚才创建服务器套接字类时所使用的同一个 . c p p和. h头文件 中。因为在前面创建的两个线程函数 S e n d T h r e a d ( )和R e c v T h r e a d ( )在客户套接字类中可以重 用。 首先添加O p e n ( )函数。这个函数使用 C S o c k e t类的O p e n ( )和C o n n e c t ( )函数创建套接字并与 服务器应用程序连接: 第13章第消息和通信第第305下载BOOL CWzdClient::Open( LPCTSTR lpszHostAddress, UINT nHostPort ) { if ( Create() && Connect( lpszHostAddress, nHostPort ) ) { // put socket into synchronous mode DWORD arg = 0; AsyncSelect( 0 ); IOCtl( FIONBIO, &arg ); return TRUE; } return FA L S E ; } 接下来添加一个发送函数。由于只有一个服务器,所以该发送函数不需要标识符。向发 送数据结构中加入相应的数据,并调用 S e n d T h r e a d ( )函数完成实际的发送任务: void CWzdClient::SendEx( LPSTR lpBuf, int len ) { // initialize the structure we will pass to thread m_SendData.pSocket = this; m_SendData.lpBuf = lpBuf; m_SendData.len = len; // start the thread AfxBeginThread( SendThread,&m_SendData ); } 注意 SendThread()函数与前面服务器套接字中使用的函数是同一个函数。 添加L i s t e n E x ( )函数。这个函数将使客户套接字开始监听报文。与服务器套接字不同的是 它不监听连接。L i s t e n E x ( )实际上将调用R e c v T h r e a d ( )来完成实际的监听任务: void CWzdClient::ListenEx( int hdrSz, int bodyPos, CWzdQueue *pQueue, CWnd *pWnd, UINT id ) { // initialize receive data m_RecvData.pSocket = this; m_RecvData.hdrSz = hdrSz; m_RecvData.bodyPos = bodyPos; m_RecvData.pQueue = pQueue; m_RecvData.pWnd = pWnd; m_RecvData.id = id; // start the thread AfxBeginThread( RecvThread,&m_RecvData ); } 注意 RecvThread()函数与前面服务器套接字中使用的函数是同一个函数。 3) 创建报文队列类 报文队列类负责维护从一个或者多个套接字来的报文,直到它们被处理。报文队列类不 仅包含用于容纳报文信息的通用报文类,而且使用同步过程防止线程取出报文时其他线程向 306第第第三部分第内部处理实例 下载队列添加报文。 使用C l a s s Wi z a r d创建从C O b L i s t派生的新类。 在新类中嵌入C M u t e x类变量: CMutex m_mutex; 在新类中添加A d d ( )函数,它使用C O b L i s t的A d d Ta i l ( )函数在队列中添加报文对象。使用 m _ m u t e x和C S i n g l e L o c k类以同步对该函数的访问: void CWzdQueue::Add( CWzdMsg *pMsg ) { CSingleLock slock( &m_mutex ); if ( slock.Lock( 1000 ) ) // timeout in milliseconds, // default = INFINITE { A d d Tail( pMsg ); // fifo } } 在新类中添加R e m o v e ( )函数,它使用 C O b L i s t的R e m o v e H e a d ( )函数从队列中清除报文对 象。再次使用m _ m u t e x和C S i n g l e L o c k类保护对该函数的访问。 CWzdMsg *CWzdQueue::Remove() { CSingleLock slock( &m_mutex) ; if ( slock.Lock( 1000 ) ) // timeout in milliseconds, // default = INFINITE { if ( !IsEmpty() ) return ( CWzdMsg* )RemoveHead(); } return NULL; } 参考本实例结尾的程序清单— 报文队列类以查看浏览这个类的完整代码列表。 4) 使用新的服务器套接字类 首先,在应用程序类的I n i t I n s t a n c e ( )函数调用A f x S o c k e t I n i t ( )初始化套接字动态链接库。 在应用程序窗口类(例如C M a i n F r a m e )中嵌入新的套接字类和报文队列类对象: CWzdServer m_server; CWzdQueue m_queue; 然后打开套接字,开始监听: if ( m_server. O p e n ( 1 0 3 2 // between 1025 and 0xffffffff set by you // to identify this server to your other apps ) ) { m _ s e r v e r. L i s t e n E x ( 1 0 , // size of message header - 1 , // position of size of message body in header // -1 means all message lengths are fixed 第13章第消息和通信第第307下载& m _ q u e u e , // CWzdQueue to store new messages t h i s , // pWnd of window to send messages 3 2 // starting id—as new connections are made // this number increases ) ; } 当套接字接收到新报文,使用 W M _ N E W _ M E S S A G E消息通知用户。对该消息的处理可 如下所示: L R E S U LT CMainFrame::OnNewMessage( WPA R A M , L PARAM ) { CWzdMsg *pMsg = NULL; while ( pMsg = m_queue.Remove() ) { // pMsg contains: // m_nID - - the user defined id of which port // sent the message // m_pHdr - - the message header // m_pBody - - the message body // m_len - - the total message length // m_error - - any errors // make sure to delete the message after processing! delete pMsg; } return 0L; } 如果连接终止,将接收到W M _ D O N E _ M E S S A G E消息: L R E S U LT CMainFrame::OnDoneMessage( WPARAM id,LPARAM error ) { // gets here if a socket receive thread returns // id == id of client port that terminated // error == any error that caused the socket to close return 0L; } 注意C W z d S e r v e r将跟踪已建立的新客户端连接,用户所要知道的是:当新的连接建立时, 该连接将得到一个标识符号码,该标识符号码可从任何来自该连接的报文消息中返回,它可 以设置为任意值,每建立一个新的连接,对应的标识符号码便递增一个值。在应用程序中可 以使用一个列表来保存标识符号码和实际客户端连接的关联。当连接关闭时,可从列表中删 去标识符号。 为发送报文到客户应用程序,使用: m _ s e r v e r. S e n d E x ( 3 2 , // id of client port h e l l o , // buffer to send 1 0 // length of buffer to send ) ; 如果应用程序终止,还必须调用服务器套接字类的函数 C l o s e E x ( )。可在窗口类中添加 308第第第三部分第内部处理实例 下载W M _ C L O S E消息处理函数,并在其中调用该函数: void CMainFrame::OnClose() { m _ s e r v e r. C l o s e E x ( ) ; C M D I F r a m e W n d : : O n C l o s e ( ) ; } 5) 使用新的客户套接字类 在应用程序类的I n i t I n s t a n c e ( )函数中调用A f x S o c k e t I n i t ( )初始化套接字动态链接库。 在应用程序窗口类(例如C M a i n F r a m e )中嵌入新的套接字类和报文队列类对象。 CWzdClient m_client; CWzdQueue m_queue; 打开套接字,开始监听服务器应用程序: if ( m_client.Open( " l o c a l h o s t " , // system address of server specified as: // "ftp.myhost.com" or "128.23.1.22" or // "localhost" for the same machine 1 0 3 2 // the server’s port number ) ) { m _ c l i e n t . L i s t e n E x ( 1 0 , // size of message header - 1 , // position of size of message body in header // -1 means all message lengths are fixed & m _ q u e u e , // CWzdQue to store new messages t h i s , // pWnd of window to send messages 0 // the user defined id of which client socket // sent the message ) ; } 虽然以上步骤允许应用程序监听来自服务器应用程序的报文,但是这些报文只是在服务 器应用程序接收到客户发送报文后才响应返回。为此,需首先向服务器应用程序发送报文, 使用下列代码: m _ c l i e n t . S e n d E x ( h e l l o , // buffer to send 1 0 // length of buffer to send ) ; 就像使用服务器应用程序一样,当新报文出现在队列中时,客户应用程序将接收到 W M _ N E W _ M E S S A G E消息,当连接终止时,则接收到 W M _ D O N E _ M E S S A G E消息。可以按 照类似于前面介绍的服务器应用程序消息处理方式来处理这些消息。 使用C l a s s Wi z a r d向需要关闭套接字的窗口类中添加 W M _ C L O S E消息处理函数: void CMainFrame::OnClose() { m _ c l i e n t . C l o s e ( ) ; 第13章第消息和通信第第309下载C M D I F r a m e W n d : : O n C l o s e ( ) ; } 4. 注意 ■ 受本实例篇幅所限,一些细节在上面的步骤中省略了,例如报文类的确切构成和传递 给线程函数的数据结构的具体格式。在本例结尾的程序清单中可以看到这些细节代码实现。 ■ 报文的头和主体被当作两个实体来处理,所以报文可以是变长度的,在本例中,报文 体的长度以指定的固定偏移量保存在报头中,该偏移值在调用 L i s t e n E x ( )函数时指定。它是一 个1 6位的变量。对不同的大小,只需在 R e c v T h r e a d ( )函数中改变重载的短整型 ( s h o r t * )的类型 即可。如果报文为固定长度,则可以删去本例中对报文体的处理代码,或者在指定报文长度 变量的位置时使用- 1。 ■ 本例使用套接字的同步模式,意味着在读取或写入完整的报文之前,读写操作不会返 回。如果应用程序完成其他的事情,例如响应用户输入,这样显然会出现问题。因此需要通 过各自的线程完成读写操作。套接字虽然可以实现同步读写,但实现十分复杂,而且不会比 使用线程的方式完成更多的功能。 ■ 套接字允许在应用程序之间使用串行化 ( S e r i a l i z a t i o n )通信,因此可以容易地传输可变 长度的类对象,而不是具有报文头和报文体的数据结构。串行化考虑了 Intel 和M o t o r o l a计算 机之间的大小终端,但是通信双方的应用程序都必须使用 M F C创建(例如:在Wi n d o w s主机和 苹果机之间 )。为使用串行化来实现套接字,需要使用下面的代码重写 S e n d T h r e a d ( )和 R e c v T h r e a d ( )函数: // create a socket file class object CSocketFile file( & s o c k // either the client or server socket class ) ; // construct an archive CArchive ar( & f i l e , // the file from above C A r c h i v e : : l o a d // for RecvThread() // CArchive:: store // for SendThread() ) ; // for RecvThread() to read a message ar >> object; // for SendThread() to write a message ar << object; 通过使用多态性,甚至可以发送可变长度的报文。有关套接字的更多知识,请参阅第 3 章。 5. 使用光盘时注意 运行两个工程实例,一个处于调试模式,另一个处于发布模式。单击每个程序上的 Te s t菜 单。可以指定任一个应用程序作为服务器应用程序或者是客户应用程序。程序中还有用于在 两个应用程序之间发送报文的菜单命令,可以在 C M a i n F r a m e中中断并监视这些菜单命令。 6. 程序清单— 报文队列类 // WzdQue.h: interface for the CWzdQueue class. / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / 310第第第三部分第内部处理实例 下载#if !defined( AFX_QUEUE_H__81CE0F22_8C16_11D2_A18D_99620BDF6820__INCLUDED_ ) #define AFX_QUEUE_H__81CE0F22_8C16_11 D 2 _ A 1 8 D _ 9 9 6 2 0 B D F 6 8 2 0 _ _ I N C L U D E D _ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #include class CWzdMsg : public CObject { p u b l i c : CWzdMsg( int id,LPSTR pHdr,LPSTR pBody,int len,int error ); virtual ~CWzdMsg(); int m_nID; LPSTR m_pHdr; LPSTR m_pBody; int m_len; int m_error; } ; class CWzdQueue : public CObList { p u b l i c : void Add( CWzdMsg *pMsg ); CWzdMsg *Remove(); p r i v a t e : CMutex m_mutex; } ; # e n d i f // !defined( AFX_QUEUE_H__81CE0F22_8C16_11D2_A18D_99620BDF6820__INCLUDED_ ) // WzdQue.cpp: implementation of the CWzdQueue class. / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #include "stdafx.h" #include "WzdQue.h" / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Construct Message / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / CWzdMsg::CWzdMsg( int id,LPSTR pHdr,LPSTR pBody,int len,int error ) { m_nID = id; m_pHdr = pHdr; m_pBody = pBody; 第13章第消息和通信第第311下载m_len = len; m_error = error; } C W z d M s g : : ~ C W z d M s g ( ) { delete []m_pHdr; delete []m_pBody; } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Add Message / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / void CWzdQueue::Add( CWzdMsg *pMsg ) { CSingleLock slock( &m_mutex ); if ( slock.Lock( 1000 ) ) // timeout in milliseconds, default = INFINITE { A d d Tail(pMsg); // fifo } } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Remove Message / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / CWzdMsg *CWzdQueue::Remove() { CSingleLock slock( &m_mutex ); if ( slock.Lock( 1000 ) ) // timeout in milliseconds, default = INFINITE { if ( !IsEmpty() ) return ( CWzdMsg* )RemoveHead(); } return NULL; } 7. 程序清单— 套接字类 // WzdSock.h: interface for the CWzdServer and CWzdClient class. / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #if !defined( AFX_WZDSOCK_H__81CE0F20_8C16_11D2_A18D_99620BDF6820__INCLUDED_ ) #define AFX_WZDSOCK_H__81CE0F20_8C16_11 D 2 _ A 1 8 D _ 9 9 6 2 0 B D F 6 8 2 0 _ _ I N C L U D E D _ #if _MSC_VER > 1000 #pragma once 312第第第三部分第内部处理实例 下载#endif // _MSC_VER > 1000 #include #include #include "WzdQue.h" #define WM_NEW_MESSAGE WM_USER+1 #define WM_DONE_MESSAGE WM_USER+2 / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Thread data / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / typedef struct t_SENDDATA { CSocket *pSocket; LPSTR lpBuf; int len; } SENDDATA ; typedef CMap SOCKMAP; typedef struct t_RECVDATA { CSocket *pSocket; int hdrSz; int bodyPos; CWzdQueue *pQueue; CWnd *pWnd; UINT id; } RECVDATA ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdServer / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / class CWzdServer : public CSocket { p u b l i c : C W z d S e r v e r ( ) { } ; virtual ~CWzdServer(); BOOL Open( UINT nPort ); void CloseEx(); void SendEx( int id, LPSTR lpBuf, int len ); void ListenEx( int hdrSz, int bodyPos, CWzdQueue *pQueue, CWnd *pWnd, UINT id ); // Overrides virtual void OnAccept ( int nErrorCode ); p r i v a t e : int m_id; 第13章第消息和通信第第313下载S E N D D ATA m_SendData; R E C V D ATA m_RecvData; SOCKMAP m_mapSockets; } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdClient / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / class CWzdClient : public CSocket { p u b l i c : CWzdClient() {}; virtual ~CWzdClient() {}; BOOL Open( LPCTSTR lpszHostAddress, UINT nHostPort ); void SendEx( LPSTR lpBuf, int len ); void ListenEx( int hdrSz, int bodyPos, CWzdQueue *pQueue, CWnd *pWnd, UINT id ); p r i v a t e : S E N D D ATA m_SendData; R E C V D ATA m_RecvData; } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Threads / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / UINT SendThread( LPVOID pParam ); UINT RecvThread( LPVOID pParam ); # e n d i f // !defined( AFX_WZDSOCK_H__81CE0F20_8C16_11D2_A18D_99620BDF6820__INCLUDED_ ) // WzdSock.cpp: implementation of the CWzdServer and CWzdClient classes. / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #include "stdafx.h" #include "WzdSock.h" / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdServer / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Cleanup / / / / / / / / / / / / / / / / / / / / / / / / / C W z d S e r v e r : : ~ C W z d S e r v e r ( ) { // cleanup all created sockets int id; CSocket *pSocket; 314第第第三部分第内部处理实例 下载for ( POSITION pos = m_mapSockets.GetStartPosition(); pos; ) { m_mapSockets.GetNextAssoc( pos,id,pSocket ); delete pSocket; } } / / / / / / / / / / / / / / / / / / / / / / / / / // Open Socket / / / / / / / / / / / / / / / / / / / / / / / / / BOOL CWzdServer::Open( UINT nPort ) { return Create( nPort ); } / / / / / / / / / / / / / / / / / / / / / / / / / // Send to Socket / / / / / / / / / / / / / / / / / / / / / / / / / void CWzdServer::SendEx( int id, LPSTR lpBuf, int len ) { // locate the socket for this id CSocket *pSocket = m_mapSockets[id]; if ( pSocket ) { m_SendData.pSocket = pSocket; m_SendData.lpBuf = lpBuf; m_SendData.len = len; // start the thread A f x B e g i n T h r e a d ( S e n d T h r e a d , & m _ S e n d D a t a ) ; } } / / / / / / / / / / / / / / / / / / / / / / / / / // Listen to Socket / / / / / / / / / / / / / / / / / / / / / / / / / void CWzdServer::ListenEx( int hdrSz, int bodyPos, CWzdQueue *pQueue, CWnd *pWnd, UINT id ) { // initialize receive data m_RecvData.hdrSz = hdrSz; m_RecvData.bodyPos = bodyPos; m_RecvData.pQueue = pQueue; m_RecvData.pWnd = pWnd; m_id = id; // starting id // start listening L i s t e n ( ) ; } 第13章第消息和通信第第315下载// Listen() calls OnAccept() when a new client is attempting to connect void CWzdServer::OnAccept ( int nErrorCode ) { if ( nErrorCode == 0 ) { // create a new socket and add to map CSocket *pSocket = new CSocket; m_mapSockets[m_id] = pSocket; // use this new socket to connect to client Accept( ( CAsyncSocket& )*pSocket ); // put socket into synchronous mode DWORD arg = 0; pSocket -> AsyncSelect( 0 ); pSocket -> IOCtl( FIONBIO, &arg ); // setup this socket to listen for client messages m_RecvData.pSocket = pSocket; m_RecvData.id = m_id++; // start the thread AfxBeginThread( RecvThread,&m_RecvData ); } } / / / / / / / / / / / / / / / / / / / / / / / / / // Close Sockets / / / / / / / / / / / / / / / / / / / / / / / / / void CWzdServer::CloseEx() { int id; CSocket *pSocket; for ( POSITION pos = m_mapSockets.GetStartPosition(); pos; ) { m_mapSockets.GetNextAssoc( pos,id,pSocket ); pSocket -> Close(); } C l o s e ( ) ; } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdClient / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Open Socket / / / / / / / / / / / / / / / / / / / / / / / / / 316第第第三部分第内部处理实例 下载BOOL CWzdClient::Open( LPCTSTR lpszHostAddress, UINT nHostPort ) { if ( Create() && Connect( lpszHostAddress, nHostPort ) ) { // put socket into synchronous mode DWORD arg = 0; AsyncSelect( 0 ); IOCtl( FIONBIO, &arg ); return TRUE; } return FA L S E ; } / / / / / / / / / / / / / / / / / / / / / / / / / // Send to Socket / / / / / / / / / / / / / / / / / / / / / / / / / void CWzdClient::SendEx( LPSTR lpBuf, int len ) { // initialize the structure we will pass to thread m_SendData.pSocket = this; m_SendData.lpBuf = lpBuf; m_SendData.len = len; // start the thread AfxBeginThread( SendThread,&m_SendData ); } / / / / / / / / / / / / / / / / / / / / / / / / / // Listen to Socket / / / / / / / / / / / / / / / / / / / / / / / / / void CWzdClient::ListenEx( int hdrSz, int bodyPos, CWzdQueue *pQueue, CWnd *pWnd, UINT id ) { // initialize receive data m_RecvData.pSocket = this; m_RecvData.hdrSz = hdrSz; m_RecvData.bodyPos = bodyPos; m_RecvData.pQueue = pQueue; m_RecvData.pWnd = pWnd; m_RecvData.id = id; // start the thread AfxBeginThread( RecvThread,&m_RecvData ); } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Threads 第13章第消息和通信第第317下载/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / UINT SendThread( LPVOID pParam ) { // get data from thread creator S E N D D ATA *pSend = ( SENDDATA * )pParam; // do the write pSend -> pSocket -> Send( pSend -> lpBuf, pSend -> len ); return 0; } UINT RecvThread( LPVOID pParam ) { // get data from thread creator R E C V D ATA *pRecv = ( RECVDATA * )pParam; int len = 1; int error = 0; char *pBody = NULL; char *pHdr = NULL; // while both sockets are open while ( TRUE ) { // read the header int res; pBody = NULL; pHdr = new char[pRecv -> hdrSz]; if ( ( res = pRecv -> pSocket -> CAsyncSocket::Receive( pHdr, pRecv -> hdrSz)) == SOCKET_ERROR ) error = ::GetLastError(); e l s e len = res; // if closing down, exit thread if ( len == 0 || error == WSAECONNRESET || error == WSAECONNABORTED ) b r e a k ; // read the body??? if ( !error && len && pRecv -> bodyPos != -1 ) { int bodyLen = *( ( short * )pHdr+pRecv -> bodyPos ); pBody = new char[bodyLen]; if ( ( res = pRecv -> pSocket -> CAsyncSocket::Receive( pBody, bodyLen ) ) == SOCKET_ERROR ) error = ::GetLastError(); e l s e len += res; // if closing down, exit thread 318第第第三部分第内部处理实例 下载if (len == 0 || error == WSAECONNRESET || error == WSAECONNABORT E D ) b r e a k ; } // put message in queue pRecv -> pQueue -> Add(new CWzdMsg( pRecv -> id,pHdr, p B o d y,len,error ) ); // post message to window to process this new message pRecv -> pWnd -> PostMessage( WM_NEW_MESSAGE ); } // cleanup anything we started delete []pHdr; delete []pBody; // tell somebody we stopped pRecv -> pWnd -> SendMessage( WM_DONE_MESSAGE,( WPARAM )pRecv -> id,( LPARAM )error ); return 0; } // WzdQue.h: interface for the CWzdQueue class. / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #if !defined( AFX_QUEUE_H__81CE0F22_8C16_11D2_A18D_99620BDF6820__INCLUDED_ ) #define AFX_QUEUE_H__81CE0F22_8C16_11 D 2 _ A 1 8 D _ 9 9 6 2 0 B D F 6 8 2 0 _ _ I N C L U D E D _ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #include class CWzdMsg : public CObject { p u b l i c : CWzdMsg( int id,LPSTR pHdr,LPSTR pBody,int len,int error ); virtual ~CWzdMsg(); int m_nID; LPSTR m_pHdr; LPSTR m_pBody; int m_len; int m_error; } ; class CWzdQueue : public CObList { p u b l i c : void Add( CWzdMsg *pMsg ); CWzdMsg *Remove(); 第13章第消息和通信第第319下载p r i v a t e : CMutex m_mutex; } ; # e n d i f // !defined( AFX_QUEUE_H__81CE0F22_8C16_11D2_A18D_99620BDF6820__INCLUDED_ ) // WzdQue.cpp: implementation of the CWzdQueue class. / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #include “stdafx.h” #include “WzdQue.h” / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Construct Message / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / CWzdMsg::CWzdMsg( int id,LPSTR pHdr,LPSTR pBody,int len,int error ) { m_nID = id; m_pHdr = pHdr; m_pBody = pBody; m_len = len; m_error = error; } C W z d M s g : : ~ C W z d M s g ( ) { delete []m_pHdr; delete []m_pBody; } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Add Message / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / void CWzdQueue::Add( CWzdMsg *pMsg ) { CSingleLock slock( &m_mutex ); if ( slock.Lock( 1000 ) ) // timeout in milliseconds, default = INFINITE { A d d Tail(pMsg); // fifo } } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Remove Message / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / 320第第第三部分第内部处理实例 下载CWzdMsg *CWzdQueue::Remove() { CSingleLock slock( &m_mutex ); if ( slock.Lock( 1000 ) ) // timeout in milliseconds, default = INFINITE { if ( !IsEmpty() ) return (CWzdMsg*)RemoveHead(); } return NULL; } 13.6 实例52:使用串行或并行I/O 1. 目标 在M F C的应用程序中支持存取串行或者并行设备。 2. 策略 与串行或并行设备通信实际上和与磁盘文件交互没有什么区别。在本例中将使用 M F C的 C F i l e类通过指定COM1: 或者L P T 2 :来实现与这些设备的通信。将读写操作转换为与串行或者 并行设备的实际转换过程由硬件驱动程序完成。本例在此方式上实现了更进一步的功能,即 允许应用程序同时与几个串行或者并行设备通信,其中使用了多任务以避免某一设备过于繁 忙而堵塞。本例中实际上借用了在前面套接字实例中创建的报文队列类和一些术语,以实现 将接收到的报文引导到一个中央消息处理程序。与前面例子不同的是,串行和并行通信是如 此相似以至于可以将这些功能封装在一个 C F i l e的派生类中。 3. 步骤 1) 创建新的端口I / O类 使用C l a s s Wi z a r d创建从C F i l e派生的新类。 使用文本编辑器在新类中添加 O p e n L P T ( )函数,用于打开并行设备。这里仅使用 C F i l e : : M o d e R e a d Wr i t e模式调用O p e n ( )函数: BOOL CWzdPortIO::OpenLPT( int n,CFileException *e ) { CString portName; portName.Format( "LPT%d:", n ); return Open( portName, CFile::modeReadWrite, e ); } 创建O p e n C O M ( )函数,用于打开串行设备。实际上,串行设备也必须使用::SetCommState () 设置波特率和停止位,如下所示: BOOL CWzdPortIO::OpenCOM( int n, CFileException *e, int baud, int parity, int databits, int stopbits ) { CString portName; portName.Format( "COM%d:", n ); if ( Open( portName, CFile::modeReadWrite, e ) ) { DCB dcb; 第13章第消息和通信第第321下载::GetCommState( ( HANDLE )m_hFile, &dcb ); if ( baud != -1 ) dcb.BaudRate = baud; if ( databits != -1 ) dcb.ByteSize = databits; if ( stopbits != -1 ) dcb.StopBits = stopbits; if ( parity != -1 ) dcb.Parity = parity; ::SetCommState( ( HANDLE )m_hFile, &dcb ); return( TRUE ); } return( FALSE ); } 为向端口设备发送报文。只需使用 C F i l e的Wr i t e ( )函数,但是在线程中而不是在应用程序 中调用它,这样可以避免应用程序陷入困境: void CWzdPortIO::Send( LPSTR lpBuf, int len ) { // initialize the structure we will pass to thread m_SendData.pFile = this; m_SendData.lpBuf = lpBuf; m_SendData.len = len; // start the thread AfxBeginThread( SendThread,&m_SendData ); } 如前面所述,发送线程只使用 C F i l e的Wr i t e ( )函数: UINT SendThread( LPVOID pParam ) { // get data from thread creator S E N D D ATA *pSend = ( SENDDATA * )pParam; // do the write pSend -> pFile -> Write( pSend -> lpBuf, pSend -> len ); return 0; } 为从端口读取数据,在此采用了类似 Wi n d o w s套接字中监听的概念,创建一个叫做 L i s t e n ( )的函数,它在线程中使用一个 C F i l e的R e a d ( )函数。该函数在接收到字节之前一直进行 读取,当接收到字节时,一个报文对象将创建并加入到报文队列中,同时一个 W M _ N E W _ M E S S A G E消息将发送到主应用程序并在此处理,实际上在这里使用的报文队列类来自上一个 套接字实例。 使用文本编辑器添加一个 L i s t e n ( )函数,在其中首先使用 : : S e t C o m m Ti m e o u t ( )函数避免该 端口在进行读取操作时出现超时,然后调用 R e c v T h r e a d ( )函数来进行实际的读取操作: void CWzdPortIO::Listen( int hdrSz, int bodyPos, CWzdQueue *pQueue, CWnd *pWnd, UINT msg, UINT id ) { // cancel timeouts! we want to wait forever until // next message comes in COMMTIMEOUTS cto; : : G e t C o m m Timeouts( ( HANDLE )m_hFile, &cto ); 322第第第三部分第内部处理实例 下载c t o . R e a d I n t e r v a l Timeout = 0; c t o . Wr i t e To t a l TimeoutMultiplier = 0; c t o . Wr i t e To t a l TimeoutConstant = 0; : : S e t C o m m Timeouts( ( HANDLE )m_hFile, &cto ); // initialize the structure we will pass to thread m_RecvData.pFile = this; m_RecvData.hdrSz = hdrSz; m_RecvData.bodyPos = bodyPos; m_RecvData.pQueue = pQueue; m_RecvData.pWnd = pWnd; m_RecvData.msg = msg; m_RecvData.id = id; // start the thread AfxBeginThread( RecvThread,&m_RecvData ); } R e c v T h r e a d ( )函数使用了报头和报文体的概念,因此报文可以是可变长度的。报头一般是 固定长度的,其中包含了报文体的长度。 R e a d ( )函数可以通过这种方式确切地知道在返回前 需读取多少个字节。 使用文本编辑器添加 R e c v T h r e a d ( )函数到该类,在此设置循环直到出错。如果出错,则 停止读操作并向应用程序发送 W M _ D O N E _ M E S S A G E消息。使用 C F i l e的R e a d ( )函数读取报 文的报头和报文体,并将它们放入报文队列,然后使用 W M _ N E W _ M E S S A G E消息通知应用 程序: UINT RecvThread( LPVOID pParam ) { // get data from thread creator R E C V D ATA *pRecv = ( RECVDATA * )pParam; while ( TRUE ) // forever { // read the header int len; int error = 0; char *pHdr = new char[pRecv -> hdrSz]; t r y { len = pRecv -> pFile -> Read( pHdr, pRecv -> hdrSz ); } catch ( CFileException *e ) { error = e -> m_cause; e -> Delete(); } // read the body??? char *pBody = NULL; if ( !error && pRecv -> bodyPos != -1 ) 第13章第消息和通信第第323下载{ int bodyLen = *( ( short * )pHdr+pRecv -> bodyPos ); pBody = new char[bodyLen]; t r y { len += pRecv -> pFile -> Read( pBody, bodyLen ); } catch ( CFileException *e ) { error = e -> m_cause; e -> Delete(); } } // put message in queue pRecv -> pQueue -> Add( new CWzdMsg( pRecv -> id,pHdr, p B o d y,len,error ) ); // post message to window to process this new message pRecv -> pWnd -> PostMessage( pRecv -> msg ); } return 0; } 2) 对并行通信使用新的端口I / O类 在窗口类例如C M a i n F r a m e中,嵌入C W z d Q u e u e和C W z d P o r t I O。 CWzdPortIO m_parallel; CWzdQueue m_queue; 打开连接,并使用以下代码开始监听: CFileException e; if ( m_parallel.OpenLPT( 1 , // LPT number (1,2,etc.) & e // exception errors (defaults to NULL) ) ) { m _ p a r a l l e l . L i s t e n ( 1 0 , // size of message header - 1 , // position of size of message body // in header // -1 means all message lengths // are fixed & m _ q u e u e , // CWzdQue to store new messages t h i s , // pWnd of window to send // “new message” message W M _ N E W _ M E S S A G E , // “new message” to send 0 // the user defined id of which port // sent the message ) ; } 324第第第三部分第内部处理实例 下载为在接收到新报文时对它们进行处理,在窗口类中手工添加 W N _ N E W _ M E S S A G E处理函 数: ON_MESSAGE( WM_NEW_MESSAGE,OnNewMessage ) : : : L R E S U LT CMainFrame::OnNewMessage( WPA R A M , L PARAM ) { CWzdMsg *pMsg = NULL; while ( pMsg = m_queue.Remove() ) { // pMsg contains: // m_nID - - the user defined id of which port sent // the message // m_pHdr - - the message header // m_pBody - - the message body // m_len -- the total message length // m_error - - any errors // make sure to delete the message after processing! delete pMsg; } return OL; } 为发回报文到端口,使用: m _ p a r a l l e l . S e n d ( h e l l o , // buffer to send 7 // length of buffer to send ) ; 3) 对串行通信使用新的端口I / O类 在窗口类例如C M a i n F r a m e中,嵌入C W z d Q u e u e和C W z d P o r t I O: CWzdPortIO m_serial; CWzdQueue m_queue; 打开连接,并使用以下代码开始监听: CFileException e; if ( m_serial.OpenCOM( 1 , // COM number (1,2,etc.) & e , // exception errors (defaults to NULL) C B R _ 1 9 2 0 0 , // baud rate, also CBR_1200, CBR_2400, etc. N O PA R I T Y, // parity, also EVENPA R I T Y, ODDPA R I T Y, // MARKPA R I T Y, SPA C E PA R I T Y 8 , // number of bits in a byte O N E S TO P B I T // stopbits, also ONE5STOPBITS, TWOSTO P B I T S ) ) { m _ s e r i a l . L i s t e n ( 1 0 , // size of message header - 1 , // position of size of message body in header 第13章第消息和通信第第325下载// -1 means all message lengths are fixed & m _ q u e u e , // CWzdQue to store new messages t h i s , // pWnd of window to send “new message” message W M _ N E W _ M E S S A G E , // message to send 1 // the user defined id of which port sent // the message ) ; } 处理和发送报文的过程与前面在并行端口中的方式相同。 4. 注意 本例中使用的方案允许应用程序使用自己的标识符来区分哪一个报文来自哪一个设备, 以此实现同时监控几个设备。 5. 使用光盘时注意 运行工程的两个实例,其中一个处于调试模式,另一个处于发布模式。单击 Te s t菜单可以 试验串行或者并行通信,但是需要能够协同工作的设备。 6. 程序清单— 端口I / O类 // PortIO.h: interface for the CPortIO class. / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #if !defined( AFX_PORT I O _ H _ _ 8 1 C E 0 F 2 0 _ 8 C 1 6 _ 11D2_A18D_99620BDF6820__INCLUDED_ ) #define AFX_PORT I O _ H _ _ 8 1 C E 0 F 2 0 _ 8 C 1 6 _ 11 D 2 _ A 1 8 D _ 9 9 6 2 0 B D F 6 8 2 0 _ _ I N C L U D E D _ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #include "WzdQue.h" // define the send and receive threads typedef struct t_SENDDATA { CFile *pFile; LPSTR lpBuf; int len; } SENDDATA ; typedef struct t_RECVDATA { CFile *pFile; int hdrSz; int bodyPos; CWzdQueue *pQueue; CWnd *pWnd; UINT msg; UINT id; } RECVDATA ; 326第第第三部分第内部处理实例 下载class CWzdPortIO : public CFile { p u b l i c : CWzdPortIO() {}; virtual ~CWzdPortIO() {}; BOOL OpenLPT( int n,CFileException *e = NULL ); BOOL OpenCOM( int n,CFileException *e = NULL, int baud = -1, int parity = -1, int databits = -1, int stopbits = -1); void Send( LPSTR lpBuf, int len ); void Listen( int hdrSz, int bodyPos, CWzdQueue *pQueue, CWnd *pWnd, UINT msg, UINT id); p r i v a t e : S E N D D ATA m_SendData; R E C V D ATA m_RecvData; } ; UINT SendThread( LPVOID pParam ); UINT RecvThread( LPVOID pParam ); # e n d i f // !defined( AFX_PORT I O _ H _ _ 8 1 C E 0 F 2 0 _ 8 C 1 6 _ 11D2_A18D_99620BDF6820__INCLUDED_ ) // PortIO.cpp: implementation of the CPortIO class. / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #include "stdafx.h" #include "WzdPrtIO.h" / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Open Printer Port / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / BOOL CWzdPortIO::OpenLPT( int n,CFileException *e ) { CString portName; portName.Format( "LPT%d:", n ); return Open( portName, CFile::modeReadWrite, e ); } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Open Serial Port / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / BOOL CWzdPortIO::OpenCOM( int n, CFileException *e, int baud, int parity, int databits, int stopbits ) { CString portName; 第13章第消息和通信第第327下载portName.Format( "COM%d:", n ); if ( Open( portName, CFile::modeReadWrite, e) ) { DCB dcb; ::GetCommState( ( HANDLE )m_hFile, &dcb ); if ( baud != -1 ) dcb.BaudRate = baud; if ( databits != -1 ) dcb.ByteSize = databits; if ( stopbits != -1 ) dcb.StopBits = stopbits; if ( parity != -1 ) dcb.Parity = parity; ::SetCommState( ( HANDLE )m_hFile, &dcb ); return( TRUE ); } return( FALSE ); } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Send to Port / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / void CWzdPortIO::Send( LPSTR lpBuf, int len ) { // initialize the structure we will pass to thread m_SendData.pFile = this; m_SendData.lpBuf = lpBuf; m_SendData.len = len; // start the thread AfxBeginThread( SendThread,&m_SendData ); } UINT SendThread( LPVOID pParam ) { // get data from thread creator S E N D D ATA *pSend = ( SENDDATA * )pParam; // do the write pSend -> pFile -> Write( pSend -> lpBuf, pSend -> len ); return 0; } / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // Listen to Port / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / void CWzdPortIO::Listen( int hdrSz, int bodyPos, CWzdQueue *pQueue, CWnd *pWnd, UINT msg, UINT id ) { // cancel timeouts! we want to wait forever until next message comes in COMMTIMEOUTS cto; 328第第第三部分第内部处理实例 下载: : G e t C o m m Timeouts( ( HANDLE )m_hFile, &cto ); c t o . R e a d I n t e r v a l Timeout = 0; c t o . Wr i t e To t a l TimeoutMultiplier = 0; c t o . Wr i t e To t a l TimeoutConstant = 0; : : S e t C o m m Timeouts( ( HANDLE )m_hFile, &cto ); // initialize the structure we will pass to thread m_RecvData.pFile = this; m_RecvData.hdrSz = hdrSz; m_RecvData.bodyPos = bodyPos; m_RecvData.pQueue = pQueue; m_RecvData.pWnd = pWnd; m_RecvData.msg = msg; m_RecvData.id = id; // start the thread AfxBeginThread( RecvThread,&m_RecvData ); } UINT RecvThread( LPVOID pParam ) { // get data from thread creator R E C V D ATA *pRecv = ( RECVDATA * )pParam; while ( TRUE ) //forever { // read the header int len; int error = 0; char *pHdr = new char[pRecv -> hdrSz]; t r y { len = pRecv -> pFile -> Read( pHdr, pRecv -> hdrSz ); } catch ( CFileException *e ) { error = e -> m_cause; e -> Delete(); } // read the body??? char *pBody = NULL; if ( !error && pRecv -> bodyPos != -1 ) { int bodyLen = *( ( short * )pHdr+pRecv -> bodyPos ); pBody = new char[bodyLen]; t r y { len += pRecv -> pFile -> Read( pBody, bodyLen ); } catch ( CFileException *e ) 第13章第消息和通信第第329下载{ error = e -> m_cause; e -> Delete(); } } // put message in queue pRecv -> pQueue -> Add( new CWzdMsg( pRecv -> id,pHdr, p B o d y,len,error ) ); // post message to window to process this new message pRecv -> pWnd -> PostMessage( pRecv -> msg ); } return 0; } 330第第第三部分第内部处理实例 下载下载下载 第14章 多 任 务 由于Wi n d o w s是多任务操作系统,应用程序可以以多种方式运行。在应用程序内部通过 创建同时运行的处理线程可以实现多任务。此时还可以从自己的应用程序来执行其他应用程 序,但是它们实际上将与用户自己的应用程序同时运行。 M F C提供了一种使用方便的成员函 数,将其重载便可以处理后台程序。本章的实例具体包括: 实例53 后台处理,本例中将演示在没有命令需要处理时,一种连续清除后台应用程序临 时分配内存的方式。 实例54 运行其他应用程序。本例中将从自己的应用程序来运行其他应用程序。 实例55 改变优先级,当没有其他活跃的应用程序时,改变应用程序或者线程的优先级, 使它运行得更快或者更慢。 实例56 应用程序(工作者线程)内部的多任务处理,本例中将创建一个工作者线程 ( Wo r k e r T h r e a d )来完成数学运算功能,与此同时由应用程序查询用户的需要。工作者线程与后面例子 中的用户界面线程不同,后者意味着面向任务存在,任务完成后则终止。 实例57 应用程序 (用户界面线程 )内部的多任务处理,本例将创建用户界面线程 ( U s e r Interface Thread),该线程具有运行另外一个应用程序的功能,同时又具有与应用程序在同一 地址空间的好处,这将允许线程和应用程序之间轻而易举地共享数据。 实例58 向线程发送消息,本例将实现与用户界面线程通信。 实例59 与线程共享数据,本例将演示一种在多个线程之间无冲突地共享同一个数据地址 空间的方式。 14.1 实例53:后台处理 1. 目标 在应用程序处于空闲时,清除应用程序所占的零碎内存,在本例中,将在后台清除临时 分配的内存。 2. 策略 使用C l a s s Wi z a r d重载应用程序类的O n I d l e ( )函数,只要没有用户命令需要处理,应用程序 便调用它。 3. 步骤 后台处理 用Class Wi z a r d重载应用程序类的O n I d l e ( )成员函数。 在本例中,使用重载的O n I d l e ( )函数删除临时对象的列表: BOOL CWzdApp::OnIdle( LONG lCount ) { // clean up temporary objects while ( !m_TempList.IsEmpty() ) {delete m_Te m p L i s t . R e m o v e H e a d ( ) ; } return CWinApp::OnIdle( lCount ); } 4. 注意 ■ 确保重载的O n I d l e ( )连续调用C Wi n A p p : : O n I d l e ( ) ,否则应用程序将中断工作。 ■ 一个在其“外壳 ( s h e l l )”中运行了多个其他应用程序的应用程序可以使用该实例以确 保它产生的应用程序仍在运行,否则,它可以再次运行它们。 ■ O n I d l e ( )不是进行冗长处理过程的理想场所,例如数学处理。如果 O n I d l e ( )不能迅速返 回,则由于鼠标和键盘的消息被延迟响应而使应用程序将看起来显得反应迟钝,因此最好在 其他应用程序或者线程中完成密集运算,详见下面的实例。 5. 使用光盘时注意 运行随书附带光盘上的工程时,在W z d . c p p的O n I d l e ( )函数中设置断点(在条件语句中设置, 以免不停地产生中断)。单击W z d菜单中的Te s t命令,填满一个全局列表集合。注意 O n I d l e ( )函 数将清除这个集合。 14.2 实例54:运行其他应用程序 1. 目标 从一个应用程序运行其他应用程序。 2. 策略 由于 M F C 类库中目前没有可以运行其他应用程序的 M F C类,因此本例使用两个 Wi n d o w s A P I调用。第一个是 : : WinExec (),它是一个简单、直接的 A P I,微软不推荐使 用。: : Wi n E x e c ( )调用了第二个A P I,::CreateProcess (),它提供了对新生成程序的更广泛控制。 3. 步骤 1) 使用: : WinExec ()运行应用程序 使用: : WinExec ()运行应用程序可以参阅下面的代码。在本例中,运行了批处理文件W z d . b a t。 CString str; str = "Wzd.bat"; // execute a batch file if ( ::Wi n E x e c ( s t r, // command line SW_NORMAL ) // see ShowWindow for other options >31 ) // numbers lower then 31 are failures { AfxMessageBox( "Successfully created." ); } e l s e { AfxMessageBox( "Failed to create process." ); } 2) 使用: : C r e a t e P r o c e s s ( )运行应用程序 以另外的方式运行相同的批处理文件,但具有更多的选项,代码如下所示。 332第第第三部分第内部处理实例 下载CString str; S TA RTUPINFO si; P R O C E S S _ I N F O R M ATION pi; // specify command str = "Wzd.bat"; // zero out and initialize STA RT U P I N F O memset( &si, 0, sizeof( si ) ); si.cb = sizeof( si ); si.dwFlags = STA RT F _ U S E S H O W W I N D O W; s i . w S h o w Window = SW_SHOW; if ( CreateProcess( N U L L , // can be name of process unless // batch file, else must be // in command line: ( char * )LPCSTR( str ), // command line N U L L , N U L L , // security options FA L S E , // if true will inherit all // inheritable handles // from this process N O R M A L _ P R I O R I T Y _ C L A S S , // can also be HIGH_PRIORITY_CLASS // or IDLE_PRIORITY_CLASS N U L L , // inherit this process's // environment block N U L L , // specifies working directory // of created process & s i , // STA RTUPINFO specified above & p i // PROCESS_INFORMATION returned ) ) { AfxMessageBox( "Successfully created." ); HANDLE pH = pi.hProcess; // wait until application is ready for input if ( !WaitForInputIdle( pH,1000 ) ) { // send messages, etc. } // kill process with 0 exit code TerminateProcess( pH, 0 ); } e l s e { AfxMessageBox( "Failed to create process." ); } } 3) 获取应用程序的目录 另外一个Windows API调用: : G e t M o d u l e F i l e N a m e ( )将通知应用程序其执行文件所在磁盘的 第14章第多第任第务第第333下载子目录。当自己的应用程序与在它运行时需要访问的其他文件安装在同一目录中时,以上这 一点将特别有用。 为寻找应用程序所在的目录,使用以下代码: // change directory to this application's .exe file char szBuff e r [ 1 2 8 ] ; ::GetModuleFileName( AfxGetInstanceHandle(), szBuff e r, sizeof( szBuffer ) ); char *p = strrchr( szBuff e r, '\\' ); *p = 0; S Z B u ff e r变量包括应用程序的主目录名。 为了改变应用程序的当前工作目录到该路径,使用: _ c h d i r ( s z B u ff e r ) ; 一旦这个目录成为当前目录,应用程序就可以无需指定路径而打开任何文件。 4. 注意 ■ 传递给Wi n E x e c ( )的第二个参数与调用 S h o w Wi n d o w ( )的参数相同。它包括 S W _ H I D E , SW_MAXMIZE, SW_MINIMIZE等。此标记影响正在执行的应用程序的主窗口。 ■ 如果在A P I的命令行参数中写入了路径,确保周围加上了双引号。其中的原因是由于 有间隔符“\”,所以以下将认为是三个独立的命令行参数。 C:\Program Files\Microsoft Off i c e \ p r o g . e x e 而使用双引号,则被认为是一个参数。 "C:\Program Files\Microsoft Off i c e \ p r o g . e x e " ■ 当运行批处理文件时,实际上是在运行 C M D . E X E并将批处理文件的名字作为命令行参 数传递给它。这就是批处理文件在使用 : : C r e a t e P r o c e s s ( )时不能作为第一个参数的原因。 ■ 在本例中,使用了 Te r m i n a t e P r o c e s s ( )删除已运行的文件。然而,这种方法可能不妥 — 应用程序在未结束前不能被清除。一种更好的方法是向应用程序发送消息使它关闭。由 于M F C没有提供相应的函数,因此需要自己创建 (参考实例5 8 )。如果应用程序无法关闭,则 使用Te r m i n a t e P r o c e s s ( )。 ■ 与其运行其他应用程序,倒不如在当前的应用程序中创建简单的处理线程。应用程序 本身就是可以创建其他线程的线程。由于 Wi n d o w s是多任务处理系统,因此每个创建的线程 都可以同时运行,而且在它们完成自己的任务时将通知应用程序。由于线程与应用程序在相 同的程序地址空间中运行,所以应用程序和线程之间的数据传递与应用程序之间的数据传递 相比要容易得多。创建线程而非应用程序的例子请参考实例 5 7。 ■ 与新创建的应用程序进行通信请参考实例 4 9。若与它共享数据则请参考实例 5 0。 5. 使用光盘时注意 运行随书附带光盘上的工程时,在 M a i n f r m . c p p中的O n W z d 1 Te s t ( )或者O n W z d 2 Te s t ( )函数 中设置断点。单击Te s t菜单的W z d 1或者W z d 2,注意函数如何运行批处理文件 w z d . b a t。 14.3 实例55:改变优先级 1. 目标 改变应用程序或者线程的优先级,使其在后台运行得更快或者更慢。 334第第第三部分第内部处理实例 下载2. 策略 改变应用程序的优先级,使用::SetPriority() API。改变线程的优先级则使用: : S e t T h r e a d P r i o r i t y () API。 3. 步骤 1) 改变应用程序的优先级 改变应用程序的优先级,使用: ::SetPriorityClass( : : G e t C u r r e n t P r o c e s s ( ) , // process handle // REALT I M E _ P R I O R I T Y _ C L A S S // highest: thread must run // immediately before any // other system task // HIGH_PRIORITY_CLASS // high: time-critical threads // NORMAL_PRIORITY_CLASS // normal: thread with equal // importance to other // system applications I D L E _ P R I O R I T Y _ C L A S S // low: threads that can run in // the background of the // entire system ) ; 获得当前的优先级: DWORD priority = : : G e t P r i o r i t y C l a s s ( : : G e t C u r r e n t P r o c e s s ( ) // process handle ) ; 2) 改变线程的优先级 如下所示,在线程中改变线程的优先级: S e t T h r e a d P r i o r i t y ( T H R E A D _ P R I O R I T Y _ T I M E _ C R I T I C A L // highest priority T H R E A D _ P R I O R I T Y _ H I G H E S T // next highest T H R E A D _ P R I O R I T Y _ A B O V E _ N O R M A L // etc.... T H R E A D _ P R I O R I T Y _ N O R M A L T H R E A D _ P R I O R I T Y _ B E L O W _ N O R M A L T H R E A D _ P R I O R I T Y _ L O W E S T T H R E A D _ P R I O R I T Y _ I D L E // lowest priority ) ; 4. 注意 设置优先级并不总是有助于多任务处理。与常识相反,应该为密集占用 C P U的应用程序 或者线程赋予较低的优先级。而几乎不需要处理的应用程序或者线程应该赋予较高的优先级。 使用较低的优先级,自己可以不必把密集占用 C P U的应用程序分割成 C P U处理段,使它在后 台运行。给予实时应用程序较高的优先级,因为它们需要立刻处理所接收的消息,例如键盘 按下或者从串行设备来的消息。 5. 使用光盘时注意 将W z d . c p p中的S e t P r i o r i t y C l a s s ( )改变为较高或者较低的优先级。设置较高的优先级,然 后创建应用程序并运行它。使用 F i l e / O p e n菜单命令打开对话框。接着在系统中运行另外一个 第14章第多第任第务第第335下载应用程序,例如用Windows Explorer来查找文件。在运行中,注意当拖动应用程序的对话框时, 可以暂停其他的应用程序。 14.4 实例56:应用程序内部的多任务— 工作者线程 1. 目标 创建程序线程处理数学运算或者其他需要 C P U集中处理的函数,该线程将单独与应用程 序同时运行。 2. 策略 使用框架的A f x B e g i n T h r e a d ( )函数创建线程。为了在线程完成任务时通知主应用程序,需 创建自己的Wi n d o w s消息,线程在任务完成后将该消息发送给应用程序。 3. 步骤 1) 设置工作者线程 定义用于向线程传递数据的数据结构,一个实例结构可以如下所示: typedef struct t_THREADDATA { HWND hDoneWnd; // window handle of main app to send messages int nData; // data to process } THREADDATA ; 用下述语法编写工作者线程函数。参数 p P a r a m是前面定义的数据结构的指针。参考程序 清单— 工作者线程,可以查看其完整代码列表: UINT WzdThread( LPVOID pParam ) { // get data from thread creator T H R E A D D ATA *pData = ( THREADDATA * )pParam; // do calulations for ( int i = pData -> nData;i < 1000000;i++ ) { } // save data back to thread creator pData -> nData = i; // tell creator we’re done ::SendMessage( pData -> hDoneWnd, WM_DONE, 0, 0 ); return 0; } 返回的值由用户定义。父应用程序可以使用 G e t E x i t T h r e a d ( )检索到这一代码。使用 A f x E n d T h r e a d ( a rg )将终止工作者线程,a rg的值也将由用户自己定义。 2) 创建工作者线程 为创建线程首先使用适当的数据初始化数据结构,然后调用 A f x B e g i n T h r e a d ( )指定上面创 建的线程函数。确保在调用的类中嵌入了数据结构,这样在创建线程以后它可以驻留下来: T H R E A D D ATA m_ThreadData; 336第第第三部分第内部处理实例 下载: : : ThreadData.nData = 123; ThreadData.hDoneWnd = m_hWnd; A f x B e g i n T h r e a d ( W z d T h r e a d , // static thread process declare & m _ T h r e a d D a t a // data to send to thread ) ; 3) 当线程完成任务时通知应用程序 为了在线程完成任务时通知主应用程序,需要创建如下的 Wi n d o w s消息: #define WM_DONE WM_USER + 1 如下所示,当线程完成任务时,发送该消息: // tell creator we’re done ::SendMessage( pData -> hDoneWnd, WM_DONE, 0, 0 ); 为了让应用程序接收该消息,需要手工添加消息处理函数。首先在接收窗口的消息映射 中添加O N _ M E S S A G E _ V O I D ( )消息宏。确保这些代码在 { { } }括号内,否则C l a s s Wi z a r d不会处 理它: BEGIN_MESSAGE_MAP( CWzdVi e w, CView ) // {{AFX_MSG_MAP( CWzdView ) // }}AFX_MSG_MAP ON_MESSAGE_VOID( WM_DONE,OnDone ) E N D _ M E S S A G E _ M A P ( ) 如下添加消息处理函数: void CWzdVi e w : : O n D o n e ( ) { } 确保在类的. h文件中也定义了这个消息处理函数。使用 ON_MESSAGE_VOID() 而不是 O N _ M E S S A G E ( )宏以避免处理后者所需的多余参数。 4. 注意 ■ 由于线程和应用程序占用了相同的地址空间,因此可通过数据结构向线程传递指针。 但是不能够向C W n d结构传递指针。这是因为应用程序跟踪每一个它创建的实例和它指向的窗 口句柄,因而可以立即把 C W n d指针转换为窗口句柄。同时每个线程也跟踪它所创建的窗口。 但是如果向线程传递C W n d对象指针,线程由于没有其窗口句柄的记录,不能为窗口进行转换。 此时可以传递的只能是窗口句柄本身。如果用户愿意,仍然可以在线程内部把窗口指针封装 在C W n d对象中。 ■ 为避免两个线程同时写入相同的数据区域请参考实例 5 9。 ■ A f x B e g i n T h r e a d ( )函数仅仅是一个辅助函数,它创建 C Wi n T h r e a d类的实例,并从线程 中调用用户所提供的函数。当函数返回或者调用 A f x E n d T h r e a d ( )时,辅助程序将调用 ::PostQuitMessage ()终止线程。 ■ 工作者线程的典型用途包括电子数据表格中每一栏的数学计算功能或者文件应用程序 中的后台拼写检查。请参考实例 5 1和5 2了解涉及同步套接字和端口通信的有关内容。 ■ 如果需要一次运行几个线程,用户在定义用于通知应用程序线程已执行完毕其任务的 all done消息中,可能将同时包括线程标识号,为此,需要在消息处理函数中使用 O N _ 第14章第多第任第务第第337下载MESSAGE ()消息宏: L R E S U LT CMainFrame::OnDone( WPARAM wParam,LPARAM lParam ) { return 0; } 5. 使用光盘时注意 当运行随书附带光盘上的工程时,在 W z d v i e w. c p p中的O n Te s t W z d ( )和O n D o n e ( )中设置断 点。单击Te s t和W z d菜单命令。观察如何创建线程和报告线程结束。 #ifndef WZDTHREAD_H #define WZDTHREAD_H #define WM_DONE WM_USER + 1 typedef struct t_THREADDATA { HWND hDoneWnd; int nData; } THREADDATA ; UINT WzdThread( LPVOID pParam ); # e n d i f // WzdThread.cpp : thread process / / #include "stdafx.h" #include "WzdThread.h" / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdThread UINT WzdThread( LPVOID pParam ) { // get data from thread creator T H R E A D D ATA *pData = ( THREADDATA * )pParam; // do calulations for ( int i = pData -> nData;i < 1000000;i++ ) { } // save data back to thread creator pData -> nData = i; // tell creator we’re done ::SendMessage( pData -> hDoneWnd, WM_DONE, 0, 0 ); return 0; // return value up to you—parent can retrieve with GetExitCodeThread(); // can also call AfxEndThread(0) where the meaning of the argument is up to you } 338第第第三部分第内部处理实例 下载14.5 实例57:应用程序内部的多任务— 用户界面线程 1. 目标 创建一个单独执行某功能,且和应用程序同时运行的线程,而且该线程需要自己的用户 界面,例如在文档应用程序中的查询和替换功能。 2. 策略 和创建工作者线程一样,需要使用框架中的 AfxBegin Thread ()函数来创建用户界面线程。 这一次将对线程具有完全控制权,而不是像实例 5 6那样,只具拥有对工作者线程的部分控制 权。因此本例将创建自己的 C Wi n T h r e a d派生线程类。 3. 步骤 1) 创建新的线程类 使用C l a s s Wi z a r d创建C Wi n T h r e a d派生线程类。例如创建无模式对话框的线程类,请参考 程序清单— 用户界面线程类。在本例中创建无模式对话框而不是有模式对话框的原因是, 允许消息从主应用程序连续地转发到线程。 2) 创建用户界面线程 为启动线程可以使用如下代码: C WinThread *pThread = AfxBeginThread( RUNTIME_CLASS( CWzdThread ) ); 线程需要调用: : P o s t Q u i t M e s s a g e ( a rg )来终止,这里的 a rg参数需要用户自己定义。应用程 序为了获得a rg的值,可以调用如下代码: int arg = pThread -> GetExitCodeThread(); 注意 对于应用程序直接结束线程没有推荐的方式。线程必须自己退出并允许将自身清 除 。 用 户 需 要 做 的 是 创 建 W i n d o w s 消 息 来 通 知 线 程 终 止 。 线 程 通 过 调 用::PostQuitMessage (arg)来处理消息。请参考实例58以了解如何向线程发送消息。 4. 注意 ■ 工作者线程倾向于琐碎的处理,与它不同的是,用户界面线程具有自己的界面而且实 际上类似于运行其他应用程序。创建线程而不是其他应用程序的好处是线程可与应用程序共 享程序空间,这样可以简化线程与应用程序共享数据的功能。 ■ 典型情况是用户界面线程用于完成查询和替换等功能,或者是其他不希望占用主应用 程序大量处理时间但是需要一个界面的功能或服务,或者用户也可完全不考虑界面,将这种 类型的线程用于窗口消息服务器作为一种传递其消息的方式,以避免使自己因占用处理时间 过多而陷入困境。 ■ 在时间要求严格的应用程序 (例如实时应用程序 )中,不希望因为工作者线程启动而等 待,这时可将工作者线程中的控制逻辑内置到用户界面线程中并提前创建线程。当需要处理 事务时,向用户界面线程发送消息,此时用户界面线程已经运行并且在等待指令。 5. 使用光盘时注意 运行随书附带光盘上的工程时,单击 Te s t和W z d菜单命令,创建包含无模式对话框的线 程。 6. 程序清单— 用户界面线程类 #if !defined( AFX_WZDTHREAD_H__411 A E 4 C 2 _ E 5 1 5 _ 11D1_9B80_00AA003D8695__INCLUDED_ ) 第14章第多第任第务第第339下载#define AFX_WZDTHREAD_H__411 A E 4 C 2 _ E 5 1 5 _ 11 D 1 _ 9 B 8 0 _ 0 0 A A 0 0 3 D 8 6 9 5 _ _ I N C L U D E D _ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 // WzdThread.h : header file / / #include "WzdDialog.h" / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdThread thread class CWzdThread : public CWi n T h r e a d { D E C L A R E _ D Y N C R E ATE( CWzdThread ) p r o t e c t e d : CWzdThread(); // protected constructor used by dynamic creation // Attributes p u b l i c : // Operations p u b l i c : // Overrides // ClassWizard generated virtual function overrides // {{AFX_VIRTUAL( CWzdThread ) p u b l i c : virtual BOOL InitInstance(); virtual int ExitInstance(); // }}AFX_VIRT U A L // Implementation p r o t e c t e d : virtual ~CWzdThread(); // Generated message map functions // {{AFX_MSG( CWzdThread ) // NOTE - the ClassWizard will add and remove member functions here. // }}AFX_MSG D E C L A R E _ M E S S A G E _ M A P ( ) p r i v a t e : CWzdDialog m_dlg; } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // {{AFX_INSERT _ L O C AT I O N } } // Microsoft Developer Studio will insert additional declarations immediately 340第第第三部分第内部处理实例 下载// before the previous line. # e n d i f // !defined( AFX_WZDTHREAD_H__411 A E 4 C 2 _ E 5 1 5 _ 11D1_9B80_00AA003D8695__INCLUDED_ ) // WzdThread.cpp : implementation file / / #include "stdafx.h" #include "wzd.h" #include "WzdThread.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdThread I M P L E M E N T _ D Y N C R E ATE( CWzdThread, CWinThread ) C W z d T h r e a d : : C W z d T h r e a d ( ) { } C W z d T h r e a d : : ~ C W z d T h r e a d ( ) { } BOOL CWzdThread::InitInstance() { m_dlg.Create( IDD_WZD_DIALOG ); m _ d l g . S h o w Window( SW_SHOW ); m_pMainWnd = &m_dlg; return TRUE; // can end thread by returning FALSE here } int CWzdThread::ExitInstance() { m _ d l g . D e s t r o y Wi n d o w ( ) ; return CWi n T h r e a d : : E x i t I n s t a n c e ( ) ; } BEGIN_MESSAGE_MAP( CWzdThread, CWinThread ) // {{AFX_MSG_MAP( CWzdThread ) // NOTE - the ClassWizard will add and remove mapping macros here. // }}AFX_MSG_MAP E N D _ M E S S A G E _ M A P ( ) 第14章第多第任第务第第341下载14.6 实例58:向用户界面线程发送消息 1. 目标 向用户界面线程发送消息。 2. 策略 一般通过使用C W n d : : S e n d M e s s a g e ( )来发送消息。但是只能使用 S e n d M e s s a g e ( )向窗口发 送消息,而不是线程。因此本例中使用了另外一个函数 C Wi n T h r e a d::P o s t T h r e a d M e s s a g e ( ) 来发送消息。P o s t T h r e a d M e s s a g e ( )允许直接访问线程类中的消息映射。在本例中,将创建自 定义的窗口消息,它会导致用户界面线程终止。 注意 仍然可以用SendMessage ()向线程发送消息,但仅限于线程创建的窗口。 3. 步骤 向线程发送消息 如下所示定义窗口消息: #define WM_WZDKILLTHREAD WM_USER + 1 按通常方式启动用户界面线程,但保留一个指向由它创建的 C Wi n T h e a d对象的指针: m_pThread = AfxBeginThread( RUNTIME_CLASS( CWzdThread ) ); 向线程发送消息,如下所示使用 P o s t T h r e a d M e s s a g e ( ): m_pThread -> PostThreadMessage( WM_WZDKILLTHREAD, 0, 0 ); 为了让线程能截取消息,在线程类的消息映射中手工添加下列消息映射宏,确保在 { { } } 括号外边添加代码,以便C l a s s Wi z a r d不会被它弄糊涂: BEGIN_MESSAGE_MAP( CWzdThread, CWinThread ) // {{AFX_MSG_MAP( CWzdThread ) // }}AFX_MSG_MAP ON_THREAD_MESSAGE( WM_WZDKILLTHREAD,OnKillThread ) E N D _ M E S S A G E _ M A P ( ) 使用下列语法编写消息处理函数。在本例中,还要调用 : : P o s t Q u i t M e s s a g e ( )函数来终止线 程: L R E S U LT CWzdThread::OnKillThread( WPARAM wParam,LPARAM lParam ) { ::PostQuitMessage( 0 ); return 0; // returned to PostThreadMessage() } 同时确保在线程类的头文件中定义了这个消息处理函数。 4. 注意 ■ 由于不能访问工作者线程的消息映射,因此不能使用 P o s t T h r e a d M e s s a g e ( )向它发送消 息。通过传递给它的结构中的数据单元,可以非正式地与工作者线程通信。q例如,用户可以 在这个结构中包含b K i l l标记,当由应用程序设置该标志时将导致线程过早地异常中断。 ■ 用户界面线程能够有选择地截获已注册消息 (有关已注册消息,请参考实例 4 9 )。为处 理注册消息,可使用以下消息宏: ON_REGISTER_THREAD_MESSAGE( registered_message_id, process ) 342第第第三部分第内部处理实例 下载由于线程被认为是创建应用程序中的一部分,考虑到每个注册消息总是要占用一定资源, 因此使用已注册消息与用户界面线程通信将可能会导致它们负担过重。 5. 使用光盘时注意 当运行随书附带光盘上的工程时,单击 Te s t和W z d菜单命令,创建包含无模式对话框的线 程。接着单击E n d向线程发送消息来结束线程。 14.7 实例59:线程间的数据共享 1. 目标 在线程之间进行应用程序数据共享。同时避免由于两个线程同时访问相同的数据而引发 的冲突。 2. 策略 使用三种M F C类:C M u t e x、C S i n g l e L o c k和C M u l t i L o c k来同步多个线程对一个数据类的 同时访问。 3. 步骤 数据对象防火墙 在线程中确定共享的数据类。在每个类定义中嵌入 C M u t e x对象,如下所示: class CWzdData : public CObject { : : : // synchronization protection CMutex m_mutex; : : : } ; 如果数据类没有访问其数据的成员函数,这一步将添加它们。这些函数如下所示: void CWzdData::GetData( int *pInt,float *pFloat,DWORD *pWord ) { *pInt = m_nInt; *pFloat = m_fFloat; * p Word = m_dwWo r d ; } void CWzdData::SetData( int nInt,float fFloat,DWORD dwWord ) { m_nInt = nInt; m_fFloat = fFloat; m _ d w Word = dwWo r d ; } 在引用已嵌入 C M u t e x变量的 S e t D a t a ( )函数堆栈上创建 C S i n g l e L o c k类的实例。使用 C S i n g l e L o c k的L o c k ( )函数避免在函数内部对数据多重访问,如下所示: BOOL CWzdData::SetData( int nInt,float fFloat,DWORD dwWord ) { CSingleLock slock( &m_mutex ); if ( slock.Lock( 1000 ) ) // timeout in milliseconds, 第14章第多第任第务第第343下载// default = INFINITE { // set values—can also be lists and arrays m_nInt = nInt; m_fFloat = fFloat; m _ d w Word = dwWo r d ; return TRUE; } return FA L S E ; // timed out! // unlocks on return or you can call slock.Unlock(); } 如果其他的线程同时访问这个数据, L o c k ( )将立刻返回。否则,L o c k ( )在指定的毫秒数内 等待,直到超时并返回FA L S E。 如果在这个类中保存的数据与其他类中保存的数据相关,则在两个类中嵌入 C M u t e x变量, 两边都用C M u l t i L o c k等待,如下所示: CMutex mutex[2]; mutex[0] = &mutex1; mutex[1] = &mutex2; CMultiLock mlock( mutex,2 ); // where 2 is the number of mutexes if ( mlock.Lock( 1000 ) ) { } 要查看经过这些修改后的数据类实例,请参考本实例结尾的程序清单— 实例数据类。 4. 注意 ■ 要了解更多的有关 C M u t e x和相关类的知识,请参阅第 1章。C M u t e x使用了实例 3中 的: : C r e a t e M u t e x ( ) Windows API以避免若干应用程序的实例同时运行。 : : C r e a t e M u t e x ( )函数的 功能并不仅仅只是追踪应用程序的实例。在该实例中只是简单使用其中的部分功能。 ■ M u t e x实际应用的例子可参考实例 5 1。通过Wi n d o w s套接字线程,C O b L i s t可以安全地 实现多重访问。 5. 使用光盘时注意 当运行随书附带光盘上的工程时,在 W z d D a t a . c p p的G e t D a t a ( )中设置断点。单击 Te s t和 W z d菜单命令,观察三个线程如何试图访问同一个数据。 6. 程序清单— 数据类 #ifndef WZDDATA _ H #define WZDDATA _ H #include class CWzdData : public CObject { p u b l i c : DECLARE_SERIAL( CWzdData ) C W z d D a t a ( ) ; 344第第第三部分第内部处理实例 下载BOOL GetData( int *pInt,float *pFloat,DWORD *pWord ); BOOL SetData( int nInt,float fFloat,DWORD dwWord ); // synchronization protection CMutex m_mutex; // result data int m_nInt; float m_fFloat; DWORD m_dwWo r d ; } ; # e n d i f // WzdData.cpp : implementation of the CWzdData class / / #include "stdafx.h" #include "WzdData.h" / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdData IMPLEMENT_SERIAL( CWzdData, CObject, 0 ) C W z d D a t a : : C W z d D a t a ( ) { m_nInt = 0; m_fFloat = 0.0f; m _ d w Word = 0; } BOOL CWzdData::GetData( int *pInt,float *pFloat,DWORD *pWord ) { // we lock here too so that we’ll never read half written data CSingleLock slock( &m_mutex ); if ( slock.Lock( 1000 ) ) // timeout in milliseconds, default = INFINITE { // get values—can also be lists and arrays *pInt = m_nInt; *pFloat = m_fFloat; * p Word = m_dwWo r d ; return TRUE; } return FALSE; // timed out! // unlocks on return or you can call slock.Unlock(); } 第14章第多第任第务第第345下载BOOL CWzdData::SetData( int nInt,float fFloat,DWORD dwWord ) { CSingleLock slock(&m_mutex); // or with CMultiLock can specify several // m_mutex’s for waiting on several // data items if ( slock.Lock( 1000 ) ) // timeout in milliseconds, // default = INFINITE { // set values—can also be lists and arrays m_nInt = nInt; m_fFloat = fFloat; m _ d w Word = dwWo r d ; return TRUE; } return FALSE; // timed out! // unlocks on return or you can call slock.Unlock(); } 346第第第三部分第内部处理实例 下载下载下载 第15章 其 他 本章收集了其他一些有用的实例,范围从使用定时器到闪烁一个窗口等。这些实例包括: 实例60 创建定时器,本例将演示如何使用 MFC 类创建定时事件。 实例61 播放声音,本例将演示在应用程序中播放 . w a v文件。 实例62 创建V C + +宏,本例将回顾由M F C支持的宏语法,它可作为编写小函数或者内联 函数的一种选择。 实例63 使用函数地址,本例将演示如何传递类的成员函数的地址,实现间接调用。 实例64 处理二进制字符串,本例中将扩展 M F C的字符串类使其可以处理二进制的字 符串。 实例65 重新启动计算机,在本例中将看到如何编写程序重新启动系统。 实例66 获得可用磁盘空间,本例将演示如何检测磁盘上的剩余空间。 实例67 闪烁窗口和文本, 本例将演示如何闪烁文本信息或者窗口的标题来获得特殊效果。 15.1 实例60:创建定时器 1. 目标 了解已经过去了多少秒的时间。 2. 策略 使用C W n d : : S e t Ti m e ( )函数创建定时器。C W n d : : S e t Ti m e ( )函数有两个方式:第一个方式允 许指定静态函数在定时器超时的情况下调用;第二个模式向调用 S e t Ti m e ( )的窗口发送 W M _ T I M E R窗口消息,这个消息像其他 Wi n d o w s消息一样被相应处理。 3. 步骤 1) 创建定时器,指定时间后调用一静态函数 通过使用下面的语法定义定时器回调函数,这个函数将作为类的静态函数: p r i v a t e : static void CALLBACK EXPORT TimerProc( HWND hWnd, UINT nMsg, UINT nIDEvent, DWORD dwTime ); 使用如下代码创建具有函数名的定时器: S e t Ti m e r ( 3 , // event ID, passed to time out process 1 0 0 0 , // time out time in milliseconds C W z d Vi e w : : Ti m e r P r o c // timeout process called ) ; 在这个实例中, 1秒以后调用 C W z d Vi e w : : Ti m e r P r o c ( )函数。事件标识号“ 3”被发送到 Ti m e r P r o c ( )。 实现定时功能,如下所示: void CALLBACK EXPORT CWzdVi e w : : TimerProc( HWND hWnd, UINT nMsg,UINT nIDEvent, DWORD dwTime ) { // hWnd will be this window // nMsg will be WM_TIMER // nIDEvent will be the event id specified in SetTi m e r // dwTime will be the current system time } 2) 创建当时间超时时发送W M _ T I M E R消息的定时器 启动定时器,当定时终止时向窗口类发送 W M _ T I M E R窗口消息,可以做如下调用: S e t Ti m e r ( 4 , // event ID 2 0 0 0 , // time in milliseconds N U L L // causes a WM_TIMER message to be sent to this window ) ; 在这个实例中,W M _ T I M E R消息在2秒钟以后被发送到窗口,它使用事件标识号“ 4”。 可使用C l a s s Wi z a r d向窗口类中添加WM_TIMER 消息。创建的消息处理函数如下所示: void CWzdVi e w : : O n Timer( UINT nIDEvent ) { // nIDEvent will be the event id specified in SetTi m e r C Vi e w : : O n Timer( nIDEvent ); } 3) 取消定时器 1) 要在超时之前取消定时器,可使用 C W n d : : K i l l Ti m e r,如下所示: UINT timerID = SetTimer( 3,3000,NULL ); K i l l Ti m e r ( t i m e r I D // timer id returned from SetTi m e r ( ) ) ; 注意本例中是通过使用S e t Ti m e r ( )返回的标识号来识别要取消的定时器。 4. 注意 ■ 创建一个重复计时的定时器没有简单的方法。每一次都必须在定时器超时后调用 C W n d : : S e t Ti m e r ( )函数。 ■ 定时器将占用系统资源。如果应用程序需要多个定时器,最好是复用一个简单的系统 定时器。首先确定希望跟踪的最小时间单元,然后当应用程序的某一部分试图创建定时器时, 使它在相关列表中创建一个项目 ( e n t r y )。该项目中应该指定它所表示的系统时钟的倍数值, 以及当时间超时事件发生时向何处通知该消息 (指定W M _ T I M E R消息的窗口句柄或者要调用 的函数地址)。接下来创建以最小时间单元作为超时时限的定时器。一旦时间超时将复位该定 时器,同时为列表中所有项目中的时间计数器递增一个计数单元,直到它们超时为止。当列 表中某一个项目时间超时,将向该项目中指定的窗口指针发送 WM_TIMER 消息,或者调用 其中指定的函数地址。 5. 使用光盘时注意 运行随书附带光盘上的工程时,在 O n Te s t W z d ( )函数中设置断点。单击 Te s t和W z d菜单命 令,观察两个定时器的创建和超时过程。 348第第第三部分第内部处理实例 下载15.2 实例61:播放声音 1. 目标 使应用程序播放声音。 2. 策略 使用两个Windows API在程序中播放声音。其中 ::Beep() API创建能够设置频率和持续时 间的单音(single tone)。::sndPlaySound() API允许应用程序播放.w a v文件。 3. 步骤 1) 生成单音 为在应用程序中创建单音,可使用以下代码: : : B e e p ( 1 0 0 0 , // sound frequency in hertz 1 0 0 0 // sound duration in milliseconds ) ; 第一个参数是音调的频率,赫兹数值越高音调越高 (人耳可以听到的声音的范围是从 2 0赫 兹到2 0 0 0 0赫兹)。第二个参数是以毫秒计的持续时间。 w a v文件是数字化的音频文件。为了播放. w a v文件,系统必须有声卡。用户必须在应用程 序中手工添加M F C的多媒体支持。 2) 播放. w a v文件 为了包含M F C对. w a v文件的支持,在每个将调用该 A P I的模块中加入下面的语句: #include 同时为应用程序加入w i n m n . l i b到连接设置。 从磁盘文件播放. w a v文件,使用代码如下: : : s n d P l a y S o u n d ( " s o u n d . w a v " , // file to play SND_SYNC | // or SND_ASYNC to play in another thread SND_LOOP | // play continually (SND_ASYNC must be set too) S N D _ N O D E FA U LT // play nothing if error in .wav file ) ; 停止播放. w a v文件,可使用以下代码: ::sndPlaySound(" ", 0); 3) 在应用程序的资源中包含. w a v文件 如果希望在应用程序的资源中包含 . w a v文件,单击Developer Studio的I n s e r t和R e s o u r c e菜 单命令,打开Insert Resource对话框。单击I m p o r t然后找到并选择希望包含的 . w a v文件。一个 WAV E文件夹将自动在资源中创建,同时 . w a v文件被添加和拷贝到工程的 \ r e s目录中。 为播放定义于应用程序资源中的 . w a v文件,可以使用下面的代码。在这个实例中,使用 标识符I D R _ S O U N D识别. w a v文件。 // find .wav file name in resources and play HRSRC hRsrc = FindResource( AfxGetResourceHandle(), MAKEINTRESOURCE( IDR_SOUND ), "WAV" ); HGLOBAL hglb = LoadResource( AfxGetResourceHandle(), hRsrc ); ::sndPlaySound( ( LPCTSTR )::LockResource( hglb ), 第15章第其第第他第第349下载S N D _ M E M O RY | SND_SYNC | S N D _ N O D E FA U LT ); FreeResource( hglb ); 4. 注意 需要声卡、麦克风和多媒体软件才能创建自己的 . w a v文件。 5. 使用光盘时注意 运行随书附带光盘上的工程时,在 O n Te s t W z d ( )函数中设置断点。单击 Te s t和W z d菜单命 令,注意生成的三种声音。 15.3 实例62:创建VC++宏 1. 目标 充分利用V C + +的功能来创建宏。 2. 策略 简单回顾一下 M F C支持的宏。C + +试图以单纯的内联代码来代替宏,然而许多情况下必 须用宏实现而无法使用内联代码。 3. 步骤 1) 创建简单的宏定义 使用下面的语法来定义宏: #define WZD1 7 // WZD1 is substituted for 7 when used in code #define WZD2 // WZD2 is simply defined so that #ifdef WZD2 is TRUE 2) 创建条件宏指令 使用下面的语法,对代码行进行条件编译: #ifdef WZD1 // if defined, process next #undef WZD1 // undefine WZD1 # e n d i f #ifndef WZD1 // if WZD1 not defined, process next # e n d i f #if WZD2 = 5 // if WZD2 equals 5, process next / / #elif WZD2 > 6 // else if WZD6 is greater then 6 / / # e l s e // else... / / # e n d i f 3) 创建带参数的宏指令 带参数的简单宏的语法如下: #define WZD3( arg1,arg2 ) \ arg1 + arg2; // WZD3( 2,5 ) is substituted for 2+5 in the code 当使用续行符(“\”)时,本行内续行符的后面必须没有任何字符,即使是空格也不行。 为使宏的参数带双引号出现,可在宏参数之前田间加符号“ #”: #define WZD4( arg1,arg2 ) \ arg1 + #arg2; // WZD4( 1,test ) becomes 1 + "test" in the code 350第第第三部分第内部处理实例 下载为使宏的参数带单引号出现,可在宏参数之前加符号“ # @”: #define WZD5( arg1,arg2 ) \ arg1 + #@arg2; // WZD5( 1,t ) becomes 1 + 't' 为了在宏的标记( t o k e n )中添加宏参数,可在标记之前加符号“ # #”: #define WZD6( arg1,arg2 ) \ arg1 + arg2##3; // WZD6( 1,2 ) becomes 1 + 23 4) 使用预定义宏 V C + +编译器预定义四个宏,允许在代码中插入有关源文件的信息。当处理到包含源文件 名的文本字符串时, _ F I L E _宏指令展开( F I L E前后各有一个下划线 )。例如,在W z d View .cpp 文件的下列代码: Print( __FILE__ ); 当编译时,展开为: Print( "WzdVi e w.cpp" ); _ L I N E _展开为源文件中的当前行。 _ T I M E _和_ D AT E _宏展开为源文件编译时的日期和时 间。 4. 注意 _ F I L E _和_ L I N E _宏用于更有效地确定出错信息。例如,与其仅仅在应用程序里显示下列 出错信息: "File not found" 倒不如在代码中包含发生错误的确切位置。如下面一行代码所示: "File not found(_FILE_@_LINE_)" 在编译时,将扩展成: "File not found(WzdVi e w. c p p @ 1 2 3 2 ) " 5. 使用光盘时注意 随书附带的光盘上没有相应的实例。 15.4 实例63:使用函数地址 1. 目标 向其他应用程序传递成员函数的地址,用于执行或保存。 2. 策略 ANSI C 允许像对待其他变量一样对待函数的地址。但是在 C语言中的C + +对象中的函数是 非静态的,这意味着不但必须保持函数的地址,而且必须保存函数所在对象的地址。 在这个实例中,将向其他应用程序传递 C W z d Vi e w类的成员函数的地址。其他的函数将 间接地调用这个函数。 int CWzdVi e w : : ViewFunc( int i,LPCSTR s,BOOL b ) { i;s;b; // process values return 0; // result is returned } 第15章第其第第他第第351下载如上所示,Vi e w F u n c ( )函数包含三个参数并返回整数值。 3. 步骤 为成员函数定义函数指针类型,如下所示: typedef int ( CWzdView::*PMFUNC)(int, LPCSTR, BOOL ); 在这个实例中, P M F U N C成为函数指针类型。将该定义放在容易加入到代码的地方,例 如每个代码模块所包含的文件中。 当向这个函数传递指针的时候,还需要包含它指向其所在类的实例的指针: void Perform( CWzdView *pClass, PMFUNC pFunc ); 在这个实例中, P M F U N C驻留在 C W z d Vi e w类中。因此可以使用下面的参数调用 P e r f o r m ( )。 w z d U t i l . P e r f o r m ( x x x , // a pointer to the object that ViewFunc() sits in p F u n c // the pointer to Vi e w F u n c ( ) ) ; 为间接调用这个成员函数,需要如下结合对象和成员函数指针: // pClass is the object pointer; pFunc is the function pointer i = ( pClass -> *func )( 1,"test",TRUE ); 4. 注意 ■ 可以在C + +类中封装指针对(例如:O b j e c t : : F u n c t i o n ),如下所示: class CFunc { CFunc( CWzdView *pClass, PMFUNC func ) {m_pClass = pClass;m_pFunc = pFunc;}; classPtr *m_pClass; funcPtr *m_pFunc; Perform( int i,LPCSTR s,BOOL b ){( PClass -> *func )( i,s,b );}; } 这种方法的好处是成员函数可以作为简单的参数被传递和存储,缺点是需要一个用于各 种函数类型的封装类。还需要考虑删去多余的类实例。有时候隐藏间接调用的内部细节可能 比其所具有的简单性更易使人混淆。 ■ 本实例与其说是M F C的练习倒不如说是与 C + +的联系。既然它属于 C + +中比较晦涩难 懂的一部分内容,这里解释一下还是有必要的。 5. 使用光盘时注意 当运行随书附带光盘的工程时,在 O n Te s t W z d ( )函数中设置断点。单击 O p t i o n和W z d菜单 命令,观察一个类实例的函数被间接调用。 15.5 实例64:二进制字符串 1. 目标 使用M F C的字符串类操作二进制字符串。 2. 策略 352第第第三部分第内部处理实例 下载M F C的C S t r i n g类允许操作以零字符终止的文本字符串。为了使它也能对可能带有许多零 字符的二进制字符串进行操作,本例将把二进制字符串转化且保存为等值的十六进制文本字 符串。换句话说,字节 0 x 2 3、0 x 4 3、0 x a 4在C S t r i n g中将被保存为文本字符串“ 2 3 4 3 a 4”。 从C S t r i n g类派生出自己的类,在其中添加两个新的成员函数,它们将转换并保存二进制 字符串为文本字符串。 3. 步骤 如本实例结尾的程序清单— 字符串类所示,手工创建 C S t r i n g的派生类。C l a s s Wi z a r d不 能创建从C S t r i n g派生的类。 使用文本编辑器在该类中添加 P u t B i n a r y ( )函数。由于C S t r i n g只能够在内部保存为字符串 文本,因此需要将二进制字符串转化为文本表示。为此使用 C S t r i n g的F o r m a t ( )函数,将二进 制字节转化为一个双字符的十六进制数值: void CWzdString::PutBinary( LPBYTE pByte,int len ) { E m p t y ( ) ; CString hex; for ( int i = 0;i < len;i++ ) { hex.Format( "%02X",pByte[i] ); *this += hex; } } 该类中的另外一个成员函数是 G e t B i n a r y ( ),用于从C S t r i n g类重新生成二进制字符串。使 用s s c a n f ( )可将十六进制字符串重新转换二进制字符串,如下所示: // returned value is actual binary length int CWzdString::GetBinary( LPBYTE pByte,int maxlen ) { // make sure the string contains only valid hex characters if ( SpanIncluding( "0123456789aAbBcCdDeEfF" ) != *this ) { return 0; } // make sure less then max bytes int len = GetLength(); if ( len > maxlen*2 ) { len = maxlen*2; } // pad HEX to even number CString hex = *this; if ( ( len % 2 ) != 0 ) { l e n + + ; hex += "0"; } 第15章第其第第他第第353下载// convert to binary len/ = 2; for ( int i = 0; i < len; i++ ) { int b; sscanf( hex.Mid( ( i * 2 ), 2 ), "%02X", &b ); pByte[i] = ( BYTE )b; } return( len ); } 4. 注意 使用s s c a n f ( )函数时应小心。在上面的实例中,即使所希望的是字节,也需要在整型变量 中扫描搜索。这是因为无论如何 s s c a n f ( )都返回整数值。如果希望在字节中搜索,当 s s c a n f ( )销 毁堆栈时应用程序可能将不时地发生莫名其妙地崩溃。 5. 使用光盘时注意 当运行随书附带光盘的工程时,在 W z d Vi e w. c p p里的O n W z d Ty p e ( )函数中设置断点。单击 O p t i o n和W z d菜单命令,观察二进制字符串被操作。 6. 程序清单— 字符串类 // CWzdString.h / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / #if !defined WZDSTRING_H #define WZDSTRING_H class CWzdString : public CString { p u b l i c : CWzdString::CWzdString() : CString(){;}; CWzdString::CWzdString( const CString& str ) : CString( str ){;}; CWzdString::CWzdString( LPCTSTR lpsz ) : CString( lpsz ){;}; CWzdString::CWzdString( LPCWSTR lpsz ) : CString( lpsz ){;}; // Attributes p u b l i c : // Operations p u b l i c : void PutBinary( LPBYTE pByte,int len ); int GetBinary( LPBYTE pByte,int maxlen ); } ; / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / # e n d i f 354第第第三部分第内部处理实例 下载// WzdString.cpp / / #include "stdafx.h" #include "WzdString.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; # e n d i f / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / // CWzdString // extracts characters from beg to end CString CWzdString::Extract( int beg,int end) { return Mid( beg - 1,end - beg + 1 ); } // stores binary as HEX ascii string void CWzdString::PutBinary( LPBYTE pByte,int len ) { E m p t y ( ) ; CString hex; for ( int i = 0;i < len;i++ ) { hex.Format( "%02X",pByte[i] ); *this+ = hex; } } // retrieves HEX ascii string as binary // returned value is actual binary length int CWzdString::GetBinary( LPBYTE pByte,int maxlen ) { // make sure the string contains only valid hex characters if ( SpanIncluding( "0123456789aAbBcCdDeEfF" ) != *this ) { return 0; } // make sure less then max bytes int len = GetLength(); if ( len > maxlen*2 ) { len = maxlen*2; } // pad HEX to even number 第15章第其第第他第第355下载CString hex = *this; if ( ( len % 2 ) != 0 ) { l e n + + ; hex += "0"; } // convert to binary len/ = 2; for ( int i = 0; i < len; i++ ) { int b; sscanf( hex.Mid( ( i * 2 ), 2 ), "%02X", &b ); pByte[i] = ( BYTE )b; } return( len ); } 15.6 实例65:重新启动计算机 1. 目标 编写程序重新启动计算机系统,无论是本地系统或者是其他远程系统: 2. 策略 为关闭自己的系统,将使用 : : E x i t WindowsEx() API。关闭其他的系统则使用 : : I n i t i a t e System ShutDown () API。事实上,该A P I也会关闭自己的系统,甚至允许设置关闭的延迟时 间。在关闭系统时要考虑的另一种情况是运行 N T的计算机需要 S E _ S H U T D O W N _ P R I V I L E D G E。本例将使用: : A d j u s t To k e n P r i v i l e d g e ( )来获取此特权。 3. 步骤 1) 改变N T的特权 为重启动运行N T的计算机,需要给予应用程序关闭系统的特权: static HANDLE hTo k e n ; static TOKEN_PRIVILEGES tp; static LUID luid; if ( ::OpenProcessToken( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TO K E N _ Q U E RY, &hToken ) ) { : : L o o k u p P r i v i l e g e Value( NULL, SE_SHUTDOWN_NAME, &luid ); tp.PrivilegeCount = 1; tp.Privileges[0].Luid = luid; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; : : A d j u s t TokenPrivileges( hToken, FALSE, &tp, sizeof( TOKEN_PRIVILEGES ), NULL, NULL ); } 注意 用户的密码帐户必须能够赋予用户关闭系统的特权。 注意 建议添加以上代码,即使应用程序运行在其他的系统之上,例如 Windows 98。 356第第第三部分第内部处理实例 下载(不会损害系统)。 2) 重启动本地计算机 为重启动本地计算机,使用以下代码: : : E x i t Wi n d o w s E x ( // EWX_LOGOFF // logs user off // EWX_SHUTDOWN // shuts down system E W X _ R E B O O T // reboots system // EWX_POWEROFF // shuts down and turns system off // (if system has power-off feature) // |EWX_FORCE, // forces processes to terminate -- // use only in emergency 0 // reserved ) ; 3) 重启动本地或者远程计算机 重启动本地或者远程计算机,使用以下代码: : : I n i t i a t e S y s t e m S h u t d o w n ( " \ \ \ \ O t h e r M a c h i n e " , // computer to shut down- - // NULL = local system " G o o d b y e ! " , // a message to user 6 0 , // time to display message (in seconds) FA L S E , // TRUE = force processes to terminate T R U E // TRUE = reboot after shutdown ) ; 取消由: : I n i t i a t e S y s t e m S h u t d o w n ( )引发的重启动行为,使用以下代码: : : A b o r t S y s t e m S h u t d o w n ( " \ \ \ \ O t h e r M a c h i n e " // computer NOT to shut down- - // NULL = local system ) ; 4. 注意 远程A P I允许当远程服务器挂起或者安装新软件时,重新启动远程计算机。 5. 使用光盘时注意 当运行随书附带光盘上的工程时,单击 Te s t菜单项。第一个菜单命令立即启动计算机。第 二个命令在一秒钟以后重启动计算机,最后的命令则取消由第二条命令引发的重启动。 15.7 实例66:获得可用磁盘空间 1. 目标 检查磁盘驱动器上剩余的空间。 2. 策略 使用::GetDiskFreeSpaceEx() API。 3. 步骤 获取可用磁盘空间 使用下面的调用来获取可用磁盘空间: ULARGE_INTEGER freeTo C a l l e r ; 第15章第其第第他第第357下载ULARGE_INTEGER diskCapacity; ULARGE_INTEGER freeSpace; : : G e t D i s k F r e e S p a c e E x ( "c:\\", // pointer to the directory name & f r e e To C a l l e r, // bytes available to caller & d i s k C a p a c i t y, // total bytes on disk &freeSpace // free bytes on disk ) ; 4. 注意 对于Windows 95系统,需要O E M 2或者更高版本以使用该函数。如果应用程序不能使用 该函数,仍然可以使用: : G e t D i s k F r e e S p a c e ( ),但是必须根据返回的值计算可用空间。 5. 使用光盘时注意 当运行随书附带光盘上的工程时,单击 Te s t和W z d菜单项,打开对话框。按下按钮获得 C 盘的当前可用空间。 15.8 实例67:闪烁窗口和文本 1. 目标 在对话框中闪烁文本信息或者闪烁对话框的标题以吸引用户的注意力。 2. 策略 有两种方法可实现闪烁,并且都涉及到使用定时器来间歇地修改窗口。以文本为例,只 需要隐藏和显示它所在的窗口。以弹出窗口和重叠窗口为例,使窗口有效或者无效便可闪烁 标题栏。这种方式通常在出错或者提示消息时使用,以此吸引用户的注意。 3. 步骤 1) 创建闪烁文本 启动定时器(参考实例6 0 )。如果是在对话框类中,则在 O n I n i t D i a l o g消息处理函数中启动 定时器: S e t Ti m e r ( 1 , // event ID 2 5 0 , // time in milliseconds N U L L // causes a WM_TIMER message to be sent to this window ) ; 添加W M _ T I M E R消息处理函数,交替隐藏和显示文本: void CWzdDialog::OnTimer( UINT nIDEvent ) { if ( nIDEvent = 1 ) { static BOOL t = TRUE; m _ c t r l S t a t i c . S h o w Window( ( t = !t )?SW_SHOW:SW_HIDE ); } C D i a l o g : : O n Timer( nIDEvent ); } 注意通过使用nIDEvent 标识号,仍然可以在对话框类中将定时器消息处理函数用于其他 358第第第三部分第内部处理实例 下载目的。 关闭定时器时,确保文本可见。 2) 创建闪烁窗口 如前面所示启动定时器。 添加W M _ T I M E R消息处理函数,使用 F l a s h Wi n d o w s ( )使窗口标题栏在活动和非活跃两种 状态之间切换: void CWzdDialog::OnTimer( UINT nIDEvent ) { F l a s h Wi n d o w ( TRUE // if FALSE, forces caption bar to be active- - // useful when flashing stops and // you want bar to be active ) ; C D i a l o g : : O n Timer( nIDEvent ); } 当停止闪烁时,确保调用F l a s h Wi n d o w ( FA L S E ),以保证窗口标题条处于活动状态的 (标题 加亮显示)。 4. 注意 可以使用这种方法闪烁主窗口,如果应用程序在后台出了问题而用户正忙于其他的事情, 甚至如果主窗口在桌面上不可见,任务栏中表示该应用程序的项目也会闪烁。 5. 使用光盘时注意 当运行随书附带光盘上的工程时,单击 Te s t和W z d菜单项打开对话框。按下文本闪烁按钮 使文本闪烁或者按下窗口闪烁按钮使窗口标题栏闪烁。 第15章第其第第他第第359下载下载下载 附录A 消息和重载顺序 在第一章中,对 M F C基础特征及其封装 Windows API机制进行了回顾,并重点讨论了窗 口如何创建、窗口间如何进行消息发送, M F C应用程序由什么类组成,以及如何使用 C l a s s Wi z a r d来重载并以此增强程序的处理能力。在该章中未深入涉及错误重载的有关内容, 这也是一个导致编程失败的重要因素和来源,即使对一个熟练的 M F C程序员而言也是如此。 换句话说,错误重载包括为错误的消息添加了窗口消息处理函数或者重载了错误的 M F C类函 数等等。为避免出现错误重载,在此需要了解消息发送、函数调用以及应用程序中各种行为 的发生顺序,包括: • 创建、移动、显示和关闭窗口。 • 创建和关闭有模式对话框或者无模式对话框。 • 创建和关闭对话框应用程序。 • 创建和关闭M D I应用程序以及装载和保存M D I文档。 • 创建和关闭M D I应用程序以及装载和保存M D I文档。 在本附录中将把以上行为所涉及到的消息和重载归纳成文档。如果附录中的下列实例都 不适用于读者自己的应用程序,可以自行使用 C l a s s Wi z a r d添加并重载每个可能的消息处理函 数,然后将它们加入到受其影响的类中,并设置断点,通过这种方式来创建自己的事件发生 序列表。 注意 这些事件序列极少归档。可能的一个原因在于它们经常随着不同 M F C版本而经 常修改。一般情况下本附录所述的序列表很少被修改。 A.1 窗口 这一节中将列出在创建、显示和关闭任何类型的窗口 (包括重叠窗口、弹出窗口和子窗口 ) 的情况下用到的消息和所调用的函数。 注意 如果窗口正在由对话框创建,例如一个控件,则消息以WM_CTLCOLOR开始。 A.1.1 创建窗口 一旦调用了C W n d的C r e a t e E x ( )函数,便开始消息发送和函数的调用过程。可以看到,在 Windows API被调用之前,还可以用P r e C r e a t e Wi n d o w ( )来把握最后的机会以改变创建过程。 以上便是创建窗口的过程,注意窗口还没有绘制在屏幕上,系统将随后发送 W M _ PA I N T 和W M _ N C PA I N T消息。 第四部分 附 录表A.1 创建窗口 处理函数 类 型 描 述 参见注释 P r e C r e a t e Wi n d o w ( ) 重载函数 允许在窗口被创建之前改变创建参数 第1条 P r e S u b c l a s s Wi n d o w ( ) 重载函数 允许用户可以首先对窗口子类化 第2条 O n G e t M i n M a x I n f o ( ) 消息处理函数 允许设置对窗口大小的限制 第3条 O n N c C r e a t e ( ) 消息处理函数 通知窗口的非客户区将要创建 第4条 O n N c C a l c S i z e ( ) 消息处理函数 允许改变窗口客户区的大小 第5条 O n C r e a t e ( ) 消息处理函数 通知窗口已经被创建 第6条 O n S i z e ( ) 消息处理函数 通知窗口大小正在改变 第7条 O n M o v e ( ) 消息处理函数 通知窗口正在移动 第7条 O n C h i l d N o t i f y ( ) 重载函数 其调用作为由消息反射过程的一部分, 第8条 以通知父窗口其子窗口已经创建。 该函数仅能被子窗口调用 注释 1) 应该重载C W n d : : P r e C r e a t e Wi n d o w ( )函数来改变用于创建窗口的创建参数。为什么不只 用正确的参数来调用C r e a t e E x ( )呢?这是因为,有时用户无权访问它。例如,当应用程序打开 主窗口的时候便是这种情况。应用程序启动的时候采用缺省的窗口风格和窗口类,通过调用 该函数就获得了改变它们的机会。由于应用程序自动打开窗口的这种情况不是很多,因此该 成员函数通常都在C M a i n F r a m e而不是在其他类中重载。 2) 应当重载C W n d : : P r e S u b c l a s s Wi n d o w ( )成员函数,使其首先对窗口进行子类化。有时这是 很重要的。因为最后子类化窗口的窗口过程有可能接收不到它所需要的所有消息。例如, M F C 为C D r a g L i s t B o x类重载该函数,并在用户子类化该控件之前使该函数首先对它进行子类化。 3) WM_GETMINMAXINFO消息由系统发出,它可以防止窗口变得太大或者太小。例如, 用户拖动一个可重置大小的窗口一角时便发出该消息。 4) WM_NCCREAT E消息由系统发出,用于通知窗口非客户区将要创建。这里不要为了意 图改变缺省窗口过程而修改其所传递的参数。 M F C保存这个原始消息并在调用缺省窗口过程 时使用它。但是,可以调用自己的缺省窗口过程,这样可以安全地绕过 M F C的窗口过程。 5) WM_NCCALCSIZE消息由系统发出,允许改变窗口客户区的大小。客户区通常是除窗 口边界和滚动条等占用空间 (即已经绘制的非客户区)之外的剩余空间。 6) WM_CREAT E消息由系统在其创建窗口之后发出。此时适于创建窗口自己的所属窗口 (例如控件窗口)。 7) 发送W M _ S I Z E和W M _ M O V E消息是为了定位窗口位置和重置窗口大小。其依赖的参 数由: : C r e a t e Wi n d o w E x ( )函数设定。此时适合于重新改变所属窗口的大小,特别是这些所属窗 口的大小依赖于窗口尺寸时更是如此。重叠窗口不能获取这些消息。但它们在用 S h o w Wi n d o w ( )显示窗口的时候便可以获取这些消息。 8) WM_PA R E N T N O T I F Y消息由控制该窗口的父窗口M F C类反射。WM_ PA R E N T N O T I F Y 消息此时被发送到父窗口以通知它子窗口已经被创建。但是,因为该窗口首先从 W M _ C R E AT E 消息得知了这一情况,所以该消息实际上并没有太多的用处,只有子窗口才能得到该消息。 A.1.2 移动窗口 以下调用序列在用M o v e Wi n d o w ( )函数移动和/或者重置窗口大小的时候发生。 362第第第四部分第附 录 下载表A-2 移动窗口 处 理 函 数 类 型 描 述 参见注释 O n Wi n d o w P o s C h a n g i n g ( ) 消息处理函数 通知窗口的X , Y甚至Z坐标将要改变 第9条 O n G e t M i n M a x I n f o ( ) 消息处理函数 允许设置窗口大小的限制 第3条 O n N c C a l c S i z e ( ) 消息处理函数 允许改变窗口客户区的大小 第5条 O n Wi n d o w P o s C h a n g e d ( ) 消息处理函数 通知隐藏的窗口被显示并且 W M _ S I Z E和 第1 0条 W M _ M O V E消息将要发送 O n S i z e ( ) 消息处理函数 通知窗口消息正在改变 第7条 O n M o v e ( ) 消息处理函数 通知窗口正在移动 第7条 注释 9) WM_WINDOWPOSCHANGING消息由系统发出以响应改变窗口位置的函数,或者以 x - y坐标或者其z坐标的形式表明窗口将要移动。 10) WM_WINDOWPOSCHANGED消息由系统在它显示了隐藏窗口之后,但在发送 W M _ S I Z E和W M _ M O V E消息之前发出。显然此时重载该消息处理函数并发送 W M _ S I Z E和W M _ M O V E消息是很有效率的。 A.1.3 显示子窗口 以下调用序列在使用S h o w Wi n d o w ( )函数以显示子窗口的发生时刻。 表A-3 显示子窗口 处理函数 类 型 描 述 参见注释 O n S h o w Wi n d o w ( ) 消息处理函数 由S h o w Wi n d o w ( )函数发出 第11条 O n Wi n d o w P o s C h a n g i n g ( ) 消息处理函数 通知窗口的x - y甚至z坐标将要改变 第9条 O n Wi n d o w P o s C h a n g e d ( ) 消息处理函数 通知隐藏的窗口已经显示,将要发送 第1 0条 W M _ S I Z E和W M _ M O V E消息 注释 11) WM_SHOWWINDOW消息由S h o w Wi n d o w ( )函数发出,这里所要完成的工作最好在 O n C r e a t e或者O n S i z e ( )函数中完成。 A.1.4 显示重叠窗口或弹出窗口 对于重叠窗口和弹出窗口。其调用序列: 表A-4 显示重叠或者弹出窗口 处理函数 类 型 描 述 参见注释 O n S h o w Wi n d o w ( ) 消息处理函数 由S h o w Wi n d o w ( )函数发出 第11条 O n Wi n d o w P o s C h a n g i n g ( ) 消息处理函数 通知窗口的X , Y坐标甚至Z坐标将要改变 第9条 O n Q u e r y N e w P a l e t t e ( ) 消息处理函数 允许改变系统调色板 第1 2条 O n Wi n d o w P o s C h a n g i n g ( ) 消息处理函数 通知窗口的X , Y坐标甚至Z坐标将要改变 第9条 O n N c P a i n t ( ) 消息处理函数 允许自行绘制窗口的非客户区 第1 3条 O n E r a s e B k g n d ( ) 消息处理函数 允许选取绘制窗口背景的颜色 第1 4条 附录A第消息和重载顺序第第363下载(续) 处理函数 类 型 描 述 参见注释 O n Wi n d o w P o s C h a n g e d ( ) 消息处理函数 通知隐藏窗口已经显示,将要发送 第1 0条 W M _ S I Z E和W M _ M O V E消息 O n N c A c t i v a t e ( ) 消息处理函数 通知使用当前颜色绘制非客户区 第1 5条 O n A c t i v a t e ( ) 消息处理函数 通知将被成为当前活动的窗口 第1 6条 O n S e t F o c u s ( ) 消息处理函数 通知将要接收键盘输入的窗口 第1 7条 O n N c P a i n t ( ) 消息处理函数 允许绘制窗口的非客户区 第1 3条 O n Wi n d o w P o s C h a n g e d ( ) 消息处理函数 通知隐藏窗口已经显示,将要发送 第1 0条 W M _ S I Z E和W M _ M O V E消息 O n S i z e ( ) 消息处理函数 通知窗口的大小正在改变,仅能由重叠 第7条 窗口发出 O n M o v e ( ) 消息处理函数 通知窗口正在移动。仅能由重叠窗口发出 第7条 注释 12) WM_QUERY N E W PA L E T T E消息由系统发出,以通知某个窗口它将成为活动窗口并 可以拥有系统调色板。对没有足够显示内存以显示真彩色的系统,将由系统调色板决定获得 输入焦点的窗口在屏幕上显示什么颜色,有关该主题可以参阅第 1章。 13) WM_NCPA I N T消息由系统发出,以允许绘制窗口的非客户区。 14) WM_ERASEBKGND消息由系统发出,允许设置窗口的背景色。 15) WM_NCACTIVATE 消息由系统发出,以通知窗口用当前颜色绘制其非客户区。 16) WM_ACTIVAT E消息发送到窗口,表明它将成为活动窗口并允许其在客户区进行任 何颜色改变操作。 17) WM_SETFOCUS消息发送到窗口,以通知它现在具有输入焦点,这就意味着键盘输 入将被发送到该窗口。 A.1.5 关闭窗口 关闭任何类型窗口的时发生下列调用序列: 表A-5 关闭窗口 处理函数 类 型 描 述 参见注释 O n C l o s e ( ) 消息处理函数 单击关闭按钮时发送 第1 8条 O n D e s t r o y ( ) 消息处理函数 窗口将要被销毁时发送 第1 9条 O n N c D e s t r o y ( ) 消息处理函数 窗口被销毁后发送 第2 0条 P o s t N c D e s t r o y ( ) 重载函数 C W n d处理O n N c D e t r o y ( )的最后调用函数 第2 1条 注意 18) 按下窗口非客户区的关闭按钮时将发送 W M _ C L O S E消息,如果窗口没有关闭按钮, 可以自己发送该消息。这时可以询问用户是否真的要关闭窗口,如果回答不,则不要调用向 窗口发送W M _ D E S T R O Y消息的基类成员函数。 19) WM_DESTROY消息由任何希望销毁窗口的事件发出。这时可以发送自己窗口的销毁 消息,但要记住系统将自动销毁子窗口。 20) WM_NCDESTROY消息在窗口已经销毁后由系统发出。它执行 M F C的清除工作。 364第第第四部分第附 录 下载21) 该C W n d成员函数由C W n d的W M _ N C D E S T R O Y消息的缺省实现所调用。这是销毁所 有与该窗口相关的所有东西的最后机会。如果控制该窗口的 C W n d对象不再有用但并不希望销 毁它,则可以在此使用下列语句来析构该对象: delete this; A.2 对话框 本节将列出创建和关闭对话框时调用的消息处理函数和函数。对话框可以通过两种方式 创建:有模式和无模式。 • 有模式对话框直到其关闭才放弃控制。它有自己的消息循环,这就允许它执行其子窗口 的特定处理(例如:编辑框之间的跳格( t a b b i n g ) )。 • 无模式对话框使用应用程序的消息循环,这就允许任何窗口接收输入焦点,但失去了 有模式消息循环功能。 为了给对话框增加自己的功能,必须首先从 C D i a l o g类派生新类并使用C l a s s Wi z a r d来包含 以下的重载函数和消息处理函数。 A.2.1 创建有模式对话框 以下调用序列在创建有模式对话框的时候发生: CMyDialog dlg; d l g . D o M o d a l ( ) ; 表A-6 打开有模式对话框 处理函数 类 型 描 述 参见注释 D o M o d a l ( ) 重载函数 重载D o M o d a l ( )成员函数 P r e S u b c l a s s Wi n d o w ( ) 重载函数 允许首先子类化该窗口 第2条 O n C r e a t e ( ) 消息处理函数 通知窗口已经被创建 第2、2 7条 O n S i z e ( ) 消息处理函数 通知窗口的大小正在改变 第7条 O n M o v e ( ) 消息处理函数 通知窗口正在移动 第7条 O n S e t F o n t ( ) 消息处理函数 允许改变对话框内控件所用的字体 第2 2条 O n I n i t D i a l o g ( ) 消息处理函数 允许初始化对话框内的控件或者创建新的控件 第2 3条 O n M o v e ( ) 消息处理函数 通知窗口正在移动 第7条 O n S h o w Wi n d o w ( ) 消息处理函数 由S h o w Wi n d o w ( )函数发出 第11条 O n C t l C o l o r ( ) 消息处理函数 由父窗口发出,允许改变对话框颜色 第2 4条 O n C h i l d N o t i f y ( ) 重载函数 作为W M _ C T L C O L O R消息的结果发出 第8、2 5条 注释 22) WM_SETFONT消息由系统在创建对话框的时候发出,允许重载用于对话框资源文件 中指定的字体。字体用于对话框为其控件绘制文本。 23) WM_INITDIALOG消息由系统发出,以允许初始化或者创建任何额外的控件窗口。 24) WM_CTLCOLOR消息由系统发出,以允许确定用什么颜色绘制对话框。 25) 该成员函数由处理 W M _ C T L C O L O R消息的缺省消息处理函数调用。以便于将全部子 窗口通知消息处理过程放进 O n C h i l d N o t i f y内。 附录A第消息和重载顺序第第365下载A.2.2 关闭模式对话框 以下关闭模式对话框的调用序列与关闭窗口过程类似: 表A-7 关闭模式对话框 处理函数 类 型 描 述 参见注释 O n C l o s e 消息处理函数 单击关闭按钮时发送 第1 8条 O n K i l l F o c u s ( ) 消息处理函数 窗口失去键盘输入焦点前发送 第2 6条 O n D e s t r o y ( ) 消息处理函数 窗口将要销毁时发送 第1 9条 O n N c D e s t r o y ( ) 消息处理函数 窗口销毁后发送 第2 0条 P o s t N c D e s t r o y ( ) 消息处理函数 由C W n d调用的处理O n N c D e s t r o y ( )的最后过程 第2 1条 注释 26) 通知窗口它将不再从键盘接收击键操作。 A.2.3 创建无模式对话框 创建无模式对话框时发生以下调用序列: CMyDialog dlg; d l g . C r e a t e (⋯ ) ; 表A-8 打开无模式对话框 处理函数 类 型 描 述 参见注释 P r e S u b c l a s s Wi n d o w ( ) 重载函数 允许首先子类化该窗口 第2条 O n C r e a t e ( ) 消息处理函数 通知已经创建窗口 第6、2 7条 O n S i z e ( ) 消息处理函数 通知窗口大小正在改变 第7条 O n M o v e ( ) 消息处理函数 通知窗口正在移动 第7条 O n S e t F o n t ( ) 消息处理函数 允许改变用于对话框控件的字体 第2 2条 正如以上提到的,无模式对话框并不使用特定的 D o M o d a l L o o p ( )消息循环,这意味着在有 模式对话框中提供的某些方便功能在这里不得不手工实现。当然,无模式对话框的优点是应 用程序的其他部分并不“冻结”,其他窗口在无模式对话框启动时仍然可用。 注释 27) WM_CREAT E消息在对话框创建之后,但在任何控件窗口创建之前发送,因为无模式 对话框并不发送W M _ I N I T D I A L O G消息,所以在调用I n i t D i a l o g ( )函数之前需要等待C r e a t e ( )函 数返回。 A.3 对话框应用程序 本节列出创建和关闭对话框应用程序时调用的消息处理函数和函数。回顾第 1章的内容, 对话框应用程序实际上就是简单地将一个有模式对话框作为自己的界面,对话框应用程序没 有文档或者视。 366第第第四部分第附 录 下载A.3.1 创建对话框应用程序 除了创建应用程序所需要的特定步骤以外,创建对话框应用程序的序列和以上创建有模 式对话框的调用序列是一样的。 表A-9 创建对话框应用程序 处理函数 类 型 描 述 参见注释 I n i t I n s t a n c e ( ) 重载函数 强制性的— 初始化并确定应用程序的类型 第2 8条 参见表A-6 打开有模式对话框 第2 9条 D o M o d a l L o o p ( ) 重载函数 执行对话框内控件窗口的特定处理过程 注释 28) 该成员函数由C Wi n A p p调用以启动应用程序。实际上,对话框应用程序在该函数的派 生函数上花费了几乎所有的时间。该函数是初始化应用程序 (例如:打开数据库,注册窗口类 ) 的最佳场合。然后为创建对话框应用程序。 A p p Wi z a r d将加进下列3行: CXxxDlg dlg; m_pMainWnd = &d int nResponse = dlg.DoModal(); 这三行代码负责创建有模式对话框 (代码中的“X x x”是工程名字)。 29) 在此时调用序列与创建有模式对话框完全一样,除了两点不同: A p p Wi z a r d自动为W M _ I N I T D I A L O G增加一个消息处理函数,它在系统菜单中添加了 A b o u t . . .菜单项。当单击重叠窗口左上角的图标时就会出现系统菜单。 O n I n i t D i a l o g此时也设 置该图标。M D I和M D I应用程序在其装载框架窗口 (下面将讨论)的时候自动地设置该图标。 A p p Wi z a r d还在其中增加某些功能,以在其缩小时在任务栏上绘制应用程序的图标。在 S D I或 M D I应用程序中,这些工作对用户来说是不可见的。 A.3.2 关闭对话框应用程序 关闭对话框应用程序的调用序列与关闭窗口或者关闭有模式对话框是一样的,只是增加 了关闭应用程序自身的一步: 表A-10 关闭对话框应用程序 处理函数 类 型 描 述 参见注释 O n C l o s e ( ) 消息处理函数 单击关闭按钮时发送 第1 8、3 0条 O n D e s t r o y ( ) 消息处理函数 窗口将要销毁时发送 第1 9条 O n N c D e s t r o y ( ) 消息处理函数 窗口销毁后发送 第2 0条 P o s t N c D e s t r o y ( ) 重载函数 由C W n d调用作为O n N c D e s t r o y ( )的最后处理过程 第2 1条 E x i t I n s t a n c e ( ) 重载函数 调用来终止应用程序 第3 1条 注释 30) 这是唯一可以询问用户它们是否确定终止应用程序的位置。如果不终止应用程序,将 不会调用发送W M _ D E S T R O Y消息给对话框的O n C l o s e ( )基类函数。 31) 由C Wi n A p p调用以终止应用程序,此处适于关闭数据库和释放应用程序资源。 附录A第消息和重载顺序第第367下载A.4 SDI应用程序 本部分将列出创建和关闭 S D I应用程序时所调用的消息处理函数和函数,同时查看 S D I应 用程序在以下情况所使用的调用序列: 创建新文档。 打开已有文档。 保存文档。 A.4.1 创建SDI应用程序 创建S D I应用程序时发生下列调用序列。 表A - 11 打开S D I应用程序 处理函数 类 型 描 述 参见注释 I n i t I n s t a n c e ( ) 重载函数 强制性的— 初始化并决定应用程序的类型 第3 2条 创建了新文档或者加载已有文档— 参见表A - 1 2和A - 1 3 R u n ( ) 重载函数 C Wi n A p p的后台处理函数 第3 3条 O n I d l e ( ) 重载函数 允许进行自己的后台处理 第3 4条 注释 32) 由C Wi n A p p调用的成员函数,它启动应用程序并允许定制自己应用程序的初始化 (如: 打开数据库,注册窗口类 )。当A p p Wi z a r d创建工程时它重载I n i t I n s t a n c e ( )函数并填写一些初始 化代码。对S D I应用程序还增加了下列3个步骤: 创建文档模板。 解析并处理命令行。 显示主窗口。 文档模板由下列代码创建: C S i n g l e D o c Template* pDocTe m p l a t e ; p D o c Template = new CSingleDocTe m p l a t e ( I D R _ M A I N F R A M E , RUNTIME_CLASS( CExamplesDoc ), RUNTIME_CLASS( CMainFrame ), // main SDI frame window RUNTIME_CLASS( CExamplesView ) ) ; A d d D o c Template( pDocTemplate ); 文档模板将框架、文档和视类关联起来,用于装载特定的文档。每个应用程序可能有一 个或者一个以上的文档模板允许处理多种类型的文档,但这种情况并不多见,而且是为大型 M D I应用程序所保留的功能,如 Developer Studio。 具有多个文档模板的S D I应用程序 如果为S D I应用程序添加其他的文档模板: 只要用户创建或者打开一个文档, M F C就会用一个对话框提示并询问它们使用哪个模 板。M F C使用在资源文件中对应资源标识符下找到的模板名来填充该对话框,此时的资源标 识符是I D R _ M A I N F R A M E。 368第第第四部分第附 录 下载如果用户创建或者装载了已经被装载的文档类型, M F C并不创建其他框架、文档和视 类对象,而是使用已有的对象。但是,如果打开一个新文档类型就会创建第二组对象。 下列代码负责解析和处理命令行: CCommandLineInfo cmdInfo; ParseCommandLine( cmdInfo ); if ( !ProcessShellCommand( cmdInfo ) ) return FA L S E ; 命令行在P a r s e C o m m a n d L i n e ( )中进行解析,并在 P r o c e s s S h e l l C o m m a n d ( )中接受处理。一 个空命令行将使 P r o c e s s S e l l C o m m a n d ( )创建新文档。命令行上的文件名导致 P r o c e s s S h e l l - Command ()试图将其作为文档打开。 P r o c e s s S h e l l C o m m a n d ( )函数通过调用C Wi n A p p的O n C m d M s g ( )成员函数来创建并打开一 个文档。其中用到两个预定义的命令: I D _ F I L E _ N E W,用于创建一个新文档,以及 I D _ F I L E _ O P E N,用于打开一个已有文档。 I n i t I n s t a n c e ( )的最后一行代码显示并绘制主窗口: m _ p M a i n W n d - > S h o w Wi n d o w ( S W _ S H O W ) ; m _ p M a i n W n d - > U p d a t e Wi n d o w ( ) ; m _ p M a i n W n d包含了指向主窗口的指针,该主窗口由 P r o c e s s S h e l l C o m m a n d ( )创建。 33) 这个C Wi n A p p成员函数在应用程序运行后调用。该函数花费其大量时间在空闲循环 (idle loop)中进行后台处理工作,并查看应用程序消息队列中寄送来的消息—特别是, W M _ C L O S E消息将导致该循环和应用程序的终止。 34) 该C Wi n A p p成员函数允许重载 R u n ( )函数的空闲循环,以便在应用程序中执行后台处 理过程。 A.4.2 创建新的SDI文档 下列调用序列创建一个新 S D I文档。如果文档、框架和视类都还不存在,例如在程序初始 化的时候,则该文档仍然被创建。 S D I文档按照下列步骤创建: 如果没有文档类对象,则创建一个。 如果没有框架类对象并且窗口退出,则创建一个。 如果框架类对象已经创建了,则创建视类对象和窗口。 初始化文档。 激活框架和视。 表A-12 创建一个新S D I文档 处理函数 类 型 描 述 参见注释 C Wi n A p p : : O n F i l e N e w ( ) 消息处理函数 单击主窗口的 N e w命令时发送 第3 5条 C S i n g l e D o c Te m p l a t e : : O p e n D o c u m e n t F i l e ( ) 重载函数 由缺省的O n F i l e N e w ( )调用。如果文件名参数 第3 6条 是N U L L则创建一个空的文档。如果需要,则 调用下列函数创建文档、框架和视类对象 如果不存在文档类对象则创建它. . . 附录A第消息和重载顺序第第369下载370第第第四部分第附 录 下载 (续) 处理函数 类 型 描 述 参见注释 C D o c Te m p l a t e : : 重载函数 用文档模板的 C R u n t i m e C l a s s和C r e a t e O b j e c t ( ) 第3 7条 C r e a t e N e w D o c u m e n t ( ) 函数创建一个新文档类对象 如果不存在框架类对象则创建它. . . C D o c Te m p l a t e : : 重载函数 用文档模板的 C R u n t i m e C l a s s和C r e a t e O b j e c t ( ) C r e a t e N e w F r a m e ( ) 函数创建一个新框架类对象 C M a i n F r a m e : : 重载函数 导致框架窗口被创建并装载资源 L o a d F r a m e ( ) C F r a m e W n d : : C r e a t e ( ) 重载函数 创建框架窗口 C M a i n F r a m e : : 重载函数 允许框架窗口在从模板创建之前重载其风格和类 第1、3 8条 P r e C r e a t e Wi n d o w ( ) C M a i n F r a m e : : 重载函数 不是打印错误 — 第2次调用,参看注释 第1、3 8条 P r e C r e a t e Wi n d o w ( ) C M a i n F r a m e : : 重载函数 允许框架窗口预子类化 第2条 P r e S u b c l a s s Wi n d o w ( ) CMainFrame:: 消息处理函数 允许框架窗口有其最大或者最小尺寸 第3条 O n G e t M i n M a x I n f o ( ) C M a i n F r a m e : : 消息处理函数 通知框架窗口已经创建 第6、3 9条 O n C r e a t e ( ) C M a i n F r a m e : : 重载函数 由L o a d F r a m e ( )调用来创建视类对象和窗口, O n C r e a t e C l i e n t ( ) O n C r e a t e C l i e n t ( )调用接下来的函数 如果正在创建新的框架,则创建新的视类对象和视窗口. . . C F r a m e W n d : : 重载函数 用文档模板的 C R u n t i m e C l a s s和C r e a t e O b j e c t ( ) C r e a t e Vi e w ( ) 函数创建一个新视类对象 C Vi e w : : C r e a t e ( ) 重载函数 创建视窗口 C Vi e w : : 重载函数 允许视窗口在从模板创建之前重载其风格、类 第1条 P r e C r e a t e Wi n d o w ( ) C Vi e w : : 重载函数 允许视窗口预子类化 第2条 P r e S u b c l a s s Wi n d o w ( ) C Vi e w : : O n C r e a t e ( ) 消息处理函数 通知视窗口已经创建 第6、4 0条 C D o c u m e n t : : 重载函数 被调用以告诉文档类已经为其添加了一个视 O n C h a n g e d Vi e w L i s t ( ) C Vi e w : : O n S i z e ( ) 消息处理函数 重置视窗口大小 第7条 C Vi e w : : O n M o v e 消息处理函数 移动视窗口 第7条 C Vi e w : : 重载函数 通知视窗口已经创建 第8条 O n C h i l d N o t i f y ( ) C Vi e w : : O n S h o w Wi n d o w ( ) 消息处理函数 显示视窗口 第11条 如果框架和视已经创建,设置它们各自的大小. . . C M a i n F r a m e : : O n M o v e ( ) 消息处理函数 沿视窗口移动框架窗口 第7条 C M a i n F r a m e : : O n S i z e ( ) 消息处理函数 沿视窗口改变框架窗口大小 第7条 C M a i n F r a m e : : 重载函数 重新定位框架窗口内的视和控制条 R e c a l c L a y o u t ( ) C Vi e w : : 重载函数 在设计的客户区大小基础之上确定整个窗口的大小 C a l c Wi n d o w R e c t ( ) C Vi e w : : O n M o v e ( ) 消息处理函数 在框架窗口之内移动视 第7条(续) 处理函数 类 型 描 述 参见注释 C Vi e w : : O n S i z e ( ) 消息处理函数 在框架窗口之内改变视的大小 第7条 C M a i n F r a m e : : 重载函数 再一次⋯⋯ R e c a l c L a y o u t ( ) C Vi e w : : 重载函数 ⋯⋯同样的 C a l c Wi n d o w R e c t ( ) 完成文档创建. . . C S i n g l e D o c Te m p l a t e : : 重载函数 设置文档的缺省标题 S e t D e f a u l t Ti t l e ( ) C D o c u m e n t : : S e t Ti t l e ( ) 重载函数 设置实际的标题 C D o c u m e n t : : 重载函数 初始化,通常留给下面的函数来完成 第4 1条 O n N e w D o c u m e n t ( ) C D o c u m e n t : : 重载函数 初始化C D o c u m n e t的成员变量。 第4 1条 D e l e t e C o n t e n t s ( ) 用于已经装载了文档的时候 第1次更新视. . . C D o c Te m p l a t e : : 重载函数 通知视第1次初始化 I n i t i a l U p d a t e F r a m e ( ) C M a i n F r a m e : : 重载函数 通知视第1次初始化 I n i t i a l U p d a t e F r a m e ( ) C Vi e w : : 重载函数 通知视第1次初始化 第4 2条 O n I n i t i a l U p d a t e ( ) C Vi e w : : O n U p d a t e ( ) 重载函数 只要文档变化就调用以更新视 第4 3条 激活框架窗口. . . C M a i n F r a m e : : 重载函数 激活框架 A c t i v a t e F r a m e ( ) C M a i n F r a m e : : 消息处理函数 允许用自己的颜色装载系统调色板 第1 2条 O n Q u e r y N e w P a l e t t e ( ) C M a i n F r a m e : : 消息处理函数 通知应用程序正被激活 O n A c t i v a t e A p p ( ) C Vi e w : : 消息处理函数 通知视正被激活 O n A c t i v a t e Vi e w ( ) C M a i n F r a m e : : 消息处理函数 通知框架正被激活 O n A c t i v a t e ( ) C M a i n F r a m e : : 消息处理函数 显示框架 第11条 O n S h o w Wi n d o w ( ) C M a i n F r a m e : : 消息处理函数 允许改变框架背景颜色 O n E r a s e B k g n d ( ) C M a i n F r a m e : : O n P a i n t ( ) 消息处理函数 绘制框架窗口的客户区 C Vi e w : : O n P a i n t ( ) 消息处理函数 绘制视,但使用O n D r a w ( )函数 C Vi e w : : O n D r a w ( ) 重载函数 被C Vi e w的O n P a i n t ( )函数调用 第4 4条 35) ID_FILE_NEW命令消息表明用户希望装载空文档。该消息或者在应用程序初始化的 时候由P r o c e s s S h e l l C o m m a n d ( )函数发送,或者在用户单击了 F i l e菜单下的N e w命令后发送。 I D _ F I L E _ N E W消息是几个预定义M F C消息中的一个,可以自己处理该消息以创建自定义文档, 附录A第消息和重载顺序第第371下载372第第第四部分第附 录 下载 或者让M F C通过创建新M F C风格文档来处理。接下来的调用序列说明 M F C如何创建一个新文 档。 36) OpenDocumentFile()函数是文档模板类C D o c Te m p l a t e的成员函数。可以在应用程序的 任何地方调用O p e n d o c u m e n t F i l e ( )来创建一个新文档,只需选取要创建的文档模板并调用以下 代码即可: p Te m p l a t e - > O p e n D o c u m e n t F i l e ( N U L L ) ; 37) CreateNewDocument()函数是文档模板类 C D o c Te m p l a t e的另一个成员函数。可以用 C r e a t e O b j e c t ( )函数从I n i t I n s t a n c e ( )函数中的模板创建一个文档对象。但实际上不必用该函数创 建文档对象。可以用以前的方法来完成: Doc = new CDocument; 38) 第1次调用了P r e C r e a t e Wi n d o w ( )函数之后,M F C试图确定用什么窗口类来创建框架窗口。 如果这里没有指定窗口类,M F C将自动地创建A f x F r a m e O r Vi e w作为模板,而A f x R e g i s t e r W n d C l a s s ( )则实际创建它并为其命名。注意框架窗口用 A f x W n d P r o c作为其处理函数。第 2次调用该函数 则是在实际窗口正在被创建之时。 39) 通常这是在C M a i n F r a m e中创建任何控制条(对话条、状态栏和工具栏等 )的场合。 40) 此处适于创建视中的任何控件。 41) 可以按照某特定方式重载该函数以初始化文档的成员变量。但是,如果使用该函数的 缺省实现并且重载了 D e l e t e C o n t e n t s ( )而不是初始化变量,就必须再次进行该项工作以打开已 有文件。这两种情况下都要调用 D e l e t e C o n t e n t s ( )函数。 42) 此处适于初始化视中的任何控件。 43) 此处适于在文档发生变化时更新视中的任何控件或者图像。 44) 该函数通常由O n P a i n t ( )调用。应当重载该函数而不是 O n P a i n t ( )函数,这是因为如果这 样做,则将屏幕打印到打印机的功能是内置的。但只要使用了设备环境绘制到屏幕,则视内 的任何控件不能自动被打印。 A.4.3 打开已有的SDI文档 下列序列从文件中装载已有的 S D I文档,如果文档、框架和视类对象还不存在,例如在程 序初始化阶段,它们仍然被创建,但是因为以上已经回顾了这种情况,下面仅讨论这些对象 已经存在的情况: 表A-13 打开已有S D I文档 处理函数 类 型 描 述 参见注释 C Wi n A p p : : O n F i l e O p e n ( ) 消息处理函数 单击主菜单中的“O p e n”命令时发出 第4 5条 C S i n g l e D o c Te m p l a t e : : 重载函数 由缺省的O n F i l e O p e n ( )函数调用。将指定文件装载 O p e n D o c u m e n t F i l e ( ) 到文档,如果需要,可随意创建文档、框架和视类对象 C D o c u m e n t : : 重载函数 保证当前文档不被修改— 如果被修改了, S a v e M o d i f i e d ( ) 用户可以将其保存 C D o c u m e n t : : 重载函数 调用下列函数来装载已有文档的缺省实现 O n O p e n D o c u m e n t ( ) C D o c u m e n t : : 重载函数 初始化C D o c u m e n t的成员变量 第4 1条 D e l e t e C o n t e n t s ( )附录A第消息和重载顺序第第373下载 (续) 处理函数 类 型 描 述 参见注释 C D o c u m e n t : : 重载函数 使用M F C的串行化特征来装载文件到成员变量 S e r i a l i z e ( ) C D o c u m e n t : : 重载函数 设置被装载文件的路径名 S e t P a t h N a m e ( ) C D o c u m e n t : : S e t Ti t l e ( ) 重载函数 设置文档标题 C Wi n A p p : : 重载函数 在F i l e菜单中的最近打开文件的路径列表添加 A d d To R e c e n t F i l e L i s t ( ) 文件的路径名 C Vi e w : : 重载函数 通知视第1次初始化 第4 2条 O n I n i t i a l U p d a t e ( ) C Vi e w : : O n U p d a t e ( ) 重载函数 只要文档发生改变即被调用并更新视 第4 3条 注释 45) OnFileOpen()处理另一个预定义命令消息: I D _ F I L E _ O P E N。 A.4.4 保存SDI文档 以下调用序列将已经修改的文档保存到文件。 表A-14 保存S D I文档 处理函数 类 型 描 述 参见注释 C D o c u m e n t : : 消息处理函数 单击主菜单中的S a v e命令后发出 第4 6条 O n F i l e S a v e ( ) C D o c u m e n t : : 重载函数 调用以下函数保存文档到文件的缺省实现 第4 7条 O n S a v e D o c u m e n t ( ) C D o c u m e n t : : 重载函数 使用M F C的串行化特征将成员变量串行化到文件 第4 8条 S e r i a l i z e ( ) C D o c u m e n t : : 重载函数 设置保存文件的路径名 第4 9条 SetPathName() C D o c u m e n t : : 重载函数 设置保存文档的文档名 第4 9条 S e t Ti t l e ( ) C Wi n A p p : : 重载函数 在F i l e菜单中的最近打开文件的路径列表 第4 9条 A d d To R e c e n t F i l e L i s t ( ) 添加文件的路径名 注释 46) OnFileSave()处理另一个预定义的命令消息: I D _ F I L E _ S AV E。这样做的另一个方式 是由C D o c u m e n t : : O n F i l e S a v e A s ( )函数处理I D _ F I L E _ S AV E _ A S。 47) 该函数打开传递给它的文件名,然后打开一个具有该文件名的档案文件并调用 S e r i a l i z e ( )将文件存盘。 48) Serialize()允许自动地将文档存入磁盘。如果要将其存入系统注册表或者数据库,可 以重载O n F i l e S a v e ( )函数并在其中保存数据。 49) 这些函数只有在使用Save As命令期间路径名发生改变的情况下才被调用。 A.4.5 关闭SDI应用程序 以下调用序列关闭S D I应用程序。该序列假设文档没有进行修改。参见表 A - 1 4了解用于保374第第第四部分第附 录 下载 存被修改过文件时的调用序列。 表A-15 关闭S D I文档 处理函数 类 型 描 述 参见注释 C M a i n F r a m e : : O n C l o s e ( ) 消息处理函数 单击了关闭按钮或者F i l e菜单下的E x i t 第5 0条 命令发送该消息 C D o c u m e n t : : 重载函数 由O n C l o s e调用,它是调用下面的函数以查看文 C a n C l o s e F r a m e ( ) 档是否被修改以及用户是否打算保存的缺省实现 C D o c u m e n t : : 重载函数 询问用户保存已修改的文档 S a v e M o d i f i e d ( ) C M a i n F r a m e : : 消息处理函数 隐藏主框架窗口 第11条 O n S h o w Wi n d o w ( ) C Vi e w : : 消息处理函数 使视当前不被激活 O n A c t i v a t e Vi e w ( ) C M a i n F r a m e : : 消息处理函数 使框架当前不被激活 O n A c t i v a t e ( ) C M a i n F r a m e : : 消息处理函数 使应用程序当前不被激活 O n A c t i v a t e A p p ( ) C Vi e w : : O n K i l l F o c u s ( ) 消息处理函数 从应用程序中去除键盘输入焦点 第2 6条 C D o c u m e n t : : 重载函数 关闭文档,缺省实现调用下列函数 O n C l o s e D o c u m e n t ( ) C D o c u m e n t : : 重载函数 释放内存以初始化文档类 第4 1条 D e l e t e C o n t e n t s ( ) C M a i n F r a m e : : 重载函数 销毁主框架窗口 D e s t r o y Wi n d o w ( ) C M a i n F r a m e : : 消息处理函数 发送该消息以销毁主框架窗口 第1 9条 O n D e s t r o y ( ) C Vi e w : : O n D e s t r o y ( ) 消息处理函数 发送该消息以销毁视窗口 第1 9条 C Vi e w : : O n A c t i v a t e Vi e w 消息处理函数 使视当前不被激活 C Vi e w : : 重载函数 在视窗口销毁后调用 第2 1条 P o s t N c D e s t r o y ( ) C D o c u m e n t : : 重载函数 通知文档它已经失去视 O n C h a n g e d Vi e w L i s t ( ) C M a i n F r a m e : : 重载函数 在框架窗口销毁之后调用 第2 1条 P o s t N c D e s t r o y ( ) C Wi n A p p : : 重载函数 允许释放分配给应用程序的所有资源 第3 0条 E x i t I n s t a n c e ( ) 注释 50) 此处是工具栏Are you sure?提示消息的地方。如果回答是 n o,则不调用其基类函数发 送W M _ D E S T R O Y消息给主框架窗口。否则将等待框架窗口使用 S a v e M o d i f i e d ( )函数检查文档 是否已经被修改。 A.5 MDI应用程序 本节将列出创建和关闭 M D I应用程序的消息和所调用的函数。除了 M D I应用程序拥有一个由一个或一个以上子框架窗口所填充的主框架窗口以外, M D I应用程序与 S D I应用程序类 似。 另外,还将看到 M D I应用程序在以下情况所使用的调用序列: 创建新文档。 打开已有文档。 保存M D I文档。 关闭M D I子框架。 表A-16 打开M D I应用程序 处理函数 类 型 描 述 参见注释 I n i t I n s t a n c e ( ) 重载函数 强制性的—初始化并决定应用程序的类型 主窗口创建— 参见表A - 1 7 创建了新文档或者装载了已有文档— 参见表A - 1 8和A - 1 9 R u n ( ) 重载函数 C Wi n A p p的后台处理函数 O n I d l e ( ) 重载函数 允许进行自己的后台处理 51) 由C Wi n A p p调用的成员函数,它启动应用程序并允许定制自己应用程序的初始化 (如: 打开数据库、注册窗口类 )。当A p p Wi z a r d创建工程时它重载I n i t I n s t a n c e ( )函数并填写一些初始 化代码。对M D I应用程序还增加了下列4个步骤: 创建文档模板。 创建主框架窗口。 解析并处理命令行。 显示主窗口。 文档模板由下列代码创建: C M u l t i D o c Template* pDocTe m p l a t e ; p D o c Template = new CMultiDocTe m p l a t e ( I D R _ E X A M P LT Y P E , RUNTIME_CLASS( CExamplemdiDoc ), RUNTIME_CLASS( CChildFrame ), // custom MDI child frame RUNTIME_CLASS( CExamplemdiView ) ) ; A d d D o c Template( pDocTemplate ); 文档模板将框架、文档和视类关联起来用于装载特定的文档。每个应用程序可能有一个 或者一个以上的文档模板允许处理多种类型的文档,但这种情况并不多见,而且是为大型 M D I应用程序所保留的功能,如 Developer Studio。 注意这里使用的框架窗口是子框架窗口。现在只要装载一个新文档或者已有文档,就会 创建一个新的子框架、视和文档类对象并驻留在主框架窗口。那么,主框架窗口是什么时候 创建的呢? 主框架窗口由下列几行代码创建: // create main MDI Frame window CMainFrame* pMainFrame = new CMainFrame; if ( !pMainFrame -> LoadFrame( IDR_MAINFRAME ) ) 附录A第消息和重载顺序第第375下载return FA L S E ; m_pMainWnd = pMainFrame; L o a d F r a m e ( )函数和S D I应用程序一样,在此再次使用并导致下列函数调用消息序列: 表A-17 创建主框架 处理函数 类 型 描 述 参见注释 C M a i n F r a m e : : 重载函数 调用以下函数以装载框架 L o a d F r a m e ( ) C M a i n F r a m e : : 重载函数 为框架窗口指定窗口类的时候调用 第1、3 8条 P r e C r e a t e Wi n d o w ( ) C M a i n F r a m e : : 重载函数 改变框架窗口的窗口风格或者大小的时候调用 第1、3 8条 P r e C r e a t e Wi n d o w ( ) C M a i n F r a m e : : 重载函数 允许预子类化窗口 第2条 P r e S u b c l a s s Wi n d o w ( ) C M a i n F r a m e : : 消息处理函数 允许限制主框架窗口的大小 第3条 O n G e t M i n M a x I n f o ( ) C M a i n F r a m e : : 消息处理函数 主框架窗口被创建了— 可以创建工具栏、 第6条 O n C r e a t e ( ) 对话条和状态栏 C M a i n F r a m e : : 重载函数 使用“M D I C L I E N T”窗口类的窗口填充M D I O n C r e a t e C l i e n t ( ) 应用程序的客户区 C M a i n F r a m e : : 重载函数 在框架窗口内重新定位视和控制条的时候调用 R e c a l c L a y o u t ( ) 在主框架窗口创建后,下列代码负责解析和处理命令行: CCommandLineInfo cmdInfo; ParseCommandLine( cmdInfo ); if ( !ProcessShellCommand( cmdInfo ) ) return FA L S E ; 命令行在P a r s e C o m m a n d L i n e ( )中进行解析,并在 P r o c e s s S h e l l C o m m a n d ( )中接受处理。一 个空命令行将使 P r o c e s s S e l l C o m m a n d ( )创建新文档。命令行上的文件名导致 P r o c e s s S h e l l - Command ()试图将其作为文档打开。 P r o c e s s S h e l l C o m m a n d ( )函数通过调用C Wi n A p p的O n C m d M s g ( )成员函数来创建并打开一 个文档。其中用到两个预定义的 M F C命令:I D _ F I L E _ N E W,用于创建一个新文档,以及 I D _ F I L E _ O P E N,用于打开一个已有文档。这与 S D I应用程序一样。 I n i t I n s t a n c e ( )的最后一行代码显示并绘制主窗口: m_pMainWnd -> ShowWindow( SW_SHOW ); m_pMainWnd -> UpdateWi n d o w ( ) ; m _ p M a i n w n d包含了指向以上所创建的主窗口的指针。 A.5.1 创建新的MDI文档 下列调用序列创建一个新 M D I文档。文档、子框架和视类对象现在都已经创建了。除此 以外,该调用序列几乎同以上创建新的 S D I文档完全一样。 376第第第四部分第附 录 下载表A-18 创建一个新M D I文档 处理函数 类 型 描 述 参见注释 C Wi n A p p : : O n F i l e N e w ( ) 消息处理函数 单击主窗口的N e w命令时发送 第3 5条 C M u l t i D o c Te m p l a t e : : 重载函数 由O n F i l e N e w ( )的缺省实现调用。如果文件名 第3 6条 O p e n D o c u m e n t F i l e ( ) 参数是N U L L则创建一个空的文档。调用下列函数 创建文档、子框架和视类对象 创建文档类对象 . . . C D o c Te m p l a t e : : 用文档模板的C R u n t i m e C l a s s和C r e a t e O b j e c t ( ) C r e a t e N e w D o c u m e n t ( ) 重载函数 函数创建一个新文档类对象 第3 7条 创建子框架类对象和子框架窗口. . . C D o c Te m p l a t e : : 重载函数 创建一个新框架类对象 C r e a t e N e w F r a m e ( ) C C h i l d F r a m e : : 重载函数 导致框架窗口被创建并装载资源 L o a d F r a m e ( ) C C h i l d F r a m e : : 重载函数 在框架窗口类从模板中创建之前允许重载风格和类 第1、3 8条 P r e C r e a t e Wi n d o w ( ) C C h i l d F r a m e : : C r e a t e ( ) 重载函数 创建框架窗口 C M a i n F r a m e : : 重载函数 在框架窗口内重新定位视和控制条 R e c a l c L a y o u t ( ) C C h i l d F r a m e : : 重载函数 不是打印错误— 第2次调用该函数,参看注释 第1、3 8条 P r e C r e a t e Wi n d o w ( ) C C h i l d F r a m e : : 重载函数 允许框架窗口预子类化 第2条 P r e S u b c l a s s Wi n d o w ( ) C C h i l d F r a m e : : 消息处理函数 允许框架窗口有其最大或者最小尺寸 第3条 O n G e t M i n M a x I n f o ( ) C C h i l d F r a m e : : 重载函数 允许框架窗口在从模板创建之前重载其风格、类 第1、3 8条 P r e C r e a t e Wi n d o w ( ) C C h i l d F r a m e : : 消息处理函数 通知框架窗口已经创建 第6、3 9条 O n C r e a t e ( ) C C h i l d F r a m e : : 重载函数 由L o a d F r a m e ( )调用来创建视类对象和窗口。 O n C r e a t e C l i e n t ( ) L o a d F r a m e ( )调用接下来的函数 创建新的视类对象和视窗口. . . C F r a m e W n d : : 重载函数 用文档模板的C R u n t i m e C l a s s和C r e a t e O b j e c t ( ) C r e a t e Vi e w ( ) 函数创建一个新视类对象 C Vi e w : : C r e a t e ( ) 重载函数 创建视窗口 C Vi e w : : P r e C r e a t e Wi n d o w ( ) 重载函数 允许视窗口在从模板创建之前重载其风格、类 第1条 C Vi e w : : 重载函数 允许视窗口预子类化 第2条 P r e S u b c l a s s Wi n d o w ( ) C Vi e w : : O n C r e a t e ( ) 消息处理函数 通知视窗口已经创建 第6、4 0条 C D o c u m e n t : : 重载函数 被调用以通知文档类已经为其添加了一个视 O n C h a n g e d Vi e w L i s t ( ) 附录A第消息和重载顺序第第377下载378第第第四部分第附 录 下载 (续) 处理函数 类 型 描 述 参见注释 C Vi e w : : O n S i z e ( ) 消息处理函数 重置视窗口大小 第7条 C Vi e w : : O n M o v e 消息处理函数 移动视窗口 第7条 C Vi e w : : 重载函数 通知视窗口已经创建 第8条 O n C h i l d N o t i f y ( ) C Vi e w : : O n S h o w Wi n d o w ( ) 消息处理函数 显示视窗口 第11条 如果子框架和视已经创建,则设置各自的大小. . . C M a i n F r a m e : : O n M o v e ( ) 消息处理函数 沿视窗口移动框架窗口 第7条 C M a i n F r a m e : : O n S i z e ( ) 消息处理函数 沿视窗口改变框架窗口大小 第7条 C M a i n F r a m e : : 重载函数 重新定位框架窗口内的视和控制条 R e c a l c L a y o u t ( ) C Vi e w : : 重载函数 在预期的客户区大小基础上确定整个窗口的大小 C a l c Wi n d o w R e c t ( ) C Vi e w : : O n M o v e ( ) 消息处理函数 在框架窗口之内移动视 第7条 C Vi e w : : O n S i z e ( ) 消息处理函数 在框架窗口之内改变视的大小 第7条 C M a i n F r a m e : : 重载函数 再一次⋯⋯ R e c a l c L a y o u t ( ) C Vi e w : : 重载函数 ⋯⋯同样的 C a l c Wi n d o w R e c t ( ) C C h i l d F r a m e : : 重载函数 激活子框架 O n M D I A c t i v a t e ( ) 完成文档创建 . . . C D o c u m e n t : : S e t Ti t l e ( ) 重载函数 设置实际标题 C D o c u m e n t : : 重载函数 初始化,通常留给下面的函数来完成 第4 1条 O n N e w D o c u m e n t ( ) C D o c u m e n t : : 重载函数 初始化C D o c u m n e t的成员变量。用于已经 第4 1条 D e l e t e C o n t e n t s ( ) 装载了文档的时候 第1次更新视. . . C D o c Te m p l a t e : : 重载函数 通知视第1次初始化 I n i t i a l U p d a t e F r a m e ( ) C M a i n F r a m e : : 重载函数 通知视第1次初始化 I n i t i a l U p d a t e F r a m e ( ) C Vi e w : : 重载函数 通知视第1次初始化 第4 2条 O n I n i t i a l U p d a t e ( ) C Vi e w : : O n U p d a t e ( ) 重载函数 只要文档发生变化便调用以更新视 第4 3条 激活主框架窗口和子框架. . . C C h i l d F r a m e : : 重载函数 激活主框架窗口 A c t i v a t e F r a m e ( ) C C h i l d F r a m e : : 重载函数 激活子框架窗口 O n M D I A c t i v a t e ( ) C C h i l d F r a m e : : 消息处理函数 显示框架 O n S h o w Wi n d o w ( )附录A第消息和重载顺序第第379下载 (续) 处理函数 类 型 描 述 参见注释 C Vi e w : : O n A c t i v a t e Vi e w ( ) 消息处理函数 激活视 C M a i n F r a m e : : 消息处理函数 显示主框架窗口 第11条 O n S h o w Wi n d o w ( ) C M a i n F r a m e : : 消息处理函数 允许主框架窗口用它自己的颜色填充系统调色板 第1 2条 O n Q u e r y N e w P a l e t t e ( ) C M a i n F r a m e : : 消息处理函数 允许主框架窗口改变绘制背景的颜色 O n E r a s e B k g n d ( ) C M a i n F r a m e : : 消息处理函数 激活应用程序 O n A c t i v a t e A p p ( ) C Vi e w : : O n A c t i v a t e Vi e w ( ) 消息处理函数 激活视 C M a i n F r a m e : : O n A c t i v a t e ( ) 消息处理函数 激活框架窗口 C Vi e w : : O n P a i n t ( ) 消息处理函数 绘制视,但使用的是O n D r a w ( )函数 C Vi e w : : O n D r a w ( ) 重载函数 被C Vi e w的O n P a i n t ( )函数调用 A.5.2 打开已有的MDI文档 下列序列从文件中装载已有的 M D I文档,这也是与S D I文档很相似的,只是文档、框架和 视类对象都已经创建了。 表A-19 打开已有M D I文档 处理函数 类 型 描 述 参见注释 C Wi n A p p : : O n F i l e O p e n ( ) 消息处理函数 单击主菜单中的O p e n命令时发出 第3 5条 C M u l t i D o c Te m p l a t e : : 重载函数 由缺省的O n F i l e O p e n ( )函数调用。将指定文件装载 第3 6条 O p e n D o c u m e n t F i l e ( ) 到文档,调用以下函数以创建文档、子框架和视类对象 创建文档类对象 . . . C D o c Te m p l a t e : : 重载函数 创建一个新的文档类对象 第3 7条 C r e a t e N e w D o c u m e n t ( ) 创建子框架类对象和窗口. . . C C h i l d F r a m e : : 重载函数 调用下列函数来装载子框架 L o a d F r a m e ( ) C C h i l d F r a m e : : 重载函数 用自己的窗口类来创建子框架窗口 第1、3 8条 P r e C r e a t e Wi n d o w ( ) C M a i n F r a m e : : 重载函数 重新定位框架窗口内的视和控制条 R e c a l c L a y o u t ( ) C C h i l d F r a m e : : 重载函数 改变子框架窗口的其他属性 第1、3 8条 P r e C r e a t e Wi n d o w ( ) C C h i l d F r a m e : : 重载函数 允许预子类化窗口 第2条 P r e S u b c l a s s Wi n d o w ( ) C C h i l d F r a m e : : 重载函数 允许设置子框架窗口大小限制 第3条 O n G e t M i n M a x I n f o ( ) C C h i l d F r a m e : : 消息处理函数 子框架窗口已经创建,此时可以为该子框架创建 第6条 O n C r e a t e ( ) 工具栏、对话条、状态栏(续) 处理函数 类 型 描 述 参见注释 C C h i l d F r a m e : : 重载函数 用下面的函数为该子框架创建视 O n C r e a t e C l i e n t ( ) 创建视类对象和窗口. . . C Vi e w : : 重载函数 允许改变所创建视类窗口的属性 第1条 P r e C r e a t e Wi n d o w ( ) C Vi e w : : 重载函数 允许预子类化视窗口 第2条 P r e S u b c l a s s Wi n d o w ( ) C Vi e w : : O n C r e a t e ( ) 重载函数 视窗口已经创建,为该视窗口创建控件 第6条 CDocument:: 重载函数 通知文档已经添加视 O n C h a n g e d Vi e w L i s t ( ) C Vi e w : : O n S i z e ( ) 消息处理函数 设置视的大小,此时适于可以设置该视的任何子 第7条 窗口的大小 C Vi e w : : O n M o v e ( ) 消息处理函数 移动视窗口 第7条 C Vi e w : : 重载函数 通知视已经创建了窗口 第8条 O n C h i l d N o t i f y ( ) C Vi e w : : O n S h o w Wi n d o w ( ) 消息处理函数 显示视窗口 第11条 为子框架设置视. . . C C h i l d F r a m e : : O n M o v e ( ) 消息处理函数 移动子框架窗口 第7条 C C h i l d F r a m e : : O n S i z e ( ) 消息处理函数 设置子框架窗口大小 第7条 C C h i l d F r a m e : : 重载函数 重新定位框架窗口内的视和控制条 R e c a l c L a y o u t ( ) C Vi e w : : 重载函数 在所预期的客户区大小基础之上确定整个窗口的大小 C a l c Wi n d o w R e c t ( ) 完成打开文档 . . . C D o c u m e n t : : 重载函数 调用下列函数来装载已有文档的缺省实现 O n O p e n D o c u m e n t ( ) C D o c u m e n t : : 重载函数 初始化C D o c u m e n t的成员变量 第4 1条 D e l e t e C o n t e n t s ( ) C D o c u m e n t : : 重载函数 使用M F C的串行化特征装载文件到成员变量 S e r i a l i z e ( ) C D o c u m e n t : : 重载函数 设置被装载文件的路径名 S e t P a t h N a m e ( ) C D o c u m e n t : : S e t Ti t l e ( ) 重载函数 设置文档标题 C Wi n A p p : : 重载函数 在F i l e菜单中的最近打开文件的路径列表添加 A d d To R e c e n t F i l e L i s t ( ) 文件的路径名 C Vi e w : : 重载函数 通知视第1次初始化 第4 2条 O n I n i t i a l U p d a t e ( ) C Vi e w : : O n U p d a t e ( ) 重载函数 只要文档发生改变即被调用,以更新视 第4 3条 激活子框架和视. . . 380第第第四部分第附 录 下载附录A第消息和重载顺序第第381下载 (续) 处理函数 类 型 描 述 参见注释 C C h i l d F r a m e : : 重载函数 用于激活主框架窗口 A c t i v a t e F r a m e ( ) C C h i l d F r a m e : : 重载函数 用于激活子框架窗口 O n M D I A c t i v a t e ( ) C Vi e w : : 消息处理函数 发送该消息用于激活视 O n A c t i v a t e Vi e w ( ) C Vi e w : : O n K i l l F o c u s ( ) 消息处理函数 从视中去除键盘焦点. . . 第2 6条 C C h i l d F r a m e : : 消息处理函数 . . .添加到子框架窗口 O n S e t F o c u s ( ) C C h i l d F r a m e : : 消息处理函数 显示子框架窗口 第11条 O n S h o w Wi n d o w ( ) C Vi e w : : 消息处理函数 发送该消息用于激活视 O n A c t i v a t e Vi e w ( ) A.5.3 保存MDI文档 以下序列将已经修改的文档保存到文件。 表A-20 保存M D I文档 处理函数 类 型 描 述 参见注释 C D o c u m e n t : : 消息处理函数 单击主菜单中的 S a v e命令后发出 第4 6条 O n F i l e S a v e ( ) C D o c u m e n t : : 重载函数 调用以下函数保存文档到文件的缺省实现 第4 7条 O n S a v e D o c u m e n t ( ) C D o c u m e n t : : 重载函数 用M F C的串行化特色将成员变量串行化到文件 第4 8条 S e r i a l i z e ( ) C D o c u m e n t : : 重载函数 设置保存文件的路径名 第4 9条 S e t P a t h N a m e ( ) C D o c u m e n t : : S e t Ti t l e ( ) 重载函数 设置保存文档的文档名 第4 9条 C Wi n A p p : : 重载函数 在F i l e菜单中的最近打开文件的路径列表增加 第4 9条 A d d To R e c e n t F i l e L i s t ( ) 文件的路径名 A.5.4 关闭MDI子框架 下列序列发生在关闭 M D I子框架的情况下。该序列假设该子框架并没有修改过的文档。 参见表A - 1 9的序列用于保存M D I文档。 表A-21 关闭M D I子框架 处理函数 类 型 描 述 参见注释 C C h i l d F r a m e : : 消息处理函数 单击了子框架窗口的关闭按钮之后发出 第5 0条 O n C l o s e ( ) C D o c u m e n t : : 重载函数 调用下面的函数以确定是否可以关闭子框架 C a n C l o s e F r a m e ( )(续) 处理函数 类 型 描 述 参见注释 C D o c u m e n t : : 重载函数 检查文档是否被修改过,如果是将保存它 第5 2条 S a v e M o d i f i e d ( ) C D o c u m e n t : : 重载函数 调用以下函数的缺省实现 O n C l o s e D o c u m e n t ( ) C D o c u m e n t : : 重载函数 释放文档内存 第4 1条 D e l e t e C o n t e n t s ( ) CChildFrame:: 重载函数 销毁子框架窗口 D e s t r o y Wi n d o w ( ) CChildFrame:: 消息处理函数 隐藏子框架窗口 第11条 O n S h o w Wi n d o w ( ) CChildFrame:: 消息处理函数 使子框架窗口当前不被激活 O n M D I A c t i v a t e ( ) C Vi e w : : 消息处理函数 使子框架内的视当前不被激活 O n A c t i v a t e Vi e w ( ) C Vi e w : : O n K i l l F o c u s ( ) 消息处理函数 从视中去除键盘焦点 第2 6条 C C h i l d F r a m e : : 消息处理函数 用于销毁子框架窗口 第1 9条 O n D e s t r o y ( ) C Vi e w : : O n D e s t r o y ( ) 消息处理函数 用于销毁视窗口 第1 9条 C Vi e w : : 重载函数 在视窗口销毁后调用 第2 1条 P o s t N c D e s t r o y ( ) C Vi e w : : 重载函数 告诉文档它的一个视已经删除了 O n C h a n g e d Vi e w L i s t ( ) CChildFrame:: 重载函数 在子框架销毁后调用 第2 1条 P o s t N c D e s t r o y ( ) 52) 该函数的缺省实现将打开一个文件对话框,要求命名保存文件的路径。如果不保存文 件,而是保存到数据库或者系统注册表,就必须重载该函数而不能使用缺省实现。 A.5.5 关闭MDI应用程序 以下序列发生于关闭 M D I应用程序的时候。该序列假设所有的 M D I文档都没有修改。参 见表A - 2 0了解子框架被关闭时的序列。 表A-22 关闭M D I文档 处理函数 类 型 描 述 参见注释 C M a i n F r a m e : : 消息处理函数 单击了关闭按钮或者F i l e菜单下的E x i t 第5 0条 O n C l o s e ( ) 命令发送该消息 C Wi n A p p : : 重载函数 为每个打开的文档调用S a v e M o d i f i e d ( )函数 S a v e A l l M o d i f i e d ( ) CMainFrame:: 消息处理函数 隐藏主框架窗口 第11条 O n S h o w Wi n d o w ( ) C M a i n F r a m e : : 消息处理函数 使主框架当前不被激活 O n A c t i v a t e ( ) CMainFrame:: 消息处理函数 使应用程序当前不被激活 O n A c t i v a t e A p p ( ) 382第第第四部分第附 录 下载附录A第消息和重载顺序第第383下载 (续) 处理函数 类 型 描 述 参见注释 CMainFrame:: 重载函数 主框架窗口被告之已经销毁 D e s t r o y Wi n d o w ( ) C M a i n F r a m e : : 消息处理函数 发送该消息以销毁主框架窗口 第1 9条 O n D e s t r o y ( ) CMainFrame:: 重载函数 在主框架窗口销毁后调用 第2 1条 P o s t N c D e s t r o y ( ) C Wi n A p p : : 重载函数 允许释放分配给应用程序的所有资源 E x i t I n s t a n c e ( ) A.6 定制 以上描述的各个序列是针对 3种标准应用程序的:对话框、 S D I和M D I,使用S e r i a l i z e ( )函 数进行标准的文档装载和保存。显然,还有许多种改变序列的情况。例如,使用数据库而不 是使用S e r i a l i z e ( )函数,在框架窗口内装载对话条— 但要将各种情况全部列出来将覆盖全书 的内容。在本附录中列出以上内容是希望使读者认识到 M F C实际所进行的工作,而且还希望 读者意识到,不仅可以改变其缺省行为,而且还能创建自己的序列。特别是: • 可以创建混合了对话框、S D I和M D I的应用程序。 • 可以用文档模板来创建想要的任何文档,不仅仅是单击 N e w或者O p e n的结果。 A.6.1 混合应用程序 打算创建什么样的应用程序经常会使人焦虑。但实际上可以干脆创建对话框、 S D I和M D I 的混合应用程序。例如,是否打算要应用程序在某些场合作为对话框应用程序,而在另一场 合就成为M D I应用程序?为此,必须修改 C Wi n A p p的I n i t I n s t a n c e ( )函数。 例如,如果有一个 M D I应用程序,并希望它偶尔能够以对话框应用程序的面目出现,来 完成一些自动创建处理工作,那么可以如下修改 I n i t I n s t a n c e ( )函数: 如果命令行标记设置为 - D,运行以下代码将创建对话框应用程序。 CExampledlgDlg dlg; m_pMainWnd = &dlg; int nResponse = dlg.DoModal(); return FA L S E ; : 否则执行下列代码启动一个 M D I应用程序。 C M u l t i D o c Template* pDocTe m p l a t e ; p D o c Template = new CMultiDocTe m p l a t e ( I D R _ E X A M P LT Y P E , RUNTIME_CLASS( CExamplemdiDoc ), RUNTIME_CLASS( CChildFrame ), // custom MDI child frame RUNTIME_CLASS( CExamplemdiView ) ) ; A d d D o c Template( pDocTemplate ); CMainFrame* pMainFrame = new CMainFrame; if ( !pMainFrame -> LoadFrame( IDR_MAINFRAME ) ) return FA L S E ;m_pMainWnd = pMainFrame; pMainFrame -> ShowWindow( m_nCmdShow ); pMainFrame -> UpdateWi n d o w ( ) ; 具体请参考实例4。 A.6.2 使用文档的乐趣 M F C类是用预定义的命令消息 I D _ F I L E _ N E W、I D _ F I L E _ O P E N和I D _ F I L E _ S AV E,它们 提供了自动装载以及保存文档的方法。但是,可以只通过调用 C D o c Te m p l a t e的O p e n - DocumentFile ()成员函数,在任何时候装载一个文档 /框架/视组合。 例如,当要打开 M D I应用程序中的一个文档的时候,可能打算同时打开另一个文档,只 需保存以后想打开的模板即可,如下所示: m _ p S a v e D o c Template = new CMultiDocTe m p l a t e ( I D R _ E X A M P LT Y P E , RUNTIME_CLASS( CExamplemdiDoc ), RUNTIME_CLASS( CChildFrame ), // custom MDI child frame RUNTIME_CLASS( CExamplemdiVi e w ) ) ; 然后重载O p e n D o c u m e n t F i l e ( )成员函数并添加下列代码: m _ p S a v e D o c Template -> OpenDocumentFile( xxx ); 384第第第四部分第附 录 下载下载下载 附录B 绘 图 结 构 本附录将说明3种绘图相关文件格式的设计信息:图标文件、位图文件和对话框模板。每 种描述的前面将讲述这种文件的通常结构,随后将在每一部分着重按照字节顺序来讲解。 B.1 图标和光标 图标和光标文件使用一种公共的文件格式。单类型代码 (在文件头中)与双类型代码是不同 的。图标和光标文件都可能包含多个对象的信息。从最抽象的角度看,它们由下列两部分组 成。 • 文件头部分 • 图片数据部分 但是,对于文件中的所有图标 (或光标)来说,每部分都包括一个重复的结构。 B.2 文件头部分 文件头的开始几个字节说明了文件的类型以及文件中包含的对象数目。 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 1 =图标,0 0 0 2 =光标 0 0 0 4 文件中包含的图标和光标的数目 在这种“整体”信息的后面,紧跟着文件中包含的图标或光标的“目录”。 单独的图标和光标头 “目录”中的每个项目是为单个对象指定维数和颜色格式的更详细的头结构。这些项目还 包括指向实际位图数据的指针。下列结构对于文件中的每个图标或光标都会重复一次。 0 0 0 0 宽度 高度 0 0 0 2 2 = B & W,1 6 = 1 0色,0 = 2 5 6色 0 0 0 4 光标热点的x值 0 0 0 6 光标热点的y值 0 0 0 8 图标/光标数据的大小 0 0 0 a “ 0 0 0 c 图标/光标数据在文件中的偏移 0 0 0 e “ B.3 图片数据部分 文件的这个部分对于每个图标或光标都有一个独立的位图数据。每个对象的图片数据由三部分结构组成:头、颜色表和一个像素值表。这三部分结构对文件中的每个图标和光标都 会重复一次。 B.3.1 图片数据头 这个4 0字节的头说明了对象的图片特征:大小、形状以及彩色平面的数目。 0 0 0 0 头大小( 4 0字节) 0 0 0 2 0 0 0 4 图标/光标宽度 0 0 0 6 0 0 0 8 高度* 2 0 0 0 a 0 0 0 c 平面数 0 0 0 e 位数据像素指针的大小 0 0 1 0 0 0 1 2 0 0 1 4 第1个平面中的字节数 0 0 1 6 0 0 1 8 第2个平面中的字节数 0 0 1 a 0 0 1 c 第3个平面中的字节数 0 0 1 e 0 0 2 0 第4个平面中的字节数 0 0 2 2 0 0 2 4 第5个平面中的字节数 0 0 2 6 B.3.2 颜色表 仅当对象不是单色时,才有颜色表。 DWORD RGB数据 每种颜色一个项目 B.3.3 位数据 在彩色图像中,表中的每个项目对应颜色表中的一个 R G B值。在单色图像中,每个项目 的值可以为0或1 (黑或白)。 386第第第四部分第附 录 下载在颜色表中的像素指针 每一个像素对应一项 如果是单色,则只是0或1 B.4 注意 • 可以用图标或光标编辑器(Icon or Cursor Editor)为应用程序创建图标或者光标。在创建 新图标或者新光标时,这些编辑器创建缺省为 3 2×3 2像素的1 6色图标或者3 2×3 2像素的单色 光标。要创建其他类型的图标和光标,可单击 Developer Studio的I m a g e和New Device Image⋯ 菜单命令以打开 New Image对话框。如果要创建的图标或者光标还没有被列出,则单 C u s t o m 按钮创建自己的图标或者光标。用这个办法可以创建彩色光标。在创建了图标或者光标之后, 可以删除缺省的图标或者光标,做法是:在编辑器的组合框内选中缺省图标或者光标,然后 单击S t u d i o的I m a g e和Delete Device Image菜单命令。 • 当前还没有Windows API或者M F C类函数可将文件中的数据直接转变为图标或者光标。 不过可以创建一个内存文件,以此提供给相关的图标或者光标装载函数使用。 B.5 位图文件 位图( . b m p )文件的结构类似于图标或光标文件中的位图部分。通过最抽象的观点看,每个 位图文件由下列四部分构成。 • 文件头 • 位图头 • 颜色表 • 图片数据 B.5.1 文件头 文件头结构( B I T M A P F I L E H E A D E R的结构类型)包括一个文件类型标记 (“B M”)以及文 件大小、布局信息。 0 0 0 0 “B M” 0 0 0 2 文件字节大小 0 0 0 4 “ 0 0 0 6 0 0 0 0 0 0 0 8 0 0 0 0 0 0 1 0 图片数据在该文件中的偏移量 0 0 1 2 “ B.5.2 位图头 文件头的后面是位图信息头 ( B I T M A N I N F O H E A D E R的结构类型)。该结构指定了维数、 压缩类型以及图像的颜色格式。 附录B第绘 图 结 构第第387下载0 0 0 0 位图头字节大小( 4 0 ) 0 0 0 2 0 0 0 4 位图宽度像素值 0 0 0 8 位图高度像素值 0 0 1 0 平面数,必须为1 0 0 1 2 每个像素的位数 (参见B . 5 . 5 ) 0 0 1 4 压缩类型 0 =无 0 0 1 6 压缩后的图像大小 0 0 1 8 水平像素/米 0 0 1 c 垂直像素/米 0 0 2 0 颜色表中使用的颜色(参见B . 5 . 5 ) 0 0 2 4 颜色表中的重要颜色 B.5.3 颜色表 根据可用颜色数目的不同 (参见B . 5 . 5 ),颜色表的大小也不同。使用 2 4位色时,将省略颜 色表,因为图片数据的每个项目将显式地解码 R G B值。 DWORD RGB 数据 每一种颜色对应一项 B.5.4 图片数据 图片数据是一个像素值表 (可能是压缩的)。根据所使用的颜色方案的不同,解压缩的像素 值将进行不同的解释。 • 如果是单色,数据为二进制位序列,每一个像素对应一位, 0 =黑色、1 =白色。 • 如果是1 6色或2 5 6色,数据为颜色表中4或8位的指针序列,每一个像素对应一个指针。 • 如果是高彩色或真彩色,数据为 2 4位R G B值序列,每一个R G B值定义了一种像素颜色。 B.5.5 注意 位图头包含了一个“每个像素的位数 (bits per pixel)”值,该值确定颜色表的大小以及图 像数据代表什么,该值可以是 1、4、8和2 4,含义如下: • 如果该值为1,这就是单色位图。没有颜色表,图像数据只是一系列的 0和1,前者代表 黑色,后者代表白色。 • 如果该值为4或者8,这就是1 6或者2 5 6色位图。1 6色位图的颜色表是 1 6双字长,2 5 6色 位图的颜色表则是 2 5 6双字长。但如果位图头中“ #颜色表中使用的颜色 (of colors used in Color Ta b l e )”这一项的值不是0,那么该值将确定颜色表内应该具有多少项。 • 如果该值是2 4,这就是真彩色位图。因为图像数据本身包含了一系列完整的 R G B数值定 388第第第四部分第附 录 下载义位图内每个像素的颜色,所以位图内就没有颜色表。 • 有关位图和其他文件格式的详细信息,请参阅 h t t p : / / w w w. w o t s i t . o rg站点。 B.6 对话框模板 对话框模板包括一系列对话框信息,后跟多个部分的控制信息,其中每个控件包含一个 控制信息。对话框信息存放在 D L G T E M P L AT E结构中,后跟可选的 F O N T I N F O结构。控制信 息存放在D L G I T E M T E M P L AT E结构中。模板具有下列的一般结构。 • 对话框信息 • 头 • 菜单 • 类 • 标题 • 字体 • 控制信息 • 控件 • : : : • 控件n B.6.1 对话框信息 尽管对话框信息大多都包含在一个 D L G T E M P L AT E结构中,但它对查看包括菜单、类、 标题和字体信息的对话框信息来说是很有用的。 1. 头 对话框的风格、位置和大小都存放在 D L G T E M P L AT E结构的头部分中。模板的这部分还 包括对话框中的控件数目。 0 0 0 0 风格 0 0 0 2 0 0 0 4 扩展风格 0 0 0 6 0 0 0 8 控件数 0 0 1 0 对话框单元( D U )的x位置 0 0 1 2 D U的y位置 0 0 1 4 D U的宽度 0 0 1 6 D U的高度 2. 菜单 尽管菜单信息仅仅只是 D L G T E M P L AT E结构的一个字段,但这个字段对真正的 C结构来 说还是比较复杂的。有三种可互换的形式: 如果没有则为0 或 - 1 附录B第绘 图 结 构第第389下载菜单资源的o r d i n a l (参见B . 7 . 1 ) 或 菜单资源的u n i c o d e名 0 3. 类 类信息使用一个相似的变换形式存放。并且,它也有三种可替换的形式: 如果使用默认值则为0 或 - 1 类名的o r d i n a l (参见B . 7 . 1 ) 或 类名的u n i c o d e名 0 4. 标题 如果对话框有标题栏,该字段则必须包括此标题。如果没有标题栏,该字段则为空。 如果没有则为0 或 u n i c o d e标题 0 5. 字体 字体信息存放在一个单独的 F O N T I N F O结构中。仅当在对话框的风格参数中设置了 D S _ F O N T后,才有此结构。 指针大小 u n i c o d e字体名 B.7 控制信息 控制信息是一个顺序的结构,每个控件一个。每个结构具有下列形式。 • 头 • 类 • 标题 • 创建数据 头、类和标题信息存放在一个 D L G I T E M T E M P L AT E结构中。创建数据对每个控件类来说 是唯一的。 1. 头 D L G I T E M T E M P L AT E结构的几个字段构成 D L G T E M P L AT E结构的头信息。这些字段指 定风格、位置、大小和控件的 I D。 0 0 0 0 风格 0 0 0 2 0 0 0 4 扩展风格 390第第第四部分第附 录 下载0 0 0 6 0 0 0 8 对话框单元( D U )的x位置 0 0 1 0 D U的y位置 0 0 1 2 D U的宽度 0 0 1 4 D U的高度 0 0 1 6 控件I D 2. 类 该字段指定控件的类,可以作为 o r d i n a l,也可以作为可变长度的字符串。可以使用的格 式为: - 1 类名的o r d i n a l 其中: 80 = “B U T TO N” 81 = “E D I T” 82 = “S TAT I C” 83 = “L I S T B O X” 84 = “S C R O L L B A R” 85 = “C O M B O B O X” 或 类名的u n i c o d e : : : 0 3. 标题 控件可以使用可变长度的名称或一个 o r d i n a l标识。可使用下列形式: 如果没有则为0 -1 资源的o r d i n a l 可以是字符串、图标或位图 或 u n i c o d e标题 : : : 0 4. 创建数据 如果无,则为0,否则为数据的字节数 对每个控件都不同,创建控件时通过 C r e a t e E X ( )传递。 注意 • Ordinal是一个内部指针系统,用于资源编辑器以最小空间访问资源 (字符串、位图和图 附录B第绘 图 结 构第第391下载标等)。由于这些指针在内部分配,所以如果要自行对它们进行解释将会遇到麻烦。 • 用上面的结构可以动态地创建、编辑或者组合对话框模板。为了装载已有的模板供编辑, 使用如下代码: HRSRC hResource = ::FindResource( AfxGetInstanceHandle(), l p s z TemplateName, RT_DIALOG ); HGLOBAL hTemplate = LoadResource( AfxGetInstanceHandle(), hResource ); D L G T E M P L ATE *pTemplate = ( DLGTEMPLATE* )LockResource( hTemplate ); p T m p l a t e指针现在指向对话框模板,它可以由 m e m c p y ( )函数来操作,然后由下面修改的 模板创建一个对话框,使用如下代码: CreateDlgIndirect( pTemplate, pWndOwner, AfxGetInstanceHandle() ); // also run extra creation data ExecuteDlgInit( MAKEINTRESOURCE( IDD ) ); 确保使用以下代码完成清除工作: UnlockResource( hTemplate ); FreeResource( hTemplate ); 392第第第四部分第附 录 下载下载
还剩416页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

xwgg

贡献于2014-04-28

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf