C++ Builder6 编程实例精解


C++Builder 6 编程 实例精解 赵明现 编著 For C++Builder@smth zmx@smth bbs 中国·北京 2004 年 2 月 内容简介 C++Builder 6是一款快速开发 Win32 应用程序的可视化开发工具,利用它可以实现高性 能的执行效率和出色的底层控制。C++Builder 6 相比于 5 以前的版本,提供了更多的网络开 发组件,更快速方便的可视化环境。 本书共有 14 章,前三章分别介绍 C++Builder 的开发环境、比较特殊的语法,并在第三 章用实例讲述使用 C++Builder 开发应用程序的过程,以后的章节均通过实例讲述 C++Builder 某个方面的编程技术与技巧。这些实例包括文本、绘图与图象处理、文件、多媒体、系统, 以及 FTP 和 WinSock 网络通信。在第 12 章通过俄罗斯方块游戏介绍 VCL 游戏的制作,并 在第 13 章介绍游戏开发中常用的 DirectX 技术。在第 14 章讲述数据库程序的设计,并详细 讲解餐厅结账管理程序的制作过程。 本书是为对 C++Builder 有基本了解的程序员或程序爱好者而做,读者需要熟悉 C/C++ 编程。本书也可以作为软件开发人员的参考书。 前 言 Borland C++Builder 是 Borland 公司推出的全新 32 位 Windows 开发工具。C++Builder 不 仅继承了 Delphi 使用简便,功能强大,效率高等特点,而且它还结合 C++语言所有优点。 C++Builder 可以说是至今最好的 Windows 开发工具。 本书共 14 章,通过实例清晰明了的讲解 C++Builder 常用的编程技术技巧。前两章介绍 了 C++Builder 开发环境、几个特殊的语法;第三章通过列主元高斯消去法求解方程组的程序 的制作,介绍开发应用程序的一般过程和程序调试技术,其中使用到了动态控件数组技巧; 第四章制作文本处理程序,介绍菜单组件、文本组件以及工具栏等组件的使用,还讲述 MDI 程序设计方法,以及实现拖放效果的技巧;第五章制作绘图程序,介绍图形图象类组件的使 用以及图象的处理技巧;第六章介绍文件相关的操作,基于文件操作编写学生成绩管理程序, 并利用可视化文件管理组件以及 TTreeView 和 TListView 组件制作资源管理器;第七章制作 屏幕保护程序,介绍屏保程序实现的技术技巧,并讲述图象的处理和显示技巧;第八章制作 多媒体播放器,介绍多媒体组件的使用,并详解资源文件的使用方法以及使用资源文件绘制 自定义窗体的技巧;第九章制作系统信息管理程序,主要介绍在 C++Builder 中使用 Windows API 的原理与方法;第十章介绍 TNMFTP 组件的使用并基于此组件制作 FTP 工具;第十一 章利用 C++Builder 中的 WinSock 编程技术制作远程屏幕监视程序,还介绍了任务栏图标的使 用技巧;第十二章介绍 VCL 游戏制作的一般方法,以及程序帮助文件的制作方法;第十三 章介绍游戏编程常用的 DirectX 技术;第十四章介绍数据库程序的制作方法,以及设计报表 的方法和软件封面制作技巧,制作了功能比较完善的餐厅结账管理程序。 本书适合熟练 C/C++并且对 C++Builder 有一定使用经验的读者,旨在锻炼读者制作有实 用价值的较大规模的应用程序的能力,并可作为软件开发人员的参考书。 在本书的编写过程中,刘道理、陶静等都为我提供了很大的帮助,在此向他们表示衷心 的感谢! 由于作者水平有限,而且时间比较仓促,书中难免有错误和不当之处,恳请读者批评指 正。 赵明现 2004年 3 月 《C++ Builder 6 编程实例精解 赵明现》 目 录 前言 第1章 C++ BUILDER 概述 本章重点 本章介绍 C++Builder 的特点,使你对 C++Builder 有个一般的认识;介绍 C++Builder 的集成开发环境,让你对其使用有个大概的了解;还简单介绍了帮助系统的使用以及 C++Builder 在未来的可能发展趋势;还介绍了 C++Builder 中几个有意思的复活节彩蛋 学习目的 熟悉 c++ builder 的历史以及其编程环境 1.1 C++Builder 的特点 1.2 C++Builder 6 集成开发环境 1.2.1 主窗口 1.2.2 对象列表树和对象查看器 1.2.3 代码编辑器 1.2.4 窗体 1.2.5 工程管理器 1.3 C++Builder 6 中的复活节彩蛋 1.4 帮助系统 1.5 C++Builder 的未来 1.6 思考题 第2章 语法摘述 本章重点 本章讲述 C++Builder 中编程常用到的比较特殊的语法。包括几种抽象数据类型, 和几种特殊的函数 学习目的 掌握 C++Builder 中常用的一些特殊语法和函数 2.1 抽象数据类型 2.1.1 TList 类 2.1.2 AnsiString 类 2.1.3 Set(集合) 2.1.4 DynamicArray(动态数组) 2.1.5 TStream(流) 2.2 函数 2.2.1 系统函数 2.2.2 Inline 函数 2.2.3 参数个数不定的函数 2.3 思考题 目录 5 第3章 程序设计基本流程 本章重点 本章通过列主元消去法求解方程组的程序的编写过程,讲述利用 C++Builder 编写 应用程序的一般流程。 学习目的 熟悉从问题提出到程序发行的各个步骤 3.1 应用程序设计流程 3.2 算法与界面设计 3.2.1 算法 3.2.2 界面设计 3.3 代码实现 3.3.1 头文件 3.3.2 维数输入确认的处理 3.3.3 BtnInputOver 的响应 3.4 程序的调试 3.4.1 调试选项设置 3.4.2 程序执行方式 3.4.3 断点的使用 3.4.4 变量的监视 3.4.5 其它调试命令 3.5 程序的发行 3.6 思考题 第4章 文本处理程序 本章重点 本章讲述制作一个文本处理程序的过程。在文本处理程序中,设计到了菜单、文本 组件、多文档技术、工具条、状态栏等组件的用法以及相关的技巧 学习目的 本章通过制作一个文本编辑器,熟悉 c++builder 中菜单、文本组件的使用;在编辑 器中实现多文档编辑功能,使读者熟悉多窗体程序的编写和控制;文本编辑功能的实现用来 熟练程序中对文本的处理技巧;学会程序中用到的菜单融合、文件拖放等技巧 本章典型效果图 4.1 菜单的使用 4.1.1 菜单 4.1.2 菜单的设计 4.2 文本组件的使用 4.2.1 TEdit 组件 4.2.2 TMaskEdit 组件 4.2.3 TMemo 组件 4.2.4 TRichEdit 组件 4.3 多文档技术 4.3.1 MDI 程序设计技术要点 4.3.2 子窗体的管理 目录 6 4.4 界面的创建 4.4.1 主窗体与子窗体 4.4.2 工具条与状态栏 4.4.3 菜单的融合 4.5 文本编辑功能的实现 4.5.1 文件操作 4.5.2 字体、段落、查找与替换 4.5.3 剪贴板的使用及其它 4.5.4 右键菜单 4.6 高级技巧---拖放功能的实现 4.7 思考题 第5章 画图程序 本章重点 本章讲述了 C++Builder 中的图象处理技术。主要内容有 TScreen 类、TCanvas 类、 TPen 类、TBrush 类、TImage 组件的使用,光标的使用,图形文件的操作,剪贴板的使用以 及图象处理的一些高级技巧 学习目的 掌握 C++Builder 中与图形相关的组件和类的使用,掌握在程序中使用光标的方法, 以及图形文件操作和图象处理的技巧 本章典型效果图 5.1 基本图形图像类 5.1.1 TScreen 类 5.1.2 TCanvas 类 5.1.3 TPen 类 5.1.4 TBrush 类 5.2 TImage 组件的使用 5.2.1 绘图函数 5.2.2 文件相关的函数 5.3 界面的创建 5.3.1 窗体外型配置 5.3.2 光标的设置 5.4 画图功能的实现 5.4.1 设计思路 5.4.2 工具栏 5.4.3 颜色选择框 5.4.4 对鼠标事件的响应 5.5 图象的复制、粘贴和文件操作 5.5.1 图象的复制、粘贴和剪切等 5.5.2 文件操作 5.6 图形的缩放、反色及其它 5.6.1 图象的缩放和反色 目录 7 5.6.2 自定义前景色与背景色 5.6.3 “查看”菜单的响应 5.6.4 程序的初始化 5.6.5 程序的关闭 5.7 思考题 第6章 学生成绩管理&资源管理器 本章重点 本章通过学生成绩管理程序,介绍 C/C++/C++Builder 中文件的操作和使用;介绍 了 Win31 选项卡中关于磁盘文件的组件,并利用 Win32 类的组件编写资源管理器 学习目的 掌握文件的操作和使用,熟悉链表的使用,掌握 TListView、TTreeView 的使用 本章典型效果图 6.1 文件操作函数 6.1.1 C 中文件的操作 6.1.2 C++中文件的操作 6.1.3 C++Builder 中文件的操作 6.2 学生成绩管理程序 6.2.1 界面设计 6.2.2 程序逻辑结构 6.2.3 学生信息的数据结构 6.2.4 对 TabSet1、BitBtn1、BitBtn2 及各 Edit 的响应 6.3 可视化文件管理组件 6.3.1 Win31 组件 6.3.1 Win32 组件 6.4 资源管理器 6.4.1 界面的设计 6.4.2 功能的实现 6.5 思考题 第7章 屏幕保护程序的制作 本章重点 本章介绍屏幕保护程序的制作技术,包括屏保的预览、运行设置以及正常运行效果 三部分;还介绍了制作屏保程序需要使用的文字、图象处理和显示等技巧 学习目的 掌握获取命令行参数的方法,掌握注册表的使用方法,熟悉字幕、图象的特殊处理 和显示技巧 本章典型效果图 7.1 屏保制作的关键技术 7.1.1 命令行参数的获取和处理 7.1.2 注册表的使用 7.2 屏保程序的结构 7.2.1 窗体的设计 7.2.2 命令行参数的处理 目录 8 7.2.3 运行设置的功能实现 7.2.4 屏保正常运行的功能实现 7.2.5 屏保程序的运行效果 7.3 字幕技巧 7.3.1 立体文字效果 7.3.2 旋转文字效果 7.4 图象处理技巧 7.4.1 图象的柔化 7.4.2 图象的锐化 7.4.3 浮雕效果 7.4.4 图象的旋转 7.5 图象显示技巧 7.5.1 中心扩散效果 7.5.2 百叶窗效果 7.5.3 推拉效果 7.5.4 交错效果 7.5.5 雨滴效果 7.6 思考题 第8章 多媒体播放器 本章重点 本章介绍多媒体技术的使用,并使用 TMediaPlayer 组件制作一个媒体播放器;另 外,播放器的界面完全由自己绘制,这种美化窗体界面的处理方法也是很重要的一部分工作 学习目的 了解多媒体技术的概念,掌握 TMediaPlayer 组件的使用;掌握资源文件的建立和 使用,以及利用图形资源文件实现自定义窗体的方法 本章典型效果图 8.1 多媒体技术概论 8.1.1 音频与视频 8.1.2 动画、图象与文本 8.1.3 媒体控制接口(MCI) 8.2 TMediaPlayer 组件的使用 8.3 程序界面设计 8.4 资源文件的使用 8.4.1 .rc 文件的建立 8.4.2 可以通过 Windows API 函数直接访问的资源 8.4.3 能通过 API 使用的资源 8.4.4 不能通过 API 直接使用的资源 8.5 自定义窗体的实现 8.5.1 窗体界面的绘制 8.5.2 窗体的拖动 8.5.3 标题栏按钮的事件响应 目录 9 8.6 媒体播放功能的实现 8.6.1 媒体播放控制按钮的响应 8.6.2 媒体播放时间的显示 8.6.3 进度条的控制 8.6.4 OnNotify 事件的响应 8.6.5 视频显示窗口的功能 8.7 思考题 第9章 系统信息管理程序 本章重点 本章首先讲述 Windows 系统的运行机制以及 C++Builder 中如何处理消息和使用 API 函数,然后主要通过对 API 函数的使用来制作系统信息管理程序 学习目的 理解 Windows 的消息驱动机制和 C++Builder 中消息的获取和处理;掌握关于窗口、 进程、磁盘、内存、环境变量等的 API 函数的使用;熟悉多页组件的使用 本章典型效果图 9.1Windows API 使用基础 9.1.1 Windows 的运行机制 9.1.2 C++ Builder 对消息的处理 9.1.3 消息的截取和处理 9.1.4 自定义消息的发送 9.1.5 API 的使用 9.2 界面设计 9.2.1 主窗体界面的设计 9.2.2 程序总体结构 9.3 窗口和进程 9.3.1 页面中组件的添加 9.3.2 当前窗口的获取 9.3.3 当前进程的获取 9.3.4 右键菜单和进程的终止 9.4 系统和设备 9.4.1 磁盘驱动器 9.4.2 内存 9.4.3 设备 9.5 环境变量 9.6 思考题 第10章 FTP 工具制作 本章重点 本章通过 FTP 工具的制作讲述 TNMFTP 组件的使用,并复习 TListView 组件的使 用和工具栏的使用 学习目的 了解 FTP 的概念,掌握 TNMFTP 组件的使用,复习 TListView 组件使用和文件、 文件夹的操作 目录 10 本章典型效果图 10.1 FTP 概述 10.2 TNMFTP 组件 10.2.1 TNMFTP 组件的功能 10.2.2 TNMFTP 的属性、方法和事件 10.3 界面设计 10.4 功能实现 10.4.1 登陆信息对话窗 10.4.2 ListViewLocal 的实现 10.4.3 与服务器的连接 10.4.4 ListViewRemote 的实现 10.4.5 PopupMenuLocal 和 PopupMenuRemote 菜单的响应 10.4.6 对 NMFTP1 各种事件的响应 10.5 思考题 第11章 远程屏幕监视程序 本章重点 本章介绍远程屏幕监视程序的制作,包括屏幕图象的获取以及利用 WinSock 传输 图象。介绍了 WinSock 编程的概念,以及 C++Builder 中与 WinSock 相关的几个类;实现利 用 API 函数对屏幕图象的获取和格式的转换;实现利用 WinSocket 相关的组件传输图象;最 后介绍任务栏图标的使用 学习目的 了解 WinSock 编程的一般概念,熟悉 C++Builder 中关于 WinSock 的类和组件的使 用;学会利用 API 函数实现对屏幕图象的截取以及 bmp 图象的格式转换;掌握任务栏图标的 使用 本章典型效果图 11.1 WinSock 编程概述 11.1.1 WinSock 概述 11.1.2 建立服务器端 Socket 11.1.3 建立客户端 Socket 11.2 操纵 Socket 对象实现数据传输 11.2.1 TCustomWinSocket 类 11.2.2 TServerWinSocket 类 11.2.3 TClientWinSocket 类 11.2.4 TServerClientWinSocket 类 11.3 界面设计 11.3.1 服务器端 11.3.2 客户端 11.4 服务器端功能的实现 11.4.1 API 函数介绍 11.4.2 屏幕图象的截取 11.4.3 客户端命令的提取与图象的发送 目录 11 11.4.4 服务器端 Socket 其它事件的响应 11.4.5 Socket 服务的开启和关闭 11.5 客户端功能的实现 11.5.1 连接参数的设置 11.5.2 “连接”菜单的响应 11.5.3 命令的发送和返回图象的读取 11.5.4 客户端 Socket 其它事件的响应 11.5.5 其它菜单的响应 11.6 任务栏图标的使用 11.7 思考题 第12章 俄罗斯方块 本章重点 本章通过俄罗斯方块游戏的制作示例一个完整的 Windows 游戏程序的开发过程, 还介绍了制作 hlp 帮助文件的方法 学习目的 掌握对实际问题进行模块划分和分别实现的方法;掌握帮助文件的制作方法以及在 程序中启动帮助文件的方法 本章典型效果图 12.1 界面设计 12.2 游戏逻辑结构和数据组织 12.2.1 游戏的结构分析 12.2.2 Square 类的定义 12.2.3 Blocks 类的定义 12.2.4 MainFrame 类的定义 12.2.5 TetrisGame 类的定义 12.3 各类的具体实现 12.3.1 Blocks 类的实现 12.3.2 MainFrame 类的实现 12.3.3 TetrisGame 类的实现 12.4 键盘、定时器和菜单的控制 12.4.1 键盘的控制 12.4.2 定时器的控制 12.4.3 菜单的控制 12.4.4 其它 12.5 帮助文件的制作 12.5.1 RTF 文件 12.5.2 创建俄罗斯方块游戏帮助文档的 RTF 文档 12.5.3 HPJ 的创建 12.5.4 编译生成 hlp 文件 12.5.5 在游戏中启动帮助 12.5.6 Tetris 游戏中帮助的启动 目录 12 12.6 思考题 第13章 制作 DIRECTX 动画 本章重点 本章讲述利用 DirectX 技术实现动画的方法。DirectX 技术是一个很优秀的 Windows 游戏开发接口,DirectX API 基于 COM 建立,可以处理 2D、3D 图象、声音、各种输入设备、 网络功能等。本章介绍 DirectDraw、DirectSound、DirectInput 等常用技术的使用 学习目的 掌握 DirectDraw、DirectSound、DirectInput 等 DirectX 技术的使用,掌握动画制作 方法 本章典型效果图 13.1 DirectX 简介 13.1.1 DirectX 的特点 13.1.2 DirectX 的结构和组成 13.2 DirectX 使用基础 13.2.1 DirectDraw 的使用 13.2.2 DirectSound 的使用 13.2.3 DirectInput 的使用 13.3 窗体和资源 13.4 程序的实现 13.4.1 程序结构 13.4.2 头文件 13.4.3 初始化 13.4.4 帧图片的绘制 13.4.5 界面恢复 13.4.6 程序运行效果 13.5 图形操作函数的实现 13.6 思考题 第14章 餐厅管理软件 本章重点 本章介绍餐厅结账管理程序的制作。详细讲解 BDE 的使用,数据表的创建和设置, 以及 C++Builder 中数据库相关组件的使用,介绍了利用报表组件设计统计报表的具体过程, 还有制作软件封面的技术 学习目的 掌握 BDE 的使用方法、掌握利用 Database Desktop 创建和设置数据表的方法、掌 握 TTable、TDataSource、TDBGrid 等组件的使用、掌握数据库的查找、修改等操作的方法、 掌握制作 Master/Detail 类型和 List 类型报表的方法以及制作软件封面的技术 本章典型效果图 14.1 C++Builder 数据库程序开发基础 14.4.1 BDE 简介 14.4.2 数据库表的建立 14.4.3 C++Builder 数据库程序的结构 14.2 TTable 组件 目录 13 14.2.1 TTable 组件的属性和方法 14.2.2 TTable 的事件 14.3 餐厅结账管理程序的数据库设计 14.4 界面设计与功能实现 14.4.1 主界面 14.4.2 菜品、菜谱数据库维护 14.4.3 餐桌库维护 14.4.4 已点菜单库维护 14.4.5 结账库维护 14.4.6 职员信息和权限库维护 14.4.7 点(加、退)菜 14.4.8 结账 14.4.9 登陆 14.5 报表 14.5.1 餐厅职员分类统计报表 14.5.2 菜谱销售分类统计报表 14.5.3 账单统计报表 14.6 软件封面的制作 14.7 思考题 《C++ Builder 6 编程实例精解 赵明现》 第 01 章 C++Builder 概述 本章重点 C++Builder是一个强大的可视化开发工具,是灵活的 C++语言和随心所欲的可视化开发 完美结合的产物。 本章介绍 C++Builder 的特点,使你对 C++Builder 有个一般的认识;介绍 C++Builder 的 集成开发环境,让你对其使用有个大概的了解;还简单介绍了帮助系统的使用以及 C++Builder 在未来的可能发展趋势;介绍了 C++Builder 中几个有意思的复活节彩蛋。 学习目的 通过本章的学习,您可以: ■ 对 C++Builder 的特点和发展趋势有大致的了解 ■ 了解 C++Builder 的集成开发环境 第 1 章 C++Builder 概述 15 1.1 C++Builder 的特点 随着 Windows95/98、Windows NT 系统的出现,编程工作也进入了 Win32 编程时代, Windows 漂亮的图形用户界面、多媒体技术、几乎不受限制的内存资源、多任务并发编程等 技术无不激励着程序设计人员的 Win32 编程的欲望。 但是,即使是要编写一个不执行任何操作的图形窗口,也要写下五六十行的代码,更不 用说对界面的美化了。这是因为在进行 Windows 编程时,要面对的是微软窗口软件开发工具 包(Software Development Kits,SDK)中几百个 Windows API 函数以及数大本厚厚的设计手 册,如此繁重的工作使得编写 Windows 程序变的毫无乐趣可言,而且也大大降低了编程的效 率。 这种情况下,软件公司引入了 RAD(Rapid Application Development,快速应用程序开发 环境)程序开发工具,集成在 IDE(Integrated Development Environment)开发环境中,使得 程序设计者可以在 Windows 环境下,快速的开发出窗口相关的应用程序。Visual Basic、Delphi 正是其中的表表者,它们通过引入控件(Controls)或组件(Components)使得开发 Windows 应用程序变得象累积木一样容易。特别是 Delphi,其简洁、灵活以及强化大的功能,无不张 扬着 Borland 程序员天才的智慧。 Inprise(原 Borland)公司推出的 Turbo C、Turbo C++、Borland C++以及 Borland C++ Builder,无不是 C/C++编程者所钟爱的编程工具,而且每一个都称得上经典之作,Delphi 更 是 RAD 开发工具中最受钟爱的编程工具。C++ Builder 中则嵌入了 Delphi 中使用的高效的 VCL(Visual Component Library,可视化组件库),使得开发人员不必要在 C++高效的底层控制 和轻松的 VCL 编程环境之间作出选择。 安装 BCB6 对系统的最低要求如下: • Intel Pentium II/400 MHz 或兼容产品 • Microsoft Windows 98、2000 (SP2)或 XP • 128 MB RAM (建议 256 MB) • 750 MB 硬盘空间(完全安装) • CD-ROM 驱动器 • SVGA 或更高分辨率显示器(800x600,256 色) • 鼠标或其他指示设备 BCB6 的主要特性如下: • 通过 Web 服务简化企业到企业集成 • 提高 Web 应用开发的功能和速度 • 构建支持 Web 服务的高性能中间件 • 利用业届标准的优势,支持 SOAP、XML、WSDL 和 XSL 等协议 • 为 Windows 和 Linux 操作系统构建可跨平台运行的应用程序 • 通过高性能的 32 位源代码编译器提高效率 • 支持 IBM DB2、Informix、Oracle、Sybase、MySQL 、dBASE、Paradox 和 Borland 第 1 章 C++Builder 概述 16 InterBase 等数据库 可以看出 BCB6 在 Web 服务的应用中做了比较多的工作,增添了利用 C++进行电子商务的 快速开发的功能。 1.2 C++ Builder 6 集成开发环境 缺省状态的开发界面如图 1-1: 图 1-1 C++Builder6 开发界面 它由五部分组成,最上方为主窗口,下方左侧为对象列表树和对象检查器,右方为设计窗体 和代码编辑器。 小技巧: 每次启动 C++Builder 之后,都会弹出一个新建的工程,如果要取消启动后新建工程这个 操作,可以将启动 C++Builder 的快捷方式的属性中添加 –np,如: "F:\Program Files\Borland\CBuilder6\Bin\bcb.exe" –np 第 1 章 C++Builder 概述 17 1.2.1 主窗口 主窗口位于最上方,包括主菜单、工具栏的加速按钮以及组件选项板。 工具栏位于主菜单下方的左侧,有两排,常用的有第二排左侧的“View Unit”、“View Form”和“Toggle Form/Unit”。其中“View Unit”用于查看工程中的 Cpp 等源文件,快捷键 为 Ctrl+F12;“View Form”用于查看工程中的窗体,快捷键为 Shift+F12;“Toggle Form/Unit” 用于切换当前编辑的窗体和它对应的源文件,快捷键为 F12,在编程过程中使用最多。 工具栏中的“Run”、“Pause”等按钮用于程序的调试。Run 的快捷键为 F9。 主菜单中包含着各种命令操作。 图 1-2 主窗口 如图 1-2,主窗口下部右侧为组件选 一个多页面的结构,不同种类的组件放 在 项卡,它是 置 不同的页面中。在选项板中点击右键,选择 Properties 菜单,可以在弹出的属性对话窗 中编辑选项卡中各个页面以及页面中各个组件之间的顺序,也可以对页面和页面中的组件进 行删除和添加。 第 1 章 C++Builder 概述 18 图 1-3 选项卡属性编辑窗 1.2.2 对象列表树和对象检查器 图 1-4 对象列表树 树中。在窗体中组件比较多时,或者在窗体中不易用鼠标选中所添加的组件时,列表树就变 对象列表树中列出了窗体中添加的各个组件,以结构树的形式清晰的表示出各个组件之 间的关系。如果一个容器中也有其它组件,那么容器中的组件将以子节点的形式显示在列表 第 1 章 C++Builder 概述 19 得很有用了。 图 1-5 对象检查器 属性检查器上部是一个下拉菜单,从下拉菜单中可以选择窗体中的各个组件。下部是属 性和事件两个选项卡。在事件选项卡中,左侧为组件可以响应的事件类型,鼠标点击右侧可 以为对应的事件选择响应函数,如果程序中没有定义合适的函数,那么双击右侧的空白就会 自动创建响应函数。 第 1 章 C++Builder 概述 20 1.2.3 代码编辑器 图 1-6 代码编辑器 中定义的类及其属性和方法; 卡可以对不同单元的源文件进行浏览和编辑,对于同 一个单元,可以点击下方的按钮选择其 cpp 文件和头文件。 在代码编辑器中点击右键,选择属性菜单,可以在“Editor Properties”窗口中定义自己 喜欢的编辑方式,如更改 General 页中的 Tab Stops 属性,可以更改按 Tab 键光标前进的字符 数,在 Colors 页中可以更改对不同部分的代码的显示颜色。 1.2.4 窗体 窗体(Form)是在设计程序时,用来规划程序界面的地方。它是所见即所得的模式,在 选项卡上选择想要添加的组件,然后用鼠标在窗体上点击,即可添加该组件,选中窗体上的 组件即可通过属性检查器更改组件的属性和其事件处理过程。 对于组件的移动和大小的控制,可以通过鼠标操作完成,但是鼠标操作时,窗体的大小 代码编辑器由两部分组成,左侧是类浏览器,其中列出代码 右侧为代码编辑窗口,点击上方的选项 第 1 章 C++Builder 概述 21 和位置都是对齐到网格的,如果想要实现组件大小和位置的微调,方法之一就是在属性检查 器中更改器大小和位置的相应属性,另一个常用的方法就是用 Ctrl+光标键来实现组件位置微 调,用 Shift+光标键实现组件大小的微调。 如果要取消对齐到网格的功能,或者更改网格的大小,可以打开主菜单“Tools”-> “Environment Options…”,在弹出的对话窗中选择 Designer 选项卡,如下: 图 1-7 环境设置对话窗 其中“Display grad”决定是否在设计时显示网格(这些网格在执行程序 时是不显示的), Snap to grad”决定是否将组件位置和大小在鼠标拖动时对齐到网格,“Grid size”决定窗体 上网格的大小。 现,在按 Tab 键时,获得焦点的组件是有一定的顺序的,这个顺序在 组件的先后顺序决定,也可以更改。在窗体上点击右键菜单中的“Tab rder “ 运行程序时,会发 设计期间由添加 O …”,弹出“Edit Tab Order”对话框,在对话框中列出了窗体中能够获得输入焦点的组 件,按照其 Tab Order 的先后从上到下排列,选择相应的组件,点击右侧的箭头按钮可以更 改选中组件的 Tab Order。 第 1 章 C++Builder 概述 22 图 1-8 Tab Order 对话窗 1.2.5 工程管理器 图 1-9 工程管理器 通过 View 菜单的 Project Manager 菜单项打开工程管理器(快捷键是 Ctrl+Alt+F11),通 过它可以添加和删除工程中 的单元或相关的其它工程。一些协同工作的工程可以通过它加入 到同一个工程组中,通过工程组,可以操作和组织相互依赖的工程,例如多层次应用,或者 共同执行的动态链接库与可执行程序。 第 1 章 C++Builder 概述 23 如果工程中有与其它工程或程序共享的文件,就可以通过工程管理器清楚的看到各个文 件的路径,对于何时备份已经完成的程序很有帮助。 1.3 C++Build C++Builder6 中有一些很好玩的彩蛋,它们大都跟 Delph er 6 中的复活节彩蛋 i4 的彩蛋差不多。 在命令行模式下的 bcc32 命令后使用-Team 作为参数,会列出编译器的制作者名单,如 下: E:\Documents and Settings\alpher>bcc32 -Team Borland C++ 5.6 for Win32 Copyright (c) 1993, 2002 Borland It's the compiler team! John Wiegley Eli Boling Yooichi Tagawa Dawn Perchik Mark Alexander Lee Cantey Maurice Barnum Alastair Fyfe 在 bcc32 的 help 命令后加 me 或者 me!,输出如下: E:\Documents and Settings\alpher>bcc32 -help me! Borland C++ 5.6.1 for Win32 Copyright (c) 1993, 2002 Borland But I'm having too much fun! 第 1 章 C++Builder 概述 24 图 1-10 C++Builder6 正常启动界面 程序源文件之前如果使用#pragma curious_george、#pragma gpfault prettyplease、#pragma keek 果。不过最好玩的是,如果在启动 C++Builder6 之前按下 Ctrl+Shift 键, 照片。不过需要注意的是,因为在输入法中 Ctrl+Shift 能已经被使用,所以如果你按下它们没有出现照片的话,注意打开输入法设置窗口,去掉 trl+Shift 的热键。 如图 1-10 是正常启动时的启动画面,而按下 Ctrl+Shift 时,启动画面变为: a 也会有一些好玩的效 启动画面会变成开发小组的合影 可 C 第 1 章 C++Builder 概述 25 图 1-11 启动彩蛋 小技巧: 如果你觉得 C++Builder 启动太慢,可以打开主菜单 Componet|Install Packages,把列表 中用不到的 Packages 勾掉(记住不是 Remove,否则就勾不回来了) 1.4 帮助系统 不管是熟练的编程者,还是刚入门的初学者,联机帮助的使用都是很有用的,而且,往 往越是经验丰富、编程熟练的人,对帮助文件的使用也越多,越熟练。学会如何使用帮助, N 做的好,而且常常出 是程序员必备的素质。 其实常看到有人抱怨说 C++Builder 的帮助做的不好,没有 MSD 错,帮助文件里面有错误我倒是遇到过,不过,有帮助总比没帮助好,学会使用帮助,里面 的一些错误,也还是比较容易看出来的。 打开帮助的方法有两种,可以点击到组件上,然后按 F1 键,也可以通过 Help 菜单打开 帮助。 第 1 章 C++Builder 概述 26 图 1-12 帮助索引 意,C++Builder 中类名的前面一般都有一个 T。打开帮 有类的属性、方法、事件等相应的连接。 Help 菜单中的 Windows SDK 帮助,利用它可以查询各 Win32 系统转向.NET,.NET 跟 Win32 系统有本质的 的 Win32 API,随之而来的,程序开发工具也必然不 不 年推出的 Windows.NET 平台的开发工具有三个, uilder 与 C++Builder X,Borland 公司产品结构的这一变化正是由于 Windows DOS 系统向 Window 系统的转型,对于软件 向.NET 升级。Borland 公司的 上,所以面对.NET 的冲击,一方面, 主力,而另一方面,C++Builder 则开始向另一个方向发展,力图成为一种平台无关的 在查找相关类的帮助时,需要注 助之后,在上方会 在使用到 API 的帮助时,应使用 个 API 函数的声明,各个入口参数的说明等信息。 1.5 C++Builder 的未来 现在 Windows 操作系统已经开始由 区别,逐渐的,微软也将不会再提供新 得 向.NET 转型。Borland 公司在 2003 Delphi.NET、C#B 操作系统的变革引起的。 Windows 操作系统向.NET 的转型,类似于 开发工具提供商,都是一个很大的冲击,各种编程工具都争相 Delphi 和 C++Builder 都深深的根植与 Win32 的 API 之 Delphi 推出.NET 版本,随着操作系统一起升级,和 C#Builder 一起成为.NET 平台下 Borland 公司的 第 1 章 C++Builder 概述 27 原生开发工具。 与 Delphi 是由同一个开发小组开发的,这也是 Delphi 和 er 的开发分道扬镳。 不向.NET 过渡,而是要走平台无关这条路?这一方面是因为不管操 dows 中,C/C++在底层的操作和控制中都有其不可替 的 让 C++向.NET C++,一出现便恶 智之举。 熟悉 C++Builder 的都应该知道,C++Builder 的 VCL 库是用的 Delphi 的,它的代码都是 人都会觉得 C++Builder 其实是 Delphi 的 C++版本,C++Builder 的向下兼容性。在 C++Builder X 中,将不再提 ,C++Builder X 中的 framework 将是全新的,完全由 C++完成、跨平台的 framework, GUI 部分则是要基于第三方的跨平台组件库 wxWindows,当然,现在的 C++BuilderX C++Builder X 的 IDE 将使 无关性,必然大大的促进 。Borland 公司作为创造了无数经典的老牌公司,其技术和 件开发公司,所以,由 待着 Borland 公司再在 C++ 一个经典吧! 思考题 C++Builder 的特点 r 集成开发环境包括哪些部分,都有什么功能? z z z C++Builder X 之前的版本 C++Builder 一直都是轮流进行版本升级的原因,面对.NET 的转型,Borland 公司成立了专门 的开发小组,从此 Delphi 和 C++Build 为什么 C++Builder 作系统如何发展,在 UNIX/Linux,Win 代 作用,而另一方面,由于 C++语言本身的原因,它不适合向.NET 过渡。要 过渡,必然要对 C++进行大幅度的修改,象 Visual Studio.NET 的 Managed 评如潮,可见,Borland 走 C++平台无关这条路确是明 用 Object Pascal 写成,所以很多 X 的推出,很受关注的一个问题就是其 VCL 供 VCL 而其 的 1.0 版本中并没有 GUI 和 RAD 功能,这将在 2.0 版本中提供。 用 JBuilder 的 IDE, C++Builder X 如果成功,其影响力是无可置疑的,由于其平台 C++程序的在应用领域的开发潜力 经验足够的强大,而且,它一直以来都是世界级的完全平台无关的软 它开发完全平台无关的 C++BuilderX 再合适不过,我们就耐心的期 开发工具中创造 1.6 z z C++Builde 自己尝试修改集成开发环境的配置 如何使用 C++的帮助? C++Builder 的未来发展趋势如何? 《C++ Builder 6 编程实例精解 赵明现》 第 02 章 语法摘述 Builder 中编程常用到的比较特殊的语法。包括几种抽象数据类型,和几种 ,您可以: Set、DynamicArray 和 TStream 的使用 本章重点 本章讲述 C++ 特殊的函数。 学习目的 本章的学习通过 ■ 掌握 TList、AnsiString、 ■ 掌握常用的几种系统相关的函数 ■ 熟悉 Inline 函数 ■ 了解参数不定的函数的使用 第 2 章 语法摘述 29 2.1 抽象数据类型 类的方式提 常有用的抽象数据类型。包括 TList(链表)类、 si TStream(流)类。在程序编 写过 形链表,可以用来存储任意类的对象,包括用户自定义的类。虽然它是链 的数组,可以通过其 Items 属性象访问一个数组那样实现 T 方法实现对链表的各种操作, 、查找、排序以及提供链表元素数目(Count 属性)等属性。 z TList 对象中的元素个数,并为新增加的元素分配内存。如果没有足 空间,将会抛出一个 EOutOfMemory 异常。 ist 对象中 Items 属性 元 决定,Capacity 自动随之更改。如果需要多次的执行添加操作, 么 Add 方法都要重新指定内存,从而提高程序的 行 ; 执行 Add 之前先指定了 Capacity 属性,这样就避免了循环中 Add 的分 分配内存时的异常。 = {read=FCount, write=SetCount, nodefault}; 大小。对 Count 属性的更改可 以实 空指针的数目。如果要删除数组中的空指针,可以使 T 。 z C++Builder 中以 An String 类、Set(集合)类、DynamicArray(动态数组)类和 要的作用。程中,这几个类的使用在很多地方都起到很重 供了几个非 2.1.1 TList 类 TList 实现了线 表,但是它实际上是一个存放指针 对 List 对象的每一个元素的访问。TList 类提供了足够的属性和 如全套插入、删除 (1)TList 类的主要属性有: Capacity 属性 __property int Capacity = {read=FCapacity, write=SetCapacity, nodefault}; 设定 Capacity 指定 够的内存为新元素分配 这里需要注意 Capacity 属性和 Count 属性的不同。Count 属性是 TL 的 素个数,而 Capacity 是 TList 类可以包含的元素个数,由分配了多少的内存 总是大于或者等于 Count。 将一个元素插入链表,其 Capacity 属性会 那 可以先设置 Capacity,这样就能避免每次 执 效率,如下面的代码: List->Clear(); List->Capacity = Count for (int I = 0; I < Count; I++) List->Add(...); 代码中,在循环 配内存的操作,提高了程序的效率,而且,也避免了 z Count 属性 __property int Count 标志 TList 对象中的 Items 数组的个数,也即 Items 数组的 现对数组末尾处元素的添加和删除。需要注意的是 Count 属性并不一定是 TList 对象中 元素的个数,它也包含 Items 数组中的 用 List 的 Pack 方法 Items 属性 第 2 章 语法摘述 30 __property void * Items[int Index] = {read=Get, write=Put}; 通过 Items 属性可以象数组那样访问链表中的元素。 z terList; 要注意的是,不管 Items 数组 有 数组的末尾添加元素。 清除链表中的所有元素。此方法执行之后,将释放所有内存,置 Capacity 为 0。 dex); 删除数组中下标为 Index 的元素。Delete 方法并不释放元素占用的内存,也即不改变 方法 如果 Count 属性还没有达到 Capacity,则此方 Capacity 等于 Count,则按照下面规则增加空间:Capacity>8,则将 acity>4 且 Capacity<9,将 Capacity 增加 8;Capacity<4,则将 Capacity sert(int Index, void * Item); 。 x, int NewIndex); 动到 NewIndex 位置,此操作将覆盖原来 NewIndex 位置的元 List 属性 typedef void *TPointerList[134217727]; typedef TPointerList *PPoin __property PPointerList List = {read=FList}; 获取直接进入列表中元素数组的指针。 (2)TList 类的主要方法有: z Add 方法 int __fastcall Add(void * Item); Add 方法在 TList 对象的数组末尾添加一个元素,返回值为添加的元素的数组下标。如 果添加后 Count 大于 Capacity,那么将会重新为之分配内存。需 中 没有 NULL 元素,Add 方法都只是在 z Clear 方法 virtual void __fastcall Clear(void); z Delete 方法 void __fastcall Delete(int In Capacity 的值。 Exchange 方法 z void __fastcall Exchange(int Index1, int Index2); 交换链表中两个元素的位置。 z Expand TList* __fastcall Expand(void); Expand 方法为 TList 链表增加内存空间。 法不做任何操作,如果 Capacity 增加 16;Cap 增加 4。 z IndexOf 方法 int __fastcall IndexOf(void * Item); 查找列表中指定元素的数组下标。 z Insert 方法 void __fastcall In 在列表中的指定位置添加元素 z Move 方法 void __fastcall Move(int CurInde 将 CurIndex 位置的元素移 素。 第 2 章 语法摘述 31 z oid); 素。此操作并不释放内存,即只会改变 Count 性 m); 。返回值为元素删除之前的位置。 emove 方法的参数都是列表的元素对象,其示例如下: void TL A ry { pList->Add(TheObject); // 添加 AnsiString 实例到 pList 列表 表中有 " + IntToStr(pList->Count) + "个元素", rmation, TMsgDlgButtons() << mbOK, 0); z stSortCompare)(void * Item1, void * Item2); ort(TListSortCompare Compare); in { *)Item1)->Name, ((TComponent *)Item2)->Name); } vo 1::Button1Click(TObject *Sender) { Text); } Pack 方法 void __fastcall Pack(v 删除 Items 列表中所有指针值为 NULL 的元 属 的值,不会改变 Capacity 属性。 z Remove 方法 int __fastcall Remove(void * Ite 根据元素的值删除列表中的指定元素 Add 方法和 R __fastcall TForm1::Button1Click(TObject *Sender) { ist *pList = new TList(); nsiString TheObject = "This is an object." t MessageDlg("列表中有" + IntToStr(pList->Count) + "个元素", mtInformation, TMsgDlgButtons() << mbOK, 0); pList->Remove(TheObject); MessageDlg("列 mtInfo } __finally { delete pList; } } Sort 方法 typedef int __fastcall (*TLi void __fastcall S 通过自定义的比较函数 Compare 来对列表中的元素进行排序。 如下面代码: t __fastcall CompareNames(void *Item1, void *Item2) return CompareText(((TComponent id __fastcall TForm List1->Sort(Compare 第 2 章 语法摘述 32 2.1.2 Ans 类 仿照 Object Pascal 语言中的长字符串类型设计的一个字符串 类 siString 类型。比较 C 质上,AnsiString 仍 然 ,以’\0’作为字符串的结尾标志,只是在字符串的前面添加了几个 字 定义了一系列操作符,包括赋值、比较等操作,具体如下: z z z 较:“==”、“>”、“<”、“<=”、“>=”、“!=” z AnsiString 一个字符的下标为 1,而不是 0。 c_str 方法 __fastcall c_str() const; 返回与 AnsiString 相同字符内容的 C/C++标准字符串指针,即以’\0’(NULL)作为结尾标 志 AnsiString 字符串还没有赋值,那么此方法将返回一个空字符串””。此方 法 字符串的读取,如果要修改字符串,应该使用其 Insert、Delete 方法,或者[]操作 。 z Delete 方法 果 index 大于字符串的长度,则此方法 不删除任何字符;如果 count 大于从 index 开始的剩余字符数目,则此方法删除从 index 开始 所 c AnsiString __fastcall FormatFloat(const AnsiString& format,const long double& value); Form -1234 0.5 0 iString AnsiString 是 C++Builder 中 ,在 C++Builder 中,几乎所有用到字符串的地方都无一例外的使用 An ++中提供的字符串类型,AnsiString 提供了更多实用、方便的方法。本 采用 C/C++的字符串结构 节的信息头。 AnsiString 赋值:“=” 字符串连接:“+”、“+=” 字符串比 访问:“[]”,用此操作符可以象数组一样访问字符串中的字符,不同的是,在 中,第 AnsiString 类的常用的方法有: z char* 的字符串。如果 主要用于 符 AnsiString& __fastcall Delete(int index, int count); 在字符串中删除从 index 开始的 count 个字符。如 的 有剩余字符;如果 count 小于 0,此方法不删除任何字符。 z FormatFloat stati 将 value 用 format 指定的格式转换为字符串形式。典型的格式如下: 表 2-1 FormatFloat 常用格式示例 at string 1234 1234 -1234 0.5 0 0 1234 -1234 1 0 0.0 1234 -1234 .5 0 1234.00 -1234.00 0.50 0.00 #.## #,##0.00 1,234.00 -1,234.00 0.50 0.00 #,##0.00;(#,##0.00) 1,234.00 (1,234.00) 0.50 0.00 #,##0.00;;Zero 1,234.00 -1,234.00 0.50 Zero 第 2 章 语法摘述 33 0.000E+00 1.234E+03 -1.234E+03 5.000E-01 0.000E+00 #.# #E-0 1.23 Insert 方法 AnsiString& __fastcall Insert(const AnsiS # 4E3 -1.234E3 5E-1 0E0 z tring& str, int index); 串 str。 IntToHex 方法 static AnsiString __fastcall IntToHex(int value, int digits); 为 16 进制的字符串形式。 判断字符串是否为空串。如果为空,返回 true,否则,返回 false。 符串中的字符数,字节为单位。 static AnsiString __fastcall LoadStr(int ident); static AnsiString __fastcall LoadStr(HINSTANCE hInstance, int ident); 中读取标志为 ident 的字符资源。如果没有指定句柄,则从当前模块(如可执 & __fastcall LoadString(HINSTANCE hInstance, int ident); 从资源中读取字符串,与 LoadStr 不同之初在于他们的返回值的类型。 const; rCase() const; 符串中一个子串 subStr 的开始位置。如果字符串中不包含指定的子串,则返回 0。 AnsiString& __fastcall SetLength(int newLength); 将字符串的 Length 属性改为 newLength。 如:AnsiString s = AnsiString::StringOfChar('A', fastcall SubString(int index, int count) const; 在 AnsiString 字符串的 index 位置插入字符 z 将一个整数转换 z IsEmpty 方法 bool __fastcall IsEmpty() const; z Length 方法 int __fastcall Length() const; 返回字符串的长度,即字 z LoadStr 方法 从指定句柄 行程序)中读取。 z LoadString 方法 AnsiString z LowerCase/ UpperCase 方法 AnsiString __fastcall LowerCase() AnsiString __fastcall Uppe 返回一个包含原 AnsiString 字符串中所有字符的新字符串,且所有字符都是小/大写形式, 原字符串的内容不改变。 z Pos 方法 int __fastcall Pos(const AnsiString& subStr) const; 返回字 z SetLength 方法 z StringOfChar 方法 static AnsiString __fastcall StringOfChar(char ch, int count); 返回一个包含 count 个字符 ch 的字符串, 10);则字符串 s 为"AAAAAAAAAA"。 z SubString 方法 AnsiString __ 第 2 章 语法摘述 34 返回字符串的一个子串,子 符串中位置 index 开始的 count 个字符组成。 ToDouble/ ToInt/ ToIntDef 方法 st; 无可用的转换字符,产生异常。 字符串转换为一个整型数,不同之处在于如果字符串没有 抛出一个异常,后者则会返回默认值 defaultValue。 z 字符串,新字符串中删除了原字符串中前导和后随的空格字符,以及字符串 AnsiString __fastcall TrimLeft() const; ) const; TrimLeft 和 TrimRight 分别用于删除字符串前导和后随的空格字符以及控制字符。 +类模板,仿照 Delphi 中的 Set 类而得,用来实现集 数,其中 type 用来指定集合元素的类型(如 int、char、枚举变量 minval 为集合元素的最小值,minval 不能小于 0;maxval 为集合元素的最大值,它不能 于 255。 每个 Set 的实例都根据其三个参数创建相应的对象,如下面的代码,s1 和 s2 因为其参数 Set s2; 对其进行 Z'; // 初始化 tcall Clear(); 串由原字 z double __fastcall ToDouble() con ToDouble 方法将字符串转换为一个浮点数。如果字符串 int __fastcall ToInt() const; int __fastcall ToIntDef(int defaultValue) const; ToInt 和 ToIntDef 方法都是将 可用的字符,前者会 Trim/ TrimLeft/ TrimRight 方法 AnsiString __fastcall Trim() const; 返回一个新 中的控制字符。常用于输出字符串时删除前面和后面的空格。 AnsiString __fastcall TrimRight( 2.1.3 Set(集合) Set是一个 C+ 合这个抽象数据类型。 此模板使用时有三个参 等); 大 不同而成为两个不同的集合类型: Set s1; if (s1 == s2) // ERROR; illegal struct. 如果要创建同一种集合类型的多个实例,可以使用 typedef,如下: typedef Set UPPERCASESet; 对集合变量的声明并不会对它的值初始化,可以在声明变量之后使用<<操作符 赋值,如下: UPPERCASESet s1; s1 << 'A' << 'B' << 'C'; //初始化 UPPERCASESet s2; s2 << 'X' << 'Y' << ' Set 集合常用的方法有: z Clear 方法 Set& __fas 第 2 章 语法摘述 35 删除集合对象中的所有元素,方法执行之后,集合为空集。 z Contains 方法 bool __fastcall Conta ins(const T el) const; pty() const; 比较运算符 “==”、“!=”,分别表示集合的两个对象相等和不等的比较运算。 分别为集合之间的并集、交 算符。 操作符为添加指定元素到集合,>>为从集合中删除指定元素。 ++Builder 的编程中对集合元素的使用非常的多,如判断键盘事件和鼠标事件时,需 Shift 参数,就是一个集合类型,typedef SetTShiftState。 stream> system.hpp> t LowerSet; ','j'> HalfLowerSet; ample() et ae, ae2, ac, de; rSet AE, AE2, AC, DE; t aj; 了集合对象,所以下面的两个 Contains 方法都返回 false ains('a')?"does":"does not") << " contain 'a' " << endl; Set AE " << (AE.Contains('C')?"does":"does not") << " contain 'C' " << endl; << 'a' << 'b' << 'c' << 'd' << 'e'; ae2 << 'a' << 'b' << 'c' << 'd' << 'e'; c b' << 'c'; de aj << " contain 'a' " << endl; 查询集合对象中是否含有指定的元素 el。 z Empty 方法 bool __fastcall Em 判断一个集合对象是否含有元素。 z z operator +/*/- 集、并集中交集的补集运算的运 类似于 C/C++中运算符+与+=的关系,集合运算中的运算符也对应的有+=、*=和-=。 z operator <> << 在 C 要处理的 下面一段代码示例出集合使用的方法: #include UpperSet; typedef Se typedef Set ArrayName; 数组元素的数据类型,它可以是任何数据类型、组件对象,也可以是动 态数组的大小可以通过其 Length 属性来获得和更改,如下: array cout 能进行比较*/ out << (de==aj?" == ":" != ") << endl; 利用+、-、*进行集合运算 cout << "Set ae " << (ae==(ac+de)?" == ":" != ") << "set ac + de." << endl; cout << "Set de " << (de==(ae-ac)?" == ":" != ") << "set ae - ac." << endl; / Clear 方法的调 ut << "Set ae2: " << ae2 << endl; ae2.Clear(); cout << "Set ae2: " << ae2 << " after ae2.Clear()" << endl; //+=、-=、*=进行集合运算 ut << "Union ae2+=ac: Set ae2 " << (ae2==ac?" == ":" != ") << "set ac." << endl; ae2-=ac; cout << "Difference ae2-=ac: Set ae2 " << (ae2==de?" = } int { set_example(); return 0; } .1.4 DynamicArray(动态 大小。 C++Builder template cla 声明动态数组时的语 其中 type 为动态 态数组对象,这样,便成为多维动态数组。 z Length 属性 动 DynamicArray arrayOfInt; OfInt.Length = 10; << "ArrayLength: " << arrayOfInt.Length << endl; 第 2 章 语法摘述 37 设置动态数组的 Length 为 0,将释放该动态数组占用的内存。 符。通过该操作符可以对指 ] = 'A'; co 范围 态数组最小位置和最大位置,也即它们给出动 数 下: nt>& arrayOfInt) h; i++) 属性总是返回 0,High 属性返回 Length-1,所以遍历一个动态数组也可以用下面的代码: “=”运算符,实现动态数组的赋值,但是,需要注意的是,将一个动态 态数组的操作仅仅是将它们的指针指向同一个位置,并不是赋值它们指向 的,对两个动态数组的比较,也是比较它们的指针位置,而不是指针指向 赋值动态数组的内容,应该使用动态数组的 Copy 或者 CopyRange 方法。示 如下: &i_array) nt> temp = i_array; 存2018香港马会开奖现场 20; //为数组中第一个元素赋值 assert(temp[0] == 20); // temp 数组可以看到 i_array 数组中的变化 temp = i_array.Copy(); // 拷贝 i_array 数组的内容到 temp 数组 temp[0] = 10; assert(temp[0] != i_array[0]); // 上面一句对 temp 的第一个元素赋值,它不改变 i_array 数组 z 数据访问 对于动态数组的访问,实现了跟静态数组一样的“[]”操作 定元素进行赋值和读取,示例如下: void InitArray(DynamicArray &c_array) { c_array[0 c_array[1] = 'B'; ut << "Third char is: " << c_array[2]; } z 动态数组的 动态数组的 Low 和 High 属性分别表示动 态 组的位置范围,如 int TotalArray(const DynamicArray CopyRange(int startIndex, int count) const; void CopyRange(DynamicArray &dst, int startIndex, int count) const; 它用来拷贝动态数组中从 startIndex 开始的 count 个元素到数组 dst 中,使用示例 newArray = array.CopyRange(3, 5); z 多维动态数组 前面已经说道,动态数组元素的类型可以也是动态数组,这样便 动态数组的每一维可以有不同的长度,也就是,对于每一维可以分别设定其 下面的代码: typedef DynamicArray< DynamicArray < AnsiString > > T2DStringArray void foo(T2DStringArray &s_array) { SetLength(s_array, 10); for (int { // 设 SetLength(s_array[i], i+1); for (int } } 2.1.5 TStream(流) 流类用于进行各种存储媒介之间的数据操作。TStr 特殊的存储媒介之间的数据交换 之外,在TStream类实例中还 两个属性,它们都是以字 TStream 是一个抽象类,不能由它直接声明实 TStream 类的派生类有用于文 用于对指定内存区域 TStream 类的主要属性和方法有: z Position 属性 该属性表示流文件中当 z Size 属性 第 2 章 语法摘述 39 __property __int64 Size = {read=GetSize, write=SetSize64, nodefault}; ,字节为单位。 ll CopyFrom(TStream* Source, __int64 Count); Source 流文件中读取 Count 字节的内容到当前流文件中,函数返回值为实际拷贝的字 orm1::Save1Click(TObject *Sender) cation->ExeName) + ExtractFileName(Edit1->Text); AnsiString Msg = Format("Copy %s to %s", ARRAYOFCONST((Edit1->Text,NewFileName))); if (MessageDlg(Msg, mtConfirmation, mbOKCancel, 0) == mrOK) new TFileStream(Edit1->Text, fmOpenRead); try Create); __finally { FreeAndNil(NewFile); } } __finally {//释放资源 FreeAndNil(OldFile); } } } z Read 方法 virtual int __fastcall Read(void *Buffer, int Count) = 0; 此方法用于从当前流文件的当前指针所在位置开始,读取 Count 字节的内容到 Buffer 内 存位置,返回值为实际读取的字节数。如果返回值小于 Count,则意味着还没有读够 Count 个字节的内容,指针位置已经到了流文件的末尾。其它的数据读取的方法,如 ReadBuffer 和 ReadComponent,都是通过对 Read 方法的调用来实现的。 表示流文件的大小 z CopyFrom 方法 __int64 __fastca 从 节数。示例如下: void __fastcall TF { AnsiString NewFileName = ExtractFilePath(Appli { TFileStream *OldFile = { TFileStream *NewFile = new TFileStream(NewFileName, fm try { //将 OldFile 拷贝到 NewFile 中 NewFile->CopyFrom(OldFile, OldFile->Size); } 第 2 章 语法摘述 40 z Seek 方法 virtual int __fastca (int O Word Origin); virtual __in 此方法用于将流文件的当前位置由 Origin 移动 Offset 个字节,返回值为流文件中新的当 前位置(Position 属性)。Origin 参数的取值可以为 soFromBeginning(流文件的开始位置)、 soFromCurrent (流文件的当前位置)或 soFromEnd(流文件的结尾位置)。 virtual int fastcall Write(const void *Buffer, int Count) = 0; 此方法用于将 Buffer 指向的内存中的 Count 个字节写入流文件中,并将当前指针向后移 靠调用 Write 来实现的。 void __fastcall TForm1::Button1Click(TObject *Sender) { TM emoryStream(); //将列表 ListB //重置流 pm //将流文件的内容写入 RichEdit 组件 RichEdit1->Lines->LoadFromStream(pms);. delete pms; .2 函数 .2.1 系统函数 随机函数 )extern PACKAGE Extended __fastcall RandG(Extended Mean, Extended StdDev); 此函数用于产生服从高斯分布的随机数,其中 Mean 为高斯分布的均值,StdDev 为标准 。 )extern PACKAGE double __fastcall RandomFrom(const double *AValues, int AValues_Size); extern PACKAGE int __fastcall RandomFrom(const int *AValues, int AValues_Size); extern PACKAGE __int64 __fastcall RandomFrom(const __int64 *AValues, int ll Seek ffset, t64 __fastcall Seek(const __int64 Offset, TSeekOrigin Origin); z Write 方法 __ 动 Count 个字节,返回值为写入的字节数。所有其它的数据写入方法,如 WriteBuffer 和 WriteComponent 都是 TMemoryStream 是 TStream 的派生类,它有两个比较重要的方法,SaveToStream 和 LoadFromStream,下面是它们的一个示例: emoryStream* pms = new TM 框的内容写入流文件 ox1->Items->SaveToStream(pms); 文件的当前位置到文件的开头 s->Position = 0; } 2 2 z (一 差 (二 第 2 章 语法摘述 41 AValues_Size); RandomFrom 函数用于从一个数组中随即取得其元素,参数 AValues_Size 为数组的大小 Values 为返回值的2018香港马会开奖现场。 ern PACKAGE void __fastcall Randomize(void); 初始化随机数产生器。在调用随机数产生函数之前使用。 )e t ATo); 一个从 AFrom 到 ATo(包含此值)的整数,如果 AFrom 大于 ATo,可以返 回负 内置的随机数种子,在调用随即函数之前,都需要调用一次 Randomize 函数 或者 )v t time *timep); u min; /* minutes */ u u * seconds */ }; me.h> c #incl { start = clock(); end = clock(); printf("The time was: %f\n", (end - start) / CLK_TCK); } (最后一个元素在数组中的位置),A (三)ext (四 xtern PACKAGE int __fastcall RandomRange(const int AFrom, constin 此函数返回 数。 (五)extern PACKAGE int RandSeed; 该变量存储 设置一个 RandSeed。 z 时间函数 (一 oid gettime(struc 获取系统时间。 (二)void settime(struct time *timep); 设置系统时间。 其中 time 结构如下: struct time { nsigned char ti_ nsigned char ti_hour; /* hours */ unsigned char ti_hund; /* hundredths of seconds */ nsigned char ti_sec; / (三)clock_t clock(void); typedef long clock_t; 此函数一般用于计算程序中某段代码的执行时间,如下: #include ude int main(void) clock_t start, end; delay(2000); return 0; 第 2 章 语法摘述 42 2.2.2 Inline 函数 返回。如果函数代码很少,很可能函数的执行时间还没有调用和返回操作 如果该函数需要被频繁的调用,就会有大量的时间被浪费掉。 率,可以将这些短小,但是需要频繁使用的函数定义为 Inline 函 接取代该函数,这样会增加程序的长度,但是在执 时 间换时间的做法。 数之前添加 Inline 标志,如下: lin c(void){return i;} 为 Inline 函数。 长或者是递归函数,则 作为一般函数处理。 编写函数代码,则跟在函数前添加 Inline 标志的作用相同,如: //global int X{ public: i;} //默认为 Inline 函数 }; 。 ap, type); void /* ca v 普通的函数,在程序执行过程中,需要花费一定的时间调用该函数,在函数执行结束也 要花费一定的时间 花费的时间多, 为了提高程序的执行效 数,在编译时,Inline 函数内的语句会直 行 ,可以省掉函数调用和返回的时间,是以空 Inline 函数的声明就是在函 in e char * X::fun 此函数是定义类 X 中的成员函数 func 在使用 Inline 函数时需要注意,如果定义的 Inline 函数中语句太 编译时会将它 如果在类的定义中直接 int i; class char * func(void) {return char *i; 2.2.3 参数个数不定的函数 定义函数时,可以使用函数的重载定义相同函数名,不同入口参数的函数,但是,有时 参数的个数在程序不同运行情况时是不同的,这时就可以使用参数个数不定的函数 可以使用 va_arg、va_end、 和 va_start 三个宏来处理不定长的函数参数列表,使用它们 需要包含头文件 stdarg.h,它们三个的使用格式如下: void va_start(va_list ap, lastfix); type va_arg(va_list va_end(va_list ap); 如下面代码,求解所有入口参数的代数和: #include #include lculate sum of a 0 terminated list */ void sum(char *msg, ...) { int total = 0; a_list ap; 第 2 章 语法摘述 43 int arg; v while ((arg = va_arg(ap,int)) != 0) { total += arg; } printf(msg, total); t m sum("The total of 1+2+3+4 is %d\n", 1,2,3,4,0); 0; 有哪些相同点?TList 与数组有哪些相同点? iString 类型的字符串转换为标准的 C 字符串? 型有什么要求? 的内容,说明如何利用流文件实现图片由 BMP 向 JPG 格式的转换 类型定义的函数有什么区别? 定参数个数的? a_start(ap, msg); va_end(ap); } in ain(void) { return } 2.3 思考题 z TList 与链表 z 如何将 Ans z C++Builder 中常见的组件属性里,哪些是集合类型的? z 动态数组的元素类 z 参看第 11 章 11.4 节 z Inline 函数与 Define z C 中哪些函数是不 《C++ Builder 6 编程实例精解 赵明现》 第 03 章 程序设计基本流程 方程组的程序的编写过程,讲述利用 C++Builder 编写应用程 而选择相应的算法实现之; 巧;还要为应用程 后,将应用程序发行供用户使用。 的方法 本章重点 本章通过列主元消去法求解 序的一般流程。 对于实际问题,需要通过对问题的分析转化为计算机问题,从 程序的编写和修改过程中,要使用到 C++Builder 中关于程序调试的技术技 序编写友好的交互界面,详尽的说明和帮助文档;最 学习目的 通过本章的学习,您可以: ■ 了解程序设计的一般步骤 ■ 熟悉利用 C++Builder 编写应用程序的过程 ■ 掌握利用 C++Builder 的调试工具调试程序 ■ 了解发行应用程序的方法 第 3 章 程序设计基本流程 45 3 应用程序设计流程 .1 以下几个阶段: 问题分析 究分析,转化为计算机可以处理的模式。一般将实际问 算机处理实现。 设计 不需要考虑使用何种 是简单的编写流程图等。对实际问题中分出的每个模块,都要设计其实现算法。 交互界面 一部分。界面的设计要根据实 户操作的接口,而且,界面要美观。 括算法的实现和界 写程序的过程中要注意写上足够的注释。 和修改 是语法错误,发生这种错误时程序无法成功编译;其次 要添加足够的注释内容以外,说明文档的编写也是不可缺少的。 程序,或者是小组合作开发的程序,说明文档的编写是必不可少的。 针对程序的使用者。说明程序各个功能的使用方法和注意事项等 应用程序 程序,直接编译成独立 ,对于规模比较大,或者需要系统支持的程序,则需要设计其安装程序。 方程程序的实现,演示用 C++Builder 编写应用程序的一般方 制作见第 12 章 12.5 节的内容,这里不再讲述。 算法与界面设计 方法中,基本的有高斯消去法。但简单的高斯消去在有零主元出现 解的精度严重受损。这些问题可以 在 C++Builder 中开发一个应用程序,一般要经过 z 对于实际生活中的问题,需要研 题分为多个模块,分别用计 z 算法 根据对实际问题的分析,设计解决各个模块的方法。在这个阶段, 语言实现,而 z 设计 在编写应用程序的过程中,交互界面的设计也是很重要的 际需要,提供用 z 根据算法和界面编写代码 根据算法的设计和交互界面的设计,编写具体的实现代码。代码中包 面的处理。编 z 调试 程序的调试包括三个方面:首先 是运行时错误,如除数为 0;最后是逻辑错误,它会造成非预料的运行结果。 z 设计帮助和说明文档 编写程序过程中,除了 特别是对于规模比较大的 帮助文件的编写主要是 内容。 z 发行 应用程序编写完成以后,要发布给用户使用。一般对于比较小的 可执行文件即可 本章通过列主元消去法求解 法。帮助文件的 3.2 3.2.1 算法 在求解线形方程组的 的时候,会导致算法失败,而小主元的出现又会导致计算 第 3 章 程序设计基本流程 46 通过调整方程的次序而很好的解决。在每次消元时选取列中绝对值最大的作为主元,即为列 的高速消去法的理论这里不多讲,请读者参看计算方法方面的书籍,如《科学和工 大学出版社,1999 年 8 月)。这里列出列主元高斯消去 i = k : n} 行) , k+1:n) – A(k+1:n , k)*A(k,k+1:n) 设计 工作与代码的实现相互影响,代码的实现方法决定 定代码中需要实现对它们的控制。 入方程的维数,然后程序根据方程维数设定相应的输 由用户控制一步一步的执行消元操作,直到方程解出为 ,界面中包括两部分内容,即用户输入方程维数的部分,和用户输入方程系数的部 放置两部分的内容,窗体设计界面如图 3-1。 Edit,一个 Button。Edit 的使用参看第四章的内容, 提示信息,Button 用来确认方程维数的输入, 的方程维数在 GroupBox2 上动态生成一些 Edit 组件供用户 入组件,同时也作为方程解的输出区域。其上有两个 输出 1-条件数和∞-条件数;一个 Button 用于一 后显示结果,在显示结果后跳转到输入方程维数求解另 程序运行时其 Align 属性都设为 alClient,即填满整个窗口。 为 false,即不可见,在求解方程时,GroupBox1 的 Visible 的 Visible 为 true,GroupBox2 的 Visible 为 false。 个组件的位置在程序中设定。 用于输入方程系数的 Edit 和 Label 等组件位置放在 GroupBox2 主元高斯消去法。 具体 程计算基础》(施妙根 顾丽珍,清华 法的算法如下: for k = 1:n-1 确定 p(k<=p<=n)使得 |A(p,k)|=max{|A(i , k)| : A(k , 1:n)ÅÆA(p , 1:n) (交换 k 行和 p u(k)=p (记录置换矩阵Pk) if A(k , k)≠0 A(k+1:n , k)=A(k+1:n , k)/A(k , k) A(k+1:n , k+1:n)=A(k+1:n else stop ( 矩阵奇异) end end 3.2.2 界面 界面的设计要根据问题的需求,这步 需要怎样的用户控制和输入,而界面的内容决 列主元消去程序,首先需要用户输 入区域供用户输入方程的系数,最后 止。所以 分。我们使用分组组件 GroupBox 来 GroupBox1 之中包含一个 Label,一个 它用来供用户输入方程的维数;Label 用来显示 在它的响应时间中,还要根据输入 输入方程系数。 GroupBox2 用来盛放方程系数的输 StaticText 组件,它们用于在方程求解完毕时 步一步执行消元操作,并在消元结束 一个方程的界面。 GroupBox1 和 GroupBox2 在 在输入维数时,GroupBox2 的 Visible 为 false,所以设计界面时,设置 GroupBox1 GroupBox1 和 GroupBox2 中各 GroupBox2 中动态生成的 第 3 章 程序设计基本流程 47 的上半部分。 程序中实现对窗体大小的设置。 另外,窗体的大小也要根据数组的维数而改变,在 图 3-1 窗体界面设计图 3.3 代码实现 运行的结构,再根据设计的算法即可编写具体的实现代码。 头文件如下。其中定义的函数和变量的说明参看注释。 ---------------------------------------------------------------------- //--------------------------------------------------------------------------- #include #include #include #include 程序界面的设计决定了程序 3.3.1 头文件 程序 //----- #ifndef Unit1H #define Unit1H 第 3 章 程序设计基本流程 48 #include //--------------------------------------------------------------------------- class TForm1 : public TForm { __published: // IDE-managed Components TGroupBox *GroupBox1; TLabel *Label1; TEdit *EditInputN; TButton *Button1; TGroupBox *GroupBox2 TButton *BtnInputOver; //维数输入确认按钮的鼠标点击事件的响应函数 ”等的组件数组 y< DynamicArray > EditX; icArray< DynamicArray > LabelX; DynamicArray< DynamicArray > ValueX; //记录置换矩阵信息 ay< int > ValueP; 1 范数,FanAN 为矩阵的无穷范数 // FanInoA1 和 FanInoAN 分别是 A 逆矩阵的 1 范数和无穷范数 nAN,FanInoAN; // User declarations 的 OnKeyPress 事件的响应函数 oid __fastcall EditOnKeyPress(TObject *Sender, char &Key); 属性判断需要执行第几步消元,或者是其它要求的操作, //利用 tag call TForm1(TComponent* Owner); -------- TForm1 *Form1; ; TStaticText *StaticText1; TStaticText *StaticText2; void __fastcall Button1Click(TObject *Sender); //方程系数输入确认按钮的鼠标点击事件的响应函数 void __fastcall BtnInputOverClick(TObject *Sender); private: // User declarations //定义动态组件数组,EditX 是系数输入的 Edit 组件的数组, //LabelX 是显示”x1+ DynamicArra Dynam //ValueX 用来存放输入的方程系数 DynamicArr //FanA1 为矩阵 A 的 double FanA1,FanInoA1,Fa public: //动态生成的 Edit 组件 v //根据 BtnInputOver 的 Tag // 从而完成指定的功能 void __fastcall StepByStep(TObject *Sender); __fast }; //------------------------------------------------------------------- extern PACKAGE 第 3 章 程序设计基本流程 49 //--------------------------------------------------------------------------- 开始,需要进行一些初始化处理,代码如下: ---------------------------------------------------------------------- ) utN->Left+EditInputN->Width+35; 70; ------------------------------------------------ OnClick 事件处理代码如下: ------------------------------------------------------------------- astcall TForm1::Button1Click(TObject *Sender) i,j,Len_N=2; //默认维数为 2 入 N 值!",mbOK,TMsgDlgButtons()<Text.ToInt()>11) (由于数组大小的限制, 11>=N>=2)!",mbOK,TMsgDlgButtons()<Align=alClient; GroupBox2->Align=alClient; //初始化窗体大小 Form1->Width=EditInp Form1->Height=Button1->Top+ } //--------------------------- 维数输入确认按钮 Button1 的 //-------- void __f { int if(EditInputN->Text=="") { MessageDlg("请输 return; } if(EditInputN->Text.ToInt()<2 || EditInp { MessageDlg("请输入正确的 N 值 } Len_N=EditInputN->Text.ToInt(); GroupBox1->Visible=false; //隐藏 GroupBox1,生成动态组件后,显示 G //设置动态数组的大小 ValueX.Length=Len_N; 第 3 章 程序设计基本流程 50 LabelX.Length=Len_N; for(i=0;iLeft=8+j*(50+20); tX[i][j]->Top=16+i*20; tOnKeyPress; //设置其 OnKeyPress 的响应函数 lX[i][j]=new TLabel(this); abelX[i][j]->Name="LabelX"+String(i)+String(j); abelX[i][j]->Caption="X"+String(j+1); //”xn”,后无加号 lX[i][j]->Top=16+i*20+3; { ValueX[i].Length=Len } for(i=0;iParent=GroupBox2; EditX[i][j]->Name="EditX"+String(i)+String(j); EditX[i][j]->Text="0.0"; EditX[i][j]->Width=50; EditX[i][j]->Height=20; //50*20 Ed Edi EditX[i][j]->OnKeyPress=Edi EditX[i][j]->Visible=true; Labe LabelX[i][j]->Parent=GroupBox2; L if(jCaption="X"+String(j+1)+"+"; //”x1+”等 else L LabelX[i][j]->Width=20; LabelX[i][j]->Height=14; //20*14 LabelX[i][j]->Left=8+j*(50+20)+50; Labe 第 3 章 程序设计基本流程 51 LabelX[i][j]->Visible=true; } LabelX[i][j]=new TLabel(this); 0);//+50; 3; [j]=new TEdit(this); ->Parent=GroupBox2; [j]->Name="EditEnd"+String(i); [j]->Height=20; //50*20 [i][j]->Left=8+j*(50+20)+20; // j 已经自加一 [j]->Top=16+i*20; ress=EditOnKeyPress; 钮和文本组件的位置 putOver->Top=8+Len_N*20+15; Box2->Visible=true; tOver->Top+BtnInputOver->Height; 1->Height; cText3->Top=StaticText2->Top+StaticText2->Height; 新设置窗体大小 m1->Height=StaticText2->Top+StaticText2->Height+40; nInputOver->Width)/2; -------------------------- 示在 OnKeyPress 事件中实现,代码 s(TObject *Sender, char &Key) LabelX[i][j]->Parent=GroupBox2; LabelX[i][j]->Name="LabelEnd"+String(i); LabelX[i][j]->Caption="="; LabelX[i][j]->Width=20; LabelX[i][j]->Height=14; //20*14 LabelX[i][j]->Left=8+j*(50+2 LabelX[i][j]->Top=16+i*20+ LabelX[i][j]->Visible=true; EditX[i] EditX[i][j] EditX[i] EditX[i][j]->Text="0.0"; EditX[i][j]->Width=50; EditX[i] EditX EditX[i] EditX[i][j]->OnKeyP EditX[i][j]->Visible=true; } //设置按 BtnIn BtnInputOver->Visible=true; Group StaticText1->Top=BtnInpu StaticText2->Top=StaticText1->Top+StaticText //Stati //重 Form1->Width=EditX[0][EditX.Length]->Left+70; For BtnInputOver->Left=(Form1->ClientWidth - Bt } //------------------------------------------------- Edit 组件中对数字的输入需要有一定的限制,这些显 如下: //--------------------------------------------------------------------------- void __fastcall TForm1::EditOnKeyPres { 第 3 章 程序设计基本流程 52 if(Key!=8 && Key!='.' && Key!='-') //Key=8 时为 BackSpace 键 ey < '0' || Key>'9') Key=0; 电脑发声,提示输入不合法 输入的效果如图 3-2: { if(K { //Key<'0' || Key>'9'表示非数字的按键; MessageBeep(MB_OK); //让 } } } //------------------ 维数 图 3-2 维数输入效果 图 3-3 方程系数输入界面效果 nputOver 的响应 应系数输入结束事件,还用来作为一步步执行消元操作的用 输入界面。 3.3.3 BtnI BtnInputOver 按钮不仅仅响 户接口,而且,方程求解完毕后,用于跳回维数 第 3 章 程序设计基本流程 53 我们用 BtnInputOver 的 Tag 属性来标记需要执行的操作类型。当系数输入结束时,执行 钮的 OnClick 事件 StepByStep 函数中根据 Tag 来判断当前执行到第几步 代码如下: orm1::BtnInputOverClick(TObject *Sender) itX.Length;i++) ++) itX[i][j]->Text=="") (j+1 == EditX[i].Length) 确的 B("+String(i+1)+")!", mbOK,TMsgDlgButtons()<Text=ValueX[i][j]; putOver 的显示文本和 Tag 标记 ver->Caption="执行第 1 步消元"; 应函数 ver->OnClick=StepByStep; anAN=0; BtnInputOverClick 函数,将输入的系数写入数组,然后将 BtnInputOver 按 的响应函数指定为 StepByStep 函数,在 消元,从而执行相应操作。 实现 //------------------ void __fastcall TF { int i,j; //获取输入的系数 for(i=0;iText.T EditX[ } } } //改变 BtnIn BtnInputO BtnInputOver->Tag=0; //设置 BtnInputOver 的 OnClick 响 BtnInputO //计算 A 的范数 FanA1=0; F for(i=0;iTag]) > temp=i; p 行与 BtnTemp->Tag 行互换 ag) for(j=0 { Dtmp=Dtmp+fabs(ValueX[i][j]); } if(Dtmp>FanAN)FanAN=Dtmp; } for(j=0;jFanA1)FanA1=Dtmp; } } //---------------------------------------------------------------------- StepByStep 算系数矩阵 A 的逆矩阵,求得逆矩阵 //----------------- void __fastc { TButton *BtnTemp=(TButto int i,j; //tag 标记现在对哪一列进行消元 if(BtnTemp->Tag >= 0) { //-1 表示消元 //寻找列中最大系数 int temp=BtnTemp->Tag; for(i=t { if(f fabs(ValueX[temp][BtnTemp->Tag])) } ValueP[BtnTemp->Tag]=temp; //tem if(temp > BtnTemp->T { 第 3 章 程序设计基本流程 55 double Dtmp; ngth;j++)//列 ValueX[BtnTemp->Tag][j]=ValueX[temp][j]; g][j]; BtnTemp->Tag][BtnTemp->Tag] !=0 ) =BtnTemp->Tag+1;iTag]= ValueX[i][BtnTemp->Tag]/ValueX[BtnTemp->Tag][BtnTemp->Tag]; lueX[i].Length;j++) { p->Tag]* ValueX[BtnTemp->Tag][j]; [j]; 里面保存的是 L(i,k) EditX[i][BtnTemp->Tag]->Text=0; 否已经消元结束 BtnTemp->Tag+1 >= ValueX.Length-1) 要多判断一步最后 A[N][N]是不是零, 中用的是 i=BtnTemp->Tag+1 (ValueX[BtnTemp->Tag+1][BtnTemp->Tag+1]==0) tnTemp->Tag=-2; 异!"; { 计算结果"; } for(j=BtnTemp->Tag;jTag][j]; EditX[BtnTemp->Tag][j]->Text=ValueX[BtnTemp->Ta ValueX[temp][j]=Dtmp; EditX[temp][j]->Text=ValueX[temp][j]; } } //对 BtnTemp->Tag 列进行消去 if(ValueX[ { for(i { V for(j=BtnTemp->Tag+1;jText=ValueX[i] } //ValueX[i][BtnTemp->Tag]=0; 不清零, } //消元结束,将 tag+1,先判断是 if( {//消元结束 ,这里 //因为为了节约时间,前面循环 if { //矩阵奇异,方程无解 B BtnTemp->Caption="系数矩阵奇 } else BtnTemp->Tag=-1; BtnTemp->Caption="显示 第 3 章 程序设计基本流程 56 } else BtnTemp->Caption="执行第"+String(BtnTemp->Tag+1)+"步消元"; emp->Tag=-2; 矩阵奇异!"; =-1) //消元已经结束,再按按钮表示要显示结果 显示之--------- InoA=0; //矩阵范数,以及逆矩阵的范数 > ValueU; > InoValueX; //存放 A 的逆矩阵 LP; //存放 L 或者 P,中间变量 th; ength; { ValueU[i].Length=ValueU.Length; //扩展等式右边 { InoValueX[i].Length=InoValueX.Length; //扩展等式右边 } for(i=0;iTag++; } } else //矩阵奇异,方程无解 { BtnT BtnTemp->Caption="系数 } } else if(BtnTemp->Tag= { //------求条件数,并 double FanA=0,Fan //矩阵 U,消元系数,下三角阵 DynamicArray< DynamicArray DynamicArray< DynamicArray DynamicArray< DynamicArray > ValueU.Length=ValueX.Leng InoValueX.Length=ValueX.L LP.Length=ValueX.Length; //N*N矩阵 for(i=0;i=0;i--) { for(j=i;j=0;i--) { //从 ValueX 中读出 L int ii,jj; for(ii=0;iiFanInoAN)FanInoAN=Dtmp; } //求 FanA-1,A 的 1 范数 //求 FanInoA-1,即 A 逆的 1 范数 for(j=0;jFanInoA1)FanInoA1=Dtmp; } //计算并输出 FanA*FanInoA,即 件数 StaticText1->Caption="∞-cond="+String(FanAN*FanInoAN); n="1-cond="+String(FanA1*FanInoA1); ----------------- --------- //在最后一列文本框中显示计算结果,先把其他组件隐藏 for(i=0;i { EditX[i][j]->Visible=false; } } +) 1-条件数和无穷条 StaticText2->Captio //---- --- Visible=false; } LabelX[i][j]->Caption } //计算各个 x 得值并把它写到 ValueX 最后一列中 for(i=ValueX.Length-1;i { double Dtmp=0; for(j=i+1;jText=ValueX[i][ValueX.Length]; 已经显示出结果 { //现在释放所有动态资源,重新开始新的 N 输入,新的计算 for(i=0;i=0;i--) gth]=ValueX[i][ValueX.Length]/ValueX EditX[i][ValueX.Length]->Text=ValueX[i][ValueX.Length]; } //计算结束,将结果显示在文本框中 for(i=0;iCaption="求解其他方程"; BtnTemp->Tag=-2; //结果已经输出 } else //tag==-2,方程无解 或者 +) 第 3 章 程序设计基本流程 61 BtnTemp->Caption="输入结束"; StaticText1->Caption="方程尚未解出"; StaticText2->Caption="方程尚未解出"; BtnTemp->OnClick=BtnInputOverClick; GroupBox2->Visible=false; GroupBox1->Visible=true; Form1->Width=EditInputN->Left+EditInputN->Width+35; Form1->Height=Button1->Top+70; } } //---------------------------------- 程序计算过程中的效果如图 3-4: 图 3-4 (1)系数输入 图 3-4 (2)输入结束 图 3-4 (3)第一步消元结果 图 3-4 (4)第二步消元结果 第 3 章 程序设计基本流程 62 图 3-4 (5)计算结果 3.4 程序的调试 调试是为了找出程序中的错误并更正之,而程序中的错误一般有三种。第一种是语法错 误,这是不可避免的,往往由于写 时会有提示,这种错误容易发现 试和程序员的经验,也可以设置专门的异常处理过程;第 三种错误是逻辑错误,可能由算法本身的错误引起,或者由于错误使用代码(如加法写成了 减法),这种情况下,程序可以正常的运行,但是程序的结果不是我们所需要的。 程序的调试工作主要是针对第三种错误,从这个意义上来说,调试是一项具有挑战性的 工作,它需要程序员具有很高的编程水平、清晰的逻辑思维、敏锐的洞察力和发现问题的经 验与技巧。 3.4.1 调试选项设置 如图 3-5,在“Project”->“Options”菜单命令弹出的 Project Options 对话窗的选择 Compiler 选项卡中,在 Debugging 组中的三个选项,分别决定产生的可执行程序是否包含调试信息、 代码行号信息以及是否使用函数的内联扩展,在缺省的 Full Debug 模式下,三个选项都是被 选中的。 在 Compiling 组中,选项 Stack Frames 决定是否在程序中产生标准的堆栈框架。默认调 试模式下此项是被选中的。 3.4.2 程序执行方式 Run 菜单中的 Run,Program Pause 和 Program Reset 分别用于执行程序、暂停程序的运 错字符等原因造成,编译 容易解决;第二种错误是运行时错误,如运行时将一个 Edit 的内容置空,而程序中使用到 Edit 的 ToInt 方法,就会出现错误,对这种错误的处理也可以说是对程序运行中的各种异常 的处理,其解决主要依靠程序的测 第 3 章 程序设计基本流程 63 行和终止程序运行。在调试 Step Over,用于单步执 程序时,我们常要使用到其它一些菜单项: 行,每运行一行则暂停一下程序的运行,进入监控调试状态。若 遇到函数调用,则一步直接跳过。 Trace Into,紧随单步执行,遇到函数调用时,仍采用单步执行的方式,跟踪进入函数内 图 3-5 Project Options 对话窗 部。 Trace to Next Source Line,源代码紧随单步执行,这种执行方式是在 Trace Into 方式的基 础上,指示出下一步所要执行的程序代码。 Run to Cursor,执行到光标位置,这种方式下,程序运行完光标所在行的前一行代码后 就会自动暂停。 3.4.3 断点的使用 要让程序在某个地方暂停,除了上面说的使用 Run to Cursor 执行方式之外,还可以在程 序中添加断点来实现。添加断点的方式是使用菜单“Run”->“Add Breakpoint”->“Add Source Breakpoint”弹出的对话窗(如图 3-6)。对话窗中 Line Number 指定断点所在行;Condition 为断点出程序暂停的条件,只有符合了表达式条件才会暂停;Pass Count 指定程序运行经过 第 3 章 程序设计基本流程 64 断点多少次之后才使程序在该断点处暂停。 另外增添断点的简单的 就是在代码行左侧的空白处点击鼠标,如图 3-7 中的鼠标所 在位置。 增添了断点的代码行左侧右移红色圆点,整行代码红色高亮显示,如图 3-7。使用 View->Debug Windows->Breakpoints(或 Ctrl+Alt+B)将弹出断点列表框(Breakpoint List), 如图 3-8。在断点列表框中可以完成增添断点、删除断点、改变断点的可用属性等操作。 方法 图 3-6 增添断点对话窗 图 3-7 鼠标添加断点与断点的显示效果 第 3 章 程序设计基本流程 65 图 3-8 断点列表框 3.4.4 变量监视 程序在断点处停下来之后,可以使用菜单命令“Run”->“Inspect…”打开如图 3-9(1) 的对话窗,填入需要查看的变量名称,确认之后弹出如图 3-9(2)的变量查看窗,如果要改 变变量的值,点击右侧的 按钮,在弹出的修改窗中修改即可(如图 3-9(3))。 图 3-9(1) Inspect 对话窗-变量选择 图 3-9( 量查看 2) Inspect 对话窗-变 第 3 章 程序设计基本流程 66 图 3-9(3) Inspect 对话窗-变量修改 l Variables”(Ctrl+Alt+L)打开局部 变量的值进行查看,如图 3-10。 表达 式的状态和数值。可以用菜 Add Watch…”(Ctrl+F5)调出设置观察点属性的对话窗(如图 3-11)来添 加观 在对话窗中输入表达式,还可以设置各种观测格式,包括重复次数(一般用于对数组观 、有效数位、启用和禁用以及显示的数据类型等。 列表框(Watch List)会自动弹出,也可以通过菜单命令“View” ->“Debug Windows”->“Watches” (Ctrl+Alt+W)打开观察点列表框(如图 3-12),双击其中 察点属性框。 用菜单命令“View”->“Debug Windows”->“Loca 变量列表框,也可以对局部 变量状态监视常用的方法是设置观察点(Watchs),程序运行中,所设置的所有观察变量 式会始终出现在 IDE 的一个窗口中,窗口里同时还给出表达 单命令“Run”->“ 察点。 测时指定所要监测的元素个数) 在添加观察点时,观察点 的观察点会弹出观 图 3-10 局部变量列表框 第 3 章 程序设计基本流程 67 图 3-11 观察点属性框 图 3-12 观察点列表框 变量的方法 对话框(如图 3-13),用菜单命令“Run” …”(Ctrl+F7) 状态时,使用该对话窗, 可以得 量在程序暂停位置的代码处是 值。 对话窗右上的帮助按钮,参看帮助文件。 另一种监测 是使用“Evalute/Modify” ->“Evaluate/Modify 打开该对话框。当程序运行进入暂停 输入一个表达式,就 到其运算结果,需要注意的是,变 可以访问的。另外,如果在 以指定显示 表达式框中输入的是变量,还可以对它赋予新 输入表达式时可 格式。点击 第 3 章 程序设计基本流程 68 图 3-13 Evaluate/Modify 对话框 3.4.5 其它调试命令 图 3-14 Call Stack 窗口 图 3-15 Event Log 窗口 图 3-16 Thread Status 窗口 View->Debug Windows 其它调试命令包括有堆栈窗口(Call Stack,如图 3-14)、线程列 表( 3-15),以及汇编Threads,如图 3-16)、模块(Modules)、事件记录(Event Log,如图 第 3 章 程序设计基本流程 69 语言级的监视窗口(如图 3-17) 图 3-17 CPU 监视窗口 3.5 下,我们希望自己编写的应用程序拷贝到别的机器上就可以直接运行,也就是 编译 可执行文件。这需要进行三个操作: z Project Options 窗口(Shift+Ctrl+ Compiler Release 按钮。 z Runtime packages Build with runtime packages。 z Linker Linking 里的 Use dynamic RTL。 需要注意,如果用到数据库等比较特殊的组件,仅仅上面的操作还不够。例如如果是数 BDE。 制作安装程序需要使用 InstallShield,它的使用我们不再介绍,用到它的读者可以自己寻 其 程序的发行 一般情况 成独立 打开 F11),在 页中点击 在 Packages 页中勾掉 里的 在 页中勾掉 据库程序用了 BDE,则必须同时带 找 它参考资料。 第 3 章 程序设计基本流程 70 3.6 思考题 z 设计应用程序一般有哪些步骤? z 列主元消去法求解方程组的程序所解决的实际问题和它对应的算法是什 么? z 从求解方程组的程序分析如何设计程序和用户之间的交互 z C++Builder 中的调试工具主要有哪些?如何使用? z 如何设置才能编译独立的可执行程序? 《C++ Builder 6 编程实例精解 赵明现》 第 04 章 文本处理程序 本章重点 多文 学习 菜单与弹出菜单、文本组件、工具栏与状态条的使用 本章讲述制作一个文本处理程序的过程。在文本处理程序中,设计到了菜单、文本组件、 档技术、工具条、状态栏等组件的用法以及相关的技巧。 目的 通过本章的学习,您可以: ■ 掌握主 ■ 掌握对多窗体技 ■ 掌握对文本的处理方法 术的使用 ■ 掌握菜单融合和文件拖放等技巧 ■ 熟悉编写应用软件的一般方法和技巧 第 4 章 文本处理程序 72 本章典型效果图 第 4 章 文本处理程序 73 4.1 4.1 ows 程序与用户之间交互的一种很普遍很实用的方式,它操作方便而且样式 很多 菜单 菜单的使用 .1 菜单 菜单是 Wind 。一般而言,有固定菜单、下拉菜单以及弹出式菜单。比如程序的主菜单是二级下拉式 ,鼠标右键菜单是单级弹出式菜单。 图 4-1 菜单结构示意图 如图 4-1 所示,为典型的菜单 中的设置方法简略叙述如下: z 现,比如 New 菜单的 Caption 属性为” &New”。 z 快捷键 组合键,通过设置菜单的 ShortCut 属性更改。 快捷键只要程序当前在系统焦点上,就 可以用。 z 单 ck 属性为 true 时,菜单前面会有一个小勾号。一般用来标记程序中某 结构。它各个属性在 BCB 加速键 即菜单中的标题上带下划线的字母,如果是中文菜单,加速键出现在后边括号中。通过 在其 Caption 属性中相应字母前加’&’实 如 Ctrl+N 的 加速键只有在子菜单被弹出的时候才可以用,而 分栏 菜单项中显示为灰色横线的项目。一般用来对菜单项目进行分组,将功能相近的分在跟 其他的菜单项目分隔开。设计时将 Caption 属性设为’-’即可。 z 多级菜 如果要对一个菜单项目设置下一级菜单,在设计菜单时,在该项目上点击鼠标右键,选 择”Create Submenu”即可进入下一级子菜单的设计。 z Check 属性 当菜单项目的 Che 第 4 章 文本处理程序 74 种参量或属性的状态,或者作为用户传递给程序的参数。 z Enabled 属性 菜单项目不可用时,菜单会显示为灰色,并且对鼠标点击事件没有反应。 4.1.2 菜单的设计 【1】菜单编辑器 图 4-2 菜单设计器 计窗口上的菜单控件的图标双击设 即可进入如图 4-2 所示的菜单编辑器。图 4-2 是 图 4 对于 面已经提及,这里对个别没有提及的属性说明如下: 表 4-1 菜单项属性说明 属性 说明 -1 的菜单在设计阶段的情形。菜单项目的属性在选中它后,可以在属性检查器中更改。 菜单项目的几个重要的属性,前 Action 菜单的对应事件。设定此项后,不用再为菜单项的鼠标点击事件编写代码。 般用于常见的事件,如打开文件,保存文件,剪切复制等功能。 一 Bitmap 在属性检查器中点击此项目的 按钮会弹出 picture editer 窗口,用来制定此 菜单项的图标。 Break 指示菜单是否在该选项处重叠 Default 指示该选项是否为默认菜单选项,同一级中只允许有一个默认选项。此选项 设为 true 后,菜单 户双击此项目的上级菜单项时,默 认选项将被选中。 标题用黑体显示,当用 第 4 章 文本处理程序 75 G ,与 RadioItem 属性配合roupIndex 具有相同 GroupIndex 属性的菜单项可以构成一个组 使用,可以实现一组菜单项的多选一(单选)功能。 Name 菜单项的名字 Visible 设定菜单项是否可见,如果为 false,则程序运行时此项不可见。 【 菜单设计器的右键 2】 图 4-3 菜单设计器的右键菜单 由图 4-2 和图 4-3,可以看到菜单设计器的右键菜单。各项对应功能如下表: 表 4-2 菜单设计器右键功能说明 Insert 插入 菜单 Delete 删除菜单 Create Sunmenu 为菜单项创建一个子菜单 Select Menu… 弹出窗口,选择需要编辑的菜单组件 Save As Template… 把当前菜单保存为菜单模板 Insert From Template… 从模板中选择菜单项插入到当前位置 Delete Templates… 删除菜单模板 Insert From Resource… 从资源文件中添加菜单 【3】菜单模板 B B 中提供了七种已经设计C 好的常用的菜单作为模板供我们使用,可以从右键的 Insert From Template 选项打开模板列表,选择相应模板即可添加。 常需要用户输入一些文字,或者要显示给用户一段文字并且需要用户编辑, 这个 模板的删除等其他操作参看表 4-2。 这些菜单都是英文菜单,可以把它们都编辑成中文菜单然后保存。为编写中文界面提供 方便。 4.2 文本组件的使用 在程序中,经 时候,就需要用到文本组件。常用的文本组件有 Edit、Memo、MaskEdit 和 RichEdit,他 第 4 章 文本处理程序 76 们都可用于字符串的输入输出操作。这四个组件都继承自 TCustomEdit 对象,功能从简单到 复杂。Edit 和 MaskEdit 功能相似,只是 MaskEdit 提供了用户定制输入格式的功能。Memo 一般用于大段文字的显示和编辑,而 RichEdit 则提供更强大的编辑和显示功能,可以支持 RTF 只能容纳不超过 255 个字符的数据。但是一般用于显示单行文本的时候可以选择 Label 组件 或者 StaticText 组件,TEdit 组件多用来完成用户的单行输入,特别的,可以通过对其属性的 限制或者编写对应的事件函数来完成具有特定限制条件的输入。Edit 常用属性如下表: z Text 属性 最重要的属性,也是它最基本的属性。存放组件的数据内容(文本信息),格式是 AnsiString 类型。 z ReadOnly 属性 布尔量,指示 TEdit 组件中的文字内容是否可以被用户更改。如果设为 true,则不能更 改,此时其功能与 Label 组件相同。 z CharCase 属性 此属性可以强制组件中的文本全部为大写字母(对应值为 ecUpperCase)、小写字母 (ecLowCase)或者不强制改变大小写(ecNormalCase)。 CharCase 最初是 TCustomEdit 类的属性,TEdit 将其作为公共属性继承下来,但是 Memo 和 RichEdit 对应的类没有继承此属性,所以它们没有 CharCase 属性。 z PasswordChar 属性 密码替代字符。如果 TEdit 组件用于密码输入时,可以通过设置此属性来实现。默认此 属性为 0,即正常显示字符,如果用于密码输入,一般将其值改为’*’,这样不管在其中 输入什么字符,都只显示为星号,当然也可以设置为其他字符。 z MaxLength 属性 限制(一行)最多可以输入 不限制长度,此时一行的最大 中的文本。 z OnKeyPress 事件 (Rich Text Format)格式的文本。 4.2.1 TEdit 组件 TEdit 组件是最常用的组件之一,它属于 TEdit 类。可以用来显示、编辑单独的一行文本, 的字符数目。默认为 0,表示 长度由系统决定,一般为 255 个字符。 z HideSelection 属性 表示当组件失去焦点的时候,是否反色显示选中文本。如果设为 false,则当失去焦点之 后,依然可以看到组件中原来被选中的文本是白色兰底(默认配色的情况下)。 z AutoSelect 属性 该属性表示当组件获得焦点的时候,是否全选组件 z OnChange 事件 最常用的 Edit 事件,当 TEdit 组件中的文本改变的时候触发。可以用于输入信息的及时 处理。比如在输入过程中,如果 TEdit 组件中的文本是数据库中某人的名字,则适时显 示此人的其他信息,这就要用到 OnChange 事件。 第 4 章 文本处理程序 77 当按下任意按键时触发,触发此事件的时候,TEdit 组件中还有没有回显出此字符。一 格式输入的情况,即在此事件的处理函数中加入对按键的判断,如果是 则将 Key 设为 0,即空字符,这样文本编辑框就对它不做处理。 void __fastcall TForm_adduser::F2Edit1KeyPress(TObject *Sender, char &Key) { if(Key!=8) //Key=8 时为 { =='0' && F2Edit1->Text=="")) { //Key<’0’ || Key>’9’表示非数字的按键; MessageBeep(MB_OK); //让电脑发声,提示输入不合法 长度大于 6 个字符,则屏蔽输入。 的示例代码 的例程的功能,只是 EditMask 可以让我们方 的 skEdit 的 EditMask 属性的 般用于需要特定 不合法的按键, 例如下面的程序: BackSpace 键 if((Key < '0' || Key>'9')||(Key //Key=='0' && F2Edit1->Text=="" 表示在文本框内容为空的时候按了’0’ Key=0; } else if(F2Edit1->Text.Length()>=6) {//如果文本 //当然这个功能可以通过设置 MaxLength 来实现 Key=0; s MesageBeep(MB_OK); } } } 上面 的功能是控制用户在 TEdit 组件中输入 6 个以内的数字,并且数字的第 一个字符不能为 0。 4.2.2 TMaskEdit 组件 如果需要更好的定制用户输入数据的格式,可以用 TMaskEdit 组件。它的功能与 TEdit 组件基本完全一样,只是它提供定制输入格式的功能,它通过过滤功能来限制输入到 MaskEdit 中的字符数据,若输入的字符不合法,则拒绝接受,这对实现非常复杂的格式输入 是很有用的。这样的功能类似与 4.2.1 节中最后 便 定制输入格式而省去繁琐的编写程序代码的过程。 点击 Ma 按钮,可以打开 Mask Editor 掩码编辑窗口(图 4-4)。 个 占位字符,所谓占位字符,也就是在需要用户输入字符,但是还没有输 字 导入掩码文件,此类文件是”dem”后缀。注意,因为一个汉字占 两个字节,所以如果需要输入一个汉字,应该用两个掩码来表示,如 LL。 一 完整的掩码由三部分构成,三部分之间用分号格开。第一部分是掩码本身;第二部分是 指定掩码字面上的特性是否作为数据的一部分而被保存,通过”Save Literal Characters”选框更 改;第三部分是指定 入 符的时候显示出来的字符,通过更改”Character for Blanks”来更改。 点击 Masks…按钮可以 第 4 章 文本处理程序 78 图 4-4 Mask Editor 掩码编辑窗口 常用的掩码的控制字符及其说明如下表: 表 4-3 MaskEdit 的常用掩码 掩码 说明 ! 如果掩码中有’!’,则允许输入字符串之前出现引导空格而不将空格存入数据;否则, 表示允许输入字符串之后出现后随空格而不存入数据 > 表示其后的输入字符全部转换为大写字符,直到字符串输入结束或者遇到<为止 < 表示其后的输入字符全部转换为小写字符,直到字符串输入结束或者遇到>为止 <> 如果这两个字符连用,表示不检查后边字符的大小写,以用户输入为准 \ 在”\”后的字符将被视为一般的字符而非掩码字符。这一般用在要把掩码字符作为一 般字符输出的时候 L L 出现的位置只允许输入字母字符 1 只允许输入字母,但可以空缺 0 只允许输入数字 A 只允许输入字母或数字 C 只允许输入字符 ; 用来隔开掩码的三个部分 4.2.3 TMemo 组件 TMemo 组件提供多行的编辑管理功能,可用于多行文本的输入和显示,但文本的大小 是受系统的不同而有限制的,在 Windows9x 中,文本大小必须小于 64k。TMemo 组件的大 部分公共属性都继承自它的父类 TCustomMemo,并没有引入新的属性或事件。 Lines 是 TMemo 组件最基本的属性,它用于存放 TMemo 组件的文本,通过 Lines 属性, 第 4 章 文本处理程序 79 可以实现对文本的操作控 文本便是以字符串表的形 制。Lines 是一个 TStrings 对象,存放一个字符串列表,Memo 中的 式存放 Lines 中,Lines 的一个数组元素存放 Memo 中的一行文本。 如果要读取或者控制 TMemo 组件中的全部文本,可以用 Text 属性;而要对 TMemo 组 Lines 属性会非常方便。常用的行操作如下: um Mem 本 Mem ->Lines->Insert(3,"插入的一行文本"); //在第四行后插入一行文本 入的一些控制功能,常用的几个重要属性如下: z ordWrap 的时候是否自动换行。这个属性在 TMemo 件 z an 焦点的时候,Return 键和 Tab 键应该可以用来编辑文 组件对这两个键不反应,就可以通过改变组件的 WantReturns W 比如,把 WantTabs 属性设为 false,则程序运行时在编辑框中 Memo 中文本并没有反应,而是程序的焦点转移到其他组件上去了。 z SelLen 来表示文本中被选择部分的属性的。SelLength 文本的起始位置; 表示被选择部分的文本字符串。 z rollBar 的控制功能,如全 选、 销上一次操作等功能。常用到的功能如下: 表 4-4 TMemo 组件函数说明 件的文本进行行操作,用 N OfLine=Memo1->Lines->Count; //计算 Memo 中文本的行数 Memo1->Lines->Add("新添的一行文本"); //在文本尾部增加一行文本 o1->Lines->Delete(3); //删除第四行文 o1 Memo 中还提供有用户输 W 该属性决定输入的文字到达编辑框的右边界 组 含有水平滚动条的时候失效。 W tReturns 与 WantTabs 一般情况下,当 Memo 编辑框获得 本。但是如果需要 TMemo 和 antTabs 属性来实现。 按下 Tab 键, gth、SelStart 和 SelText 这三个属性都是以 Sel(Select)开头,是用 属性表示 TMemo 组件中被选择文本的长度;SelStart 属性表示被选择 SelText 属性 Sc 这个属性用来控制 TMemo 组件是否使用滚动条。它的取值及对应意义如下: ssNone 无滚动条 ssHorizontal 底部水平滚动条 ssVertical 右部垂直滚动条 ssBoth 同时使用水平和垂直滚动条 由于 TMemo 组件继承自 TCustomEdit 类,也继承了它很多比较完备 撤 函数 功能 Clear() 清除 Memo 中的文本 ClearSelection() 清除被选择的文本 ClearUndo() 取消 Undo 功能,调用之后 Undo()函数失效 CopyToClipboard() 将被选择的文本复制到剪贴板中 CutToClipboard() 将被选择的文本复制到剪贴板中,并删除被选择文本 GetSelTextBuf(Buffer,Size) 将被选择文本复制到 Buffer 指定的内存中,Size 指定复制的字符 第 4 章 文本处理程序 80 数。如果被选择区为空,则 Buffer 将接收到空字符串,如果被选 择文本比 Size-1 大,则只复制其前 Size-1 个字符。(字符串中第 Size 个字符用来存放字符串结束标志) PasteFromClipboard() 用剪贴板中文本覆盖被选择的文本,如果没有被选择文本,则将 剪贴板中文本插入到光标所在处 SelectAll() 全选编辑框中的文本 Se 文本;如果没有被选择文本, 所在处 tSelTextBuf(Buffer) 用 Buffer 中的文本覆盖当前被选择 则将 Buffer 的文本插入到当前光标 Undo() 撤销最后一次 ClearUndo()之后的所有操作。可以通过访问 CanUndo 属性来判断是否有操作可以用 Undo 撤销 4.2.4 TRichEdit 组件 ,它对文本的大小还有限制, 而且不支持复杂的文本格式。如果要处理大量的文本数据,RichEdit 是最好的选择;并且 Rich 置此种格式的属性和方法。 TRichEdit 组件支持 TMemo 组件的很多属性和方法,如 Lines、SelText 等属性以及 Clear、 Sele 里不再赘述。RichEdit 的优点在于它支 持 R 式,而与这种文本格式相关的比较重要的属性有: z 即按照 RTF 格式读写。 z 了文本的各种特性数据,如字体、字号、 。不同的是,DefAttributes 属性用于指定新增文本的格式属性,而 SelAttributes 定被选择文本的格式。 性都属于 TTextAttribute 类,它含有文字 size、style、name 等很多属性。其中 ,它是一个只读的集合类型,通过对 的判断,可以得到被选择文本的共同属性。 z raph 于设置或者返回当前所在段落的编排格式。 齐方式)、FirstIndent(段落第 段落属性。 的编辑功能、支持段落格式 的设 种语言,所以我们完全可以把 TRichEdit 组件看作一 个类 dows 写字板的程序。本章我们要制作的文本处理程序,正是基于 TRichEdit 组件。 虽然 TMemo 组件已经可以用于处理多行文本,但是始终 Edit 支持 RTF(Rich Text Format)格式的文本,并且提供设 ctAll、Undo 等方法,它们的用法也一样,所以这 TF 文本格 PlainText 指示是否以纯文本方式进行读写,缺省为 false, DefAttributes 和 SelAttributes 这是表示 RTF 格式的最重要的属性,它们记录 颜色等等 属性用于指 这两个属 ConsistentAttributes 属性对我们来说或许是最有用的 它的元素 Parag 用 它是一个只读的 TParaAttributes 对象,包含有 Alignment(对 一行文本缩进点数)、Numbering(是否出现圆点项目符号)等重要的 由于 TRichEdit 组件支持的丰富的文本格式、支持各种通用 置、可以使用项目符号甚至支持多 似 Win 第 4 章 文本处理程序 81 4.3 信息的交互;或者我们需要把不同类 的组 。在 C++Builder 中,我们有三种技术实现,它们分别对 应不 z 程序只需要一个窗体,但窗体上有不同的活页,每个活页上可以有不同的组件和 容,实际上相当于扩大了窗体的现实面积,然后折叠起来显示。 z 位相等,没有隶属关系。 z 窗体与子窗体 多文档技术 在应用程序中,我们经常需要打开多个窗口来实现 件分别放在不同的页面上来显示 同的需求。 多页面程序 多页面 内 如果需要用户输入的数据或输出给用户看得的结果比较繁杂,这种分页就很有用了,而 且在比较小的应用程序中也是很常用的,在以后章节中会详细的介绍它的用法,这里不 再详述。 多窗体技术 多窗体的程序中包含多个相互独立的窗体,各个窗体之间地 多文档技术 如图 4-5 所示,多文档界面(MDI)应用程序是一种特殊的多窗体程序,它包含唯一一个 父窗体和多个子窗体,父窗体就像一个容器一样包含着所有的子窗体。父 之间是隶属的关系,而所有的子窗体之间都是平等的。在 MDI 程序中,子窗体可以同时 都显示出来,但是同一时刻只能有一个子窗体获得焦点而处于激活状态。 多文档界面 窗体程序的窗体不能象 MDI 程序的 而不是被限制在主窗体的范围之内。 图 4-5 MDI 应用程序的界面 程序区别于多窗体程序之处在于其主窗体与子窗体之间的隶属关系,而且多 子窗体那样进行平铺和重叠等显示,它可以任意移动 第 4 章 文本处理程序 82 4.3.1 M 置即 e 属性 样一个两个窗体的 MDI 程序 面就做好了。 窗体,要实现动态创建子窗 体也 创建主窗体和子窗体,然后选择 C++ Builder 的 Project 菜单中 的 O 令(或者按 Shift+Ctrl+F11),弹出如图 4-6 的对话框,打开 Forms 选项卡,选中 图 4-6 Project Options 对话框 中,这样的目的是 窗体。 序中就可以用 new 命令来动态的创建子窗体了,例如: TChildForm =new TChildForm(this); 意 DI 程序设计技术要点 在可视化开发工具出现以前,要编写一个 MPI 应用程序要完成大量繁复的工作,如注册 父窗体和子窗体类、创建主窗体和子窗体、编写消息循环等,而 MDI 程序的消息处理跟一般 的 SDI 程序又有很大不同,所以程序员要完成大量的工作才能真正开始编写 MDI 程序的应 用部分。 而在 C++Builder 中,我们不用从最底层开始创建 MDI 应用程序,只需要进行简单的设 可。首先,通过菜单命令添加一个新的窗体作为子窗体,然后设置主窗体的 FormStyl 为 fsMDIForm,设置子窗体的 FormStyle 属性为 fsMDIChild,这 界 实际应用中,更常需要的是在程序运行是动态的生成新的子 很简单。首先象上面一样 ptions 命 自动创建窗体列表中的子窗体名字,按右移按钮移动到右侧可用窗体列表 让程序运行时不自动创建子 程这样,在 *newform 注 主窗体的源文件中应该加入#include "Unit2.h",也就是把子窗体的头文件包含进去,这样 才能用上面命令创建子窗体。 第 4 章 文本处理程序 83 4.3.2 后,可以通过不同的函数方法来实现对子窗体的关闭,布局等操作。 z DI 程序中,子窗体一旦被创建,就会显示在主窗体之中。默认情况下你会发现,子窗 体被 窗体的关闭按钮,结果窗体只是最小化,而没有真正关掉,这设计到 窗体 Action 属性。Action 属性用来指定窗体将要被关闭时执行什么操作,可以选择的值有: Ac n 的值 代表意义 子窗体的管理 子窗体被创建之 子窗体的关闭 M 创建之后,点击子 的 表 4-5 窗体 Action 取值 tio caNone 窗体不允许关闭,所以按关闭按钮没有任何反应 caHide 窗体并没有关闭,只是隐藏不显示。【子窗体是不允许隐藏的!】 caFree 窗体被关闭,并且窗体占用的内存将被释放 caMinimize 窗体不关闭,而只是最小化。这是子窗体的默认值 所以,要实现一个子窗体的关闭,需要在子窗体的 OnClose 事件中添加代码如下: nder , TCloseAction &Action) { } 这样 Close 函数,都能实现对 子窗 的关闭。 z void __fastcall TChildForm::FormClose(TObject *Se Action = caFree; ,不管是点击子窗体的关闭按钮或者是在程序中调用子窗体的 体 子窗体的布局 如图 4-7,子窗体的布局一般有层叠、水平平铺、垂直平铺三种 可以实现窗体的层叠显示;使用 Tile 方法实现平铺显示。使用 Tile 方法之前设 铺属性,可取 tbHorizontal 或 rtical 值得注意的一点是,当子窗体获得焦点时,而且子窗体有自己的主菜单,那么 动将主窗体的菜单和子窗体的菜单合并,具体请参看 4.4 节的菜单融合部分 图 4-7 子窗体布局示例 方式。用主窗体的 Cascade 方法 置 TileMode 即平 tbVe 。 C++Builder 会自 。 第 4 章 文本处理程序 84 4.4 章将要做的文本处理程序是一个 MDI 应用程序。创建 MDI 应用程序的方法前面已经 做了 的组件以及工具条、状态 栏、 单融合等比较特殊的部分做详细的讲解。 4.4 1. 需要使 用的 用于打开文件;文件保存对话框,用于保存文件; 打印 话框, 设置 建、打开、保存等操作的 加速 Combobox 组合框,用来 设置 设置文本字体的 大小 Associate)设为 Edit 的名 字, Position 设为 1。因为字 体的 x 为 1638,最小值 Min 为 1。 界面的创建 本 比较详细的讲述,这里不再详述创建过程,只对界面上的用到 菜 .1 主窗体与子窗体 主窗体界面 所示,为主窗体在设计时的图象。它包含一个 ImageList 用来存放程序中如图 4-8 图标;一个主菜单;文件打开对话窗, 对话框,用来打印文档;打印设置对话框,用作打印之前的设置。对打开文件对 过滤后缀为”*.rtf”,对保存对话窗,设置过滤后缀为”*.rtf”。 其 界面中在主菜单下方有一个工具栏(ToolBar)组件,上面放置新 钮。关于工具栏的使用,稍后会做介绍。工具栏中还有一个按 文本字体;一个 TEdit 组件和一个 UpDown 组件,它们组合在一起用于 与 Edit 组合的用法是将 UpDown 组件的关联组件(。UpDown 这样 TEdit 组件中便自动显示 UpDown 组件的 Position 值,默认 设置 UpDown 的最大值 Ma大小应该在 1 到 1638 之间,所以 第 4 章 文本处理程序 85 图 4-8 文本处理程序主窗体界面 、居右对齐以及段落设置 按钮 e 属性设置为 tbsCheck,这 样就 否被按下,以此来设置文本的字体属性。 我们只能让程序在同一时刻只有确定的一种对齐 按下的状态。为了实现这个功 能, 2. 设置文本字体; 文本 能;文本替换对话框,用于替换文本的操作。 这也是实现文本编辑最关键的组件。将它的 Aligh 属性 设置 ichEdit 组件始终占满整个窗口区域。 工具栏中还有控制字体加粗、斜体、下划线、居左对齐、居中 这几个属性都是用来标示字体的属性的,所以将它们的 Styl, 可以通过它们的 Down 属性来判断按钮是 ,对于三个设置对齐格式的按钮,另外 方式,所以在同一时刻,只能允许三个对齐按钮的一个处于被 需要把它们的 Grouped 属性设为 true。 子窗体界面 4-9 所示,为子窗体的界面。窗体中包含主菜单;字体对话框,用来如图 查找对话框,用于文本中的查找功 中还有一个 TRichEdit 组件,窗体 为 alClient,这样不论窗口大小怎么变化 TR 注意 检查主窗体和子窗体的 FormStyle 属性是否设置正确,子窗体应设为 Available Forms。 第 4 章 文本处理程序 86 图 4-9 文本处理程序子窗体界面 4.4.2 工具条与状态栏 放置着很多加速按钮的工具栏和窗体底部的状 态栏 1. 放置加速按钮(SpeedButton),组合框(ComboBox) 和编 框(Edit)等,通过这些组件实现一些操作中常用的功能。一般来说,这些功能在程序的 菜单 lBar 和 CoolBar 在 Win32 组件 中,ControlBar 在 Additional 组件页中。 窗体顶端,并且宽度随窗体 自动 ton、ComboBox、Edit 等控件,也可以用右键菜单中的 New 或空白分割条。也可以添加一个 Speed But 于 CoolBar 组件,它可以用来盛放多个工具栏,并且可以实现将一个工具栏拖拉到另 外一 个 CoolBar 组件,它默认会有比较大的高度, 然后 者 ToolBar,这样我们就 可以 Panel 或 ToolBar 中添加加速按钮等组件了,而且,我们可以在 CoolBar 中添加多个 Pan 的功能和设置方法根 CoolBar 类似。一样可以用来盛放多个工 具栏 的是,ControlBar 的宽度可以不会象 CoolBar 那样随 着窗口的宽度而自动变化,而且 ControlBar 上的工具栏的宽度也是可以固定的,这样我们就 在图 4-8 的主窗体界面中,我们可以看到 。在应用程序中它们很常见,这里做一下简要介绍。 工具栏 工具栏可以看作是一个容器,经常用它来 辑 中都能找到。 实现工具栏可以用 ToolBar、CoolBar 或 ControlBar 三个组件。Too 页 对于 ToolBar 组件,在窗体中添加它之后,默认的它会对齐 变化。可以在 ToolBar 上添加 But Button 或者 New Separator 命令来添加新的加速键 ton 之后将其 Style 属性改为 tbsSeparator 来设置分割条。 对 个工具栏的后边。我们首先在窗体中添加一 在其中添加一个 Panel(Panel 组件会自动变成工具栏的形状)或 向 el 或 ToolBar 来实现多个工具栏的功能。 对于 ControlBar 组件,它 ,工具栏的添加方法也一样。不同 第 4 章 文本处理程序 87 可以 ar 的功能。 可以对它们的边框以及凹陷程度等属性的更改来达到更加美观的效果。 2. 用来显示程序运行中动态变化的量,比如 Word 软件中光 标所 态栏中显示的;也可以作为对用户的 提示 计是,先添加 StatusBar 组件,然后可以点击右键,通过 Panels Editor 命令来编辑 和修 4.4 其子窗体具有自己的菜单,在程序运 行时 或者替换掉主窗体的菜单,或 主窗体的菜单之中。具体的融合方式是由主窗体菜单中各菜单项的 情况下,各 GroupIndex 值默认是 需要分别对主窗体和子窗体的 是 二项会替换主窗体菜单的第二项,依此类推。 A 和菜单项 C 之间,那么 融和 upIndex 大于主窗体所有菜 单项 pIndex,那么 B 会追加到融合后 最后;如果 B 的 GroupIndex 小于主 窗体 而成为融合后窗体菜单的第一项。 我们把对文字起控制作用的“编辑”菜单和“格式”菜单放 入子 “文件”和“窗口”之间,融和前后 效果 实现在一行上放置两个 ToolB 这三个组件都 状态栏 状态栏即 StatusBar 组件,一般 在页、行、列以及 Num Lock 键的状态等都是在状 的显示面板。 界面设 改状态栏中的面板。 .3 菜单的融合 在 4.3 节中已经提及到,对于 MDI 应用程序,如果 体获得焦点,其菜单就会与主菜单相互作用,,一旦子窗 者把自己的菜单插入到 GroupIndex 以及子窗体菜单中各菜单项的 GroupIndex 共同决定的。 默认 个菜单项的 相同的, 菜单项设置相应的 GroupIndex 以得到想要的融合效果。融合的规则如下: 如果子窗体菜单项与主窗体菜单项具有相同的 GroupIndex,那么子窗体的菜单项将替换 主窗体和主窗体的菜单项。这样我们知道,默认情况下, 子窗体的菜单所有项的 GroupIndex 都 一样的,那么融合的时候,子窗体菜单的第一项会替换主窗体菜单的的一项,子窗体菜 单第 如果子窗体菜单项 B 的 GroupIndex 值在主窗体菜单中菜单项 以后,菜单项 B 会插入到菜单项 A 和 C 之间;如果 B 的 Gro 的 Grou 窗体菜单的 所有 么 B 会插入菜单项的 GroupIndex,那 在本章的文本处理程序中, 窗体中,并且设置其 GroupIndex 值在主窗体菜单项 如图 4-10。 图 4-10 菜单融合效果 第 4 章 文本处理程序 88 4.5 编辑功能的实现 代码编写工作。主要是针对菜单中的各个 菜单 应的处理代码,以完成相应的功能。包括文件的新建、打开、保存、打印; 选择 的设置;段落的控制;文字的查找和替换以及其他相 关的功能和操作 4.5 z 有的文档,都要先创建一个子窗体,然后判断如果 是打 的函数如下: all TMainForm::CreateMDIChild(String Name) TMDIChild *Child; Child->Caption = Name; Child->HaveName=false; Child->HaveName=true; 志为 true Name,FileExists(Name)在文件名为 Name 的文件存在时返回 true,否 则返 样不管是新建还是打开已有文件都可以通过对 CreateMDIChild()的调用来实 现。 用来标示是否文档已经命名的 bool 型变量。 Object *Sender) ateMDIChild("未命名" + IntToStr(MDIChildCount + 1)+".rtf"); ------------------------------------------------------------------ 文本 完成了程序的界面设置以后,现在开始具体的 项,编写相 区文本的复制、剪切、粘贴;字体 。下面按照菜单项的顺序讲述具体的代码以及其原理。 .1 文件操作 文档的新建和打开 不管是新建一个文档还是打开一个已 开已有文档,就要读取文档的内容。我们编写一个创建子窗体 void __fastc { //--- create a new MDI child window ---- Child = new TMDIChild(Application); //文档没有命名 if (FileExists (Name)) { Child->RichEdit1->Lines->LoadFromFile(Name); //如果是打开已有文件,设置已命名标 } } 入口参数是文件名 回 false。这 其中子窗体的 HaveName 是自己定义的 新建和打开菜单项的响应函数如下: void __fastcall TMainForm::FileNew1Execute(T { Cre } //--------- 第 4 章 文本处理程序 89 void __fastcall TMainForm::FileOpen1Execute(TObject *Sender) if (OpenDialog->Execute()) //文件打开对话窗,如果打开成功返回 true og 的 FileName 属性中 CreateMDIChild(OpenDialog->FileName); IChildCount 是父窗体的属性,它表示当前子窗体的数目。新建响应函数的效果 是打 件名为“未命名 x”的文件,其中 x 是一个数字序号,表示新建窗口之后子窗 体的 ,它会先表示在子窗体的 Caption 中,在保存文件 的时 { //文件的名字在 OpenDial } 其中 MD 开一个文 数目。当然“未命名 x”文件还不存在 候会做处理让用户更改文件名。 注意 对于 CreateMDIChild()、FileNew1Execute()、Open1Execute()函数,需要在头文件中声明, 在 public 部分添加: void __fastcall FileNew1Execute(TObject *Sender); void __fastcall FileOpen1Execute(TObject *Sender); void __fastcall CreateMDIChild(const String Name); 然后指定菜单项“新建”的 OnClick 事件对应 FileNew1Execute()函数,“打开”的 OnClick 事件对应 Open1Execute()函数。 z veItemClick(TObject *Sender) (TMDIChild *)ActiveMDIChild; iveMDIChild) //存在激活状态的字窗体 owChild->RichEdit1->Lines->SaveToFile(NowChild->Caption); //子窗体的 Caption 属性纪录文档名字 alse; else { FileSaveAsItem->Click(); //调用另存函数 } --------------------------------------------------------- 文档的保存、另存与关闭 void __fastcall TMainForm::FileSa { TMDIChild *NowChild= if(Act { if(NowChild->HaveName) //文档已经命名 { N NowChild->RichEdit1->Modified=f //保存结束设置文档修改标志为 false } } } //------------------ 第 4 章 文本处理程序 90 void __fastcall TMainForm::FileSaveAsItemClick(TObject *Sender) { TMDIChild *NowChild=(TMDIChild *)ActiveMDIChild; owChild->Caption; //设置默认保存文件名 wChild->Caption=SaveDialog1->FileName +".rtf"; 是绝对路径 NowChild->HaveName=true; 完成保存 hild 对它做 强制 存和另存之间有互相调用的关系:如果保存还没有命名的文件则调用另存来实现,而 另存 开”与“保存”的加速按钮,指定它们的 click 函数调用 相应 新建菜单的 Click 函数是 FileNew1Execute(),那么 指定 建加速按钮的 OnClick 事件的处理函数也是 FileNew1Execute()即可。 关闭一个子窗口,很自然的,我们不希望因为编辑的文档没有保存就关闭窗口而 使文 关闭窗口之前我们要加上对文件的保存操作。通过改写子窗体的 OnC id __fastcall TMDIChild::FormCloseQuery(TObject *Sender, bool &CanClose) { if(RichEdit1->Modified) //文档改动过 rmation, TMsgDlgButtons() << mbYes << mbNo << mbAbort,0); //按 Yes 按钮 件保存函数 MainForm->FileSaveItem->Click(); { //如果在设置保存文件名时取消保存操作,则不关闭子窗体 CanClose=false; if(ActiveMDIChild) { SaveDialog1->FileName=N if(SaveDialog1->Execute()) { No //SaveDialog1->FileName //设置文档已经命名 FileSaveItem->Click(); //调用 Save 菜单 Click 函数, } } } 文件的保存和另存菜单项的相应函数如上。其中,ActiveChild 是主窗体的一个属性,它 是指向当前活动子窗体的 TForm 型指针,我们需要用(TMDIChild *)ActiveMDIC 类型转换才能通过它访问到子窗体的组件。 保 操作又是在对文档命名之后通过调用保存操作来实现的。 对工具栏中相应的“新建”、“打 菜单项的 click 函数即可,如主菜单中 新 如果要 档丢失,所以,在 loseQuery 事件的相应函数来实现上述目的: vo { int Choose=MessageDlg("是否保存对文档的修改?", mtConfi if(Choose == mrYes) { //调用文 if(RichEdit1->Modified) 第 4 章 文本处理程序 91 } } else if(Choose == mrAbort) 应可以决定是否可以关 闭窗 。如果不能关闭,设置 CanClose 属性为 false 即可。 用来弹出一个确认窗口,返回用户按下的按钮标志。可供使 用的 mbOK(mrOk) 、 mbCancel(mrCancel) 、 mbYes(mrYes) 、 mbNo(mrNo) 、 bA 的 oseItemClick(TObject *Sender) { TMDIChild *NowChild=(TMDIChild *)ActiveMDIChild; if(ActiveMDIChild) { NowChild->Close(); } } z 打印与打印设置 打印与打印设置的实现都很简单, 设置对话框即可,代码如下: void __fastcall TMainForm::FilePrintItemClick(TObject *Sender) TMDIChild *NowChild=(TMDIChild *)ActiveMDIChild; >EndDoc(); throw; { //如果选择 abort,则取消关闭窗口操作 CanClose=false; } //如果选择 No,窗口关闭,文档不保存 } } OnCloseQuery 事件在将要关闭窗体之前触发,通过对此事件的响 体 程序中的 MessageDlg 函数 按钮有 m bort(mrAbort) 、 mbRetry(mrRetry) 、 mbIgnore(mrIgnore) 、 mbAll(mrAll) 、 mbNoToAll(mrNoToAll)、mbYesToAll(mrYesToAll),后边括号中是对应此按钮的返回值。窗 口 类型也可以选择,有 mtWarning、mtError、mtInformation、mtConfirmation 和 mtCustom。 最后,主窗体中的菜单项也可以实现对子窗体的关闭,对此菜单项的响应函数如下: void __fastcall TMainForm::FileCl 应用打印和打印 { if(ActiveMDIChild) { if(PrintDialog1->Execute()) { try{ NowChild->RichEdit1->Print(NowChild->Caption); } catch(...){ Printer()- 第 4 章 文本处理程序 92 Close(); bout1Execute(TObject *Sender) 窗口之后,焦点无法回到主窗体上,直到关于 窗口 4.5 替换 z 1->Font; 据对话窗返回的字体属性设置被选择部分文本的字体 RichEdit1->SelAttributes->Assign(FontDialog1->Font); 具栏中对粗体 划线字体的控制由下面代码完成: } } } } //--------------------------------------------------------------------------- void __fastcall TMainForm::FilePrintSetItemClick(TObject *Sender) { PrinterSetupDialog1->Execute(); } z 其他 程序的退出: void __fastcall TMainForm::FileExit1Execute(TObject *Sender) { } 关于窗口的打开: void __fastcall TMainForm::HelpA { AboutBox->ShowModal(); } 其中 ShowModal()方法可以使得打开关于 被关闭为止。 .2 字体、段落、查找与 字体控制 菜单中“字体”菜单项的响应函数如下: void __fastcall TMDIChild::StyleFontItemClick(T { Object *Sender) FontDialog1->Font=RichEdit 让字体 // 对话窗打开后显示当前字体 if(FontDialog1->Execute()) {//根 } } 工 ,斜体,下 void __fastcall TMainForm::ToolButtonBClick(TObject *Sender) 第 4 章 文本处理程序 93 { TMDIChild *NowChild=(TMDIChild *)ActiveMDIChild; if(!ActiveMDIChild)return; //如果没有激活状态的子窗体,直接返回 fsBold; else //为选定文本剔除粗体属性 NowChild->RichEdit1->SelAttributes->Style=TFontStyles()>>fsBold; } //--------------------------------------------------------------------------- void __fastcall TMainForm::ToolButtonIClick(TObject *Sender) { TMDIChild *NowChild=(TMDIChild *)ActiveMDIChild; if(!ActiveMDIChild)return; if(ToolButtonI->Down) //为选定文本加入斜体属性 NowChild->RichEdit1->SelAttributes->Style=TFontStyles()<RichEdit1->SelAttributes->Style=TFontStyles()>>fsItalic; //------- -------------------------------------------------------- ::ToolButtonUClick(TObject *Sender) d *)ActiveMDIChild; //为选定文本加入底线属性 NowChild->RichEdit1->SelAttributes->Style=TFontStyles()<RichEdit1->SelAttributes->Style=TFontStyles()>>fsUnderline; } 工具栏中还有对字体起控制作用的一个组合框和一个 TEdit 组件,但是它们要随着光标 所在位置以及选择区域的变化而适时的改变,而且粗体、斜体、下划线加速按钮也要根据光 标所在位置动态改变其状态,所以对它们的响应放在本节后边单独讲述。 z 段落格式 if(ToolButtonB->Down) //为选定文本加入粗体属性 NowChild->RichEdit1->SelAttributes->Style=TFontStyles()<< NowCh } ------------ void __fastcall TMainForm { TMDIChild *NowChild=(TMDIChil if(!ActiveMDIChild)return; if(ToolButtonU->Down) 第 4 章 文本处理程序 94 对段落的控制按钮都在工具栏中,有三种对齐方式以及列表样式。 居左对齐的实现代码如下: void __fastcall TMain { TMDIChild *NowChild=(TMDIChild *)ActiveMDIChild; if(!ActiveMDIChild)return; ld->RichEdit1->Paragraph->Alignment=taLeftJustify; } 右对齐和居中对齐的函数跟上面一样,只是需要把 taLeftJustify 换为 taRightJustify 和 { ild *NowChild=(TMDIChild *)ActiveMDIChild; if(!ActiveMDIChild)return; ->RichEdit1->Paragraph->Numbering=nsNone; 设置之 it1->Paragraph->Numbering=nsBullet; e; 属性 Numbering 有两个值,nsBullet 表示使用列表,nsNone 表示不使用 列表 ,所以需要把此菜单项的响应函数为: :StyleListItemClick(TObject *Sender) st->Click(); z 工具栏中字体状态组件对光标所在区的动态响应 代码如下,对代码的说明见注释内容: void __fastcall TMDIChild::RichEdit1SelectionChange(TObject *Sender) { if(RichEdit1->SelLength==0) { //设置粗体、斜体、下划线按钮状态 Form::ToolButtonLeftClick(TObject *Sender) NowChi taCenter。 列表样式按钮的事件响应代码如下: void __fastcall TMainForm::ToolButtonListClick(TObject *Sender) TMDICh if(NowChild->RichEdit1->Paragraph->Numbering==nsBullet) {//已经设置了 list,则取消 NowChild ToolButtonList->Down=false; } else //还没有设置 list,则 { NowChild->RichEd ToolButtonList->Down=tru } } 其中,段落的列表 格式。由于子窗体菜单中有控制列表的菜单项 void __fastcall TMDIChild: { MainForm->ToolButtonLi } 第 4 章 文本处理程序 95 MainForm->ToolB RichEdit1->SelAttribut uttonB->Down= es->Style.Contains(fsBold); MainForm->ToolButtonI->Down= RichEdit1->SelAttributes->Style.Contains(fsItalic); MainForm->ToolButtonU->Down= RichEdit1->SelAttributes->Style.Contains(fsUnderline); //设置段落对齐格式按钮状态 switch(RichEdit1->Paragraph->Alignment) { case taLeftJustify: MainForm->ToolButtonLeft->Down=true; break; case taCenter: MainForm->ToolButtonCenter->Down=true;break; case taRightJustify: MainForm->ToolButtonRight->Down=true; break; } //列表项状态 caBullet=1,caNone=0 MainForm->ToolButtonList->Down=bool(RichEdit1->Paragraph->Numbering); //设置字体名称、字体大小的显示 MainForm->ComboBox1->Text=RichEdit1->SelAttributes->Name; MainForm->EditFontSize->Text=RichEdit1->SelAttributes->Size; } } 因为 RichEdit1->SelAttributes、RichEdit1->Paragraph 返回的都只是光标所在处的属性,所 以如果选择了具有不同属性的一段文本,工具栏中的组件只会随光标所在位置的属性而改变, 择文本区的属性的更改,这不是我们所想要得。为此,我 对 SelLength 的判断。 z 工具栏中 ComboBox 与 TEdit 组件对字体的控制 - void __fastcall TMainForm::ComboBox1Change(TObject *Sender) TMDIChild *NowChild=(TMDIChild *)ActiveMDIChild; 而它们的改变又会触发对整个被选 们加上 工具栏中 ComboBox 组合框的初始化: void __fastcall TMainForm::FormCreate(TObject *Sender) { //获取系统字体 ComboBox1 >Items=Screen->Fonts; } 改变组合框选项对字体的控制: { if(!ActiveMDIChild)return; NowChild->RichEdit1->SelAttributes->Name= 第 4 章 文本处理程序 96 ComboBox1->Items->Strings[ComboBox1->ItemIndex]; } 改变 TEdit 组件的值对字体大小的控制: void __fastcall TMainForm::EditFontSiz e(TObject *Sender) { ild *NowChild=(TMDIChild *)ActiveMDIChild; D {//如果没有活 EditFontSiz } ntSiz {// 时 return; =EditFontSiz ize->Text=size; z 查找与替换 功能 功能,要实现这两个功能并不复杂,因为 C++ Builde 查找( 框(ReplaceDialog)。这两个对话窗在程序中只有当 使 ute 查找对话框而来的,它不但可以用来 查找 且支 。 窗的 Execute 方法,调出对话窗,然后具体的查找 件中设置。 所以,文本处理程序中,查找菜单的指定代码为: ld::EditFindItemClick(TObject *Sender) { Point(MainForm dth/2, Form->Top + MainForm->Height/2); FindDialog1->Execute(); } 在 ngDialog 组件的 OnFind 事件的处理代码 eChang TMDICh if(!ActiveM IChild) 动窗体,则置空 e->Text=""; return; if(EditFo 字体大小为 e->Text=="") 空的 候,对文本不做任何更改 } int size e->Text.ToInt(); //字体大小在 1-1638 之间 if(size<1)size=1; if(size>1638)size=1638; EditFontS NowChild->RichEdit1->SelAttributes->Size=size; } 查找与替换 是文本编辑器中很常用的 r 中提供 FingDialog)和替换对话 用了它的 Exec 方法后才会出现。替换对话框是基于 字符串,而 持替换指定字符串的功能 要使用查找功能,需要先使用查找对话 以及处理工作在查找对话窗的 OnFind 事 void __fastcall TMDIChi //设置查找窗体的位置在主窗体的中央 FindDialog1->Position = ->Left + MainForm->Wi Main 设置 Fi 如下: 第 4 章 文本处理程序 97 vo astcall TMDIChild: { ndAt, S //如果当前有 文本 当前 //否则,在文 hEdit1- R else os = 0 // ToEnd表示 档结尾的长度 = Rich =Ric t( Find tchCase); At != SetFocus(); RichEdit1->SelStart = FoundAt; RichEdit1->SelLength = F (); } e geD sg lo 要同 的功能,其查找功能的实现与查找对话窗相同,这里 不 ;“替 OnReplace 事件 实现 all T ceDi 下 id __f :FindDialog1Find(TObject *Sender) int Fou tartPos, ToEnd; 被选定 //那么在 被选择文本的最后一个字符处开始查找 本开头处开始查找 if (Ric >SelLength) StartPos = ichEdit1->SelStart + RichEdit1->SelLength; StartP ; 从查找位置开始到文 ToEnd Edit1->Text.Length() - StartPos; FoundAt hEdit1->FindTex Dialog1->FindText, StartPos, ToEnd, TSearchTypes()<< stMa if (Found -1) { //找到匹配字符串后,把焦点返回程序主窗体 MainForm-> indDialog1->FindText.Length lse { Messa lg("没有找到匹配的字符串!", mtWarning, TM DlgButtons() << mbOK,0); FindDia g1->CloseDialog(); //关闭查找框 } } 替换对话窗 时具有查找和替换 再列举其代码 换”菜单项的代码与“查找”菜单项也相似;替换功能通过对其 编写代码来 ,代码如下: void __fastc MDIChild::ReplaceDialog1Replace(TObject *Sender) { if(Repla alog1->Options.Contains(frReplace)) { //用户按 了对话窗中的替换按钮 int SelPos=RichEdit1->Lines->Text.Pos(ReplaceDialog1->FindTextA); if(SelPos>0) { 第 4 章 文本处理程序 98 RichEdit1->SelStart=SelPos-1; ceTextA; 串", } else if(ReplaceDialog1->Options.Contains(frReplaceAll)) 部替换 t FindAt do { if(Rich Sta else StartPos=RichEdit1->SelStart; End FindA if(Find { Rich Rich Rich Find in ou essag essa RichEdit1->SelLength=ReplaceDialog1->FindTextA.Length(); //将选定字符串替换为指定字符串 RichEdit1->SelText=ReplaceDialog1->Repla } else { MessageDlg("没有找到匹配的字符 mtWarning,TMsgDlgButtons() << mbOK,0); } { //全 in ,StartPos,ToEnd,FindCount=0; Edit1->SelLength!=0) rtPos=RichEdit1->SelStart+RichEdit1->SelLength; To =RichEdit1->Text.Length()-StartPos; t=RichEdit1->FindTextA(ReplaceDialog1->FindTextA, StartPos,ToEnd,TSearchTypes()<SelStart=FindAt; Edit1->SelLength=ReplaceDialog1->FindTextA.Length(); Edit1->SelText=ReplaceDialog1->ReplaceTextA; } Count++; }while(F if(FindC dAt!=-1); nt) { M eDlg("字符串替换完毕!", mtWarning,TMsgDlgButtons() << mbOK,0); } else { M geDlg("没有找到匹配的字符串", 第 4 章 文本处理程序 99 } } 4.5.3 剪贴板 z 介绍 TRichEdit 、复制、粘 贴的方法,我们只需 菜单项的代码如 tcall TMDIChild: {//剪切 R >CutT //------------------- all TMDIChild: it1->C } ----- void __fastcall T t1->P } 对主窗体工具栏 制、粘贴按钮,只需要让它们的 OnClick 事件调用子窗体的 对应 Click void __fastcall T ject *Sender) { owChild=(TMDIChild *)ActiveMDIChild; if(!ActiveMDIChild)return; z } mtWarning,TMsgDlgButtons() << mbOK,0); } 的使用及其他 剪贴板的使用 组件的时候已经讲过 TRichEdit 组件提供对被选文本进行剪切 要调用它们就可以完成对文本的剪切、复制和粘贴操作。 下: void __fas :EditCutItemClick(TObject *Sender) ichEdit1- } oClipboard(); -------------------------------------------------------- void __fastc {//复制 :EditCopyItemClick(TObject *Sender) RichEd opyToClipboard(); //-------------- -------------------------------------------------------- MDIChild::EditPasteItemClick(TObject *Sender) {//粘贴 RichEdi asteFromClipboard(); 中的剪切、复 菜单项的 函数即可,下面只列出剪切按钮的代码,其他的类似可得: MainForm::ToolButtonCutClick(TOb TMDIChild *N NowChild->EditCutItem->Click(); } 其他 全选功能的实现: void __fastcall TMDIChild::EditSelectAllClick(TObject *Sender) { RichEdit1->SelectAll(); 第 4 章 文本处理程序 100 对 MDI 子窗口的排列控制,这里只列出实现水平平铺控制的代码: call TMainForm::WindowTileItemClick(TObject *Sender) tbHorizontal; 。 4.5 一般的应用程序中都会把常用的几个菜单选项放入鼠标右键菜单中,为操作控制程序提 供方便。我们也可以在文本编辑 首先,在子窗体中添加一个 PopupMenu 组件,然后指定它 RichEdit 的 PopupMenu 属性。 编 u 组件的内容,添加剪切、复制、粘 设置列表格式、全选菜单项, 然后 菜单 ick 事件调用主菜单中相应菜单项的 Click 函数。例如对剪切菜 单, OnC 代码如下: astcall PopMemuCutClick(TObject *Sender) utIte 4.5.1 中文档新建加速按钮的方法 k()而不用编写代码,它们效果相同。 以设 图标一样,因为已经在子窗体中 include 了主窗体的头文件,所以子窗体中可以用主窗体的 ImageList 组件为各个菜单项指定 Imag 效果 void __fast { TileMode= Tile(); } 然后设置窗体排列控制的加速按钮的响应函数即可 .4 右键菜单 程序中加入鼠标右键菜单。 辑 PopupMen 贴、字体、 指定各个 项的 OnCl 相应它的 lick 事件的 void __f TMDIChild:: { EditC m->Click(); } 其他菜单项的代码类似,在此不一一列举。也可以象 一样设置右键菜单的 OnClick 函数为 EditCutItemClic 最后还可 定右键菜单的图标,跟主菜单的 eIndex, 如图 4-11。 图 4-11 右键菜单 4 巧--- 程 放,就是当用户用鼠 .6 高级技 拖放功能的实现 很多的应用 序都支持拖放文件,比如 Word、realone 等。所谓拖 第 4 章 文本处理程序 101 标 文件图标拖放到应用程序界面上时,程序可以打开这些文把 件。 要实现拖放功能,需要完成三个步骤(其原理请参看第九章内容)。 个程序可以接受拖动进来的文件。应该 放在 OnFormCreate 事件中。在 FormCreate()中加入 DragAcceptFiles(Handle,true),其中它的 件的窗口句柄,第二个参数如果是 true,则窗口可以接受拖动文件, void __fastcall TMainForm::FormCreate(TObject *Sender) Items=Screen->Fonts; 系统的 WM_DROPFILES 消息,指定处理此类消息的函数。 中加入如下内容: stcall MyDropFileFunction(TWMDropFiles & Msg); MESSAGE_HANDLER( yDropFileFunction); END_MESSAGE_MAP(TForm); 这样 文件拖动进窗体 WM_DROPFILE 时,程序便会把消 工作 DropFileFunction 数。 最后,就是要编写 MyDropFileFunction 函数来处理文件拖动消息。 void __fastcall TMainForm::MyDropFileFunction(TWMDropFiles & Msg) //获得拖动进来的文件数目 int NumOfFiles=DragQueryFile((HDROP)Msg.Drop,0xFFFFFFFF,NULL,0); 0;i //允许接受拖动文件 DragAcceptFiles(Handle,true); } 然后,需要截取 在主窗体的头文件 void virtual __fa BEGIN_MESSAGE_MAP WM_DROPFILES,TWMDropFiles,M ,当有 而触发 S 消息 息处理 交给 My 函 { for(int i= Strin NameOfFile.SetLength(Length) //从路径中截取文件扩展名 NameOfExt = NameOfFile.LowerCase().SubString( NameOfFile.Length()-3,4); if(NameOfExt==".txt" || NameOfExt==".rft") 第 4 章 文本处理程序 102 //创建子窗体 CreateMDIChild(AnsiS 打开文件 tring(NameOfFile)); } } 个参数是系统传递给应用程序的拖动文件消息,里面保存 着关于被拖动文件的参数;第二个参数说明了返回文件的索引;第三个参数是读取的文件路 要 。 } //释放拖放文件消息占用的内存资源 DragFinish((HDROP)Msg.Drop); 其中函数 DragQueryFile()的第一 径 保存的内存2018香港马会开奖现场;最后是这部分内存的大小。函数的返回值是路径的长度。如果把索引 参数设置为 0xFFFFFFFF,则函数返回被拖动文件的数目。 程序中,首先得到被拖动文件个数,然后用一个循环依次判断各个文件是不是文本文件, 如果是,则调用 CreateMDIChild()函数创建一个子窗体并打开文件 DragFinish()函数用来释放保存拖放文件路径所占用的内存资源。 图 4-12 拖放效果 第 4 章 文本处理程序 103 4 思考题 .7 Memo 组件,有工具条 、查找和替换等 为记事本程序添加合适的右键菜单 为记事本程序实现文件拖放功能 为之设定菜单融合规则 z 参照 Windows 记事本程序,设计自己的记事本,要求程序中有合适的菜单,而且使用 T z 在记事本程序中添加文本控制功能,如字体设置 z z z 编写一个多文档的程序, 《C++ Builder 6 编程实例精解 赵明现》 第 05 章 画图程序 本章重点 类、TBrush 类、TImage 组 件的 与图形相关的组件和类的使用,掌握在程序 中使 ■ TCanvas,TBrush,T ■ TImage 组件的使用 ■ 图形的绘制 出文 法 和 择 域的方法 用 状 果的实现 本章通过制作一个类似与 Windows 画图程序的 MyPaint 程序,着重讲述了 C++Builder 中的图象处理技术。主要内容有 TScreen 类、TCanvas 类、TPen 使用,光标的使用,图形文件的操作,剪贴板的使用以及图象处理的一些高级技巧。 学习目的 通过本章的学习,您可以掌握 C++Builder 中 用光标的方法,以及图形文件操作和图象处理的技巧。具体的,通过本章的学习,您需 要掌握一下内容: Pen 类的属性和方法 基本 技巧 ■ 图象上输 字的方 ■ 图象的放大 缩小 ■ 在图象中选 区 ■ 剪贴板的使 ■ 喷枪工具雾 效 ■ 图象的反色 ■ 图象的打开、保存与打印操作 ■ CoolBar 组件的使用 ■ CColorGrid 组件的使用 ■ CSpinEdit 组件的使用 ■ 复习加速按钮的用法 第 5 章 画图程序 105 本章典型效果图 第 5 章 画图程序 106 5.1 基本图形图像类 传统的 Winodws 程序绘图要通过 GDI(Graphics Device Interface)来实现,这对于程序初学 的人来说,GDI 的绘图系统却显得过于庞大复杂,使 用 些与 GDI 打交道的繁琐工作已经被封装在几种基本类之中,对于 序编写者来说,只要调用这些类的相关成员函数即可完成画图操作。具有图像属性类的组 件很 、屏幕特效的制作等都是很方便的。 5.1 TSc 窗体或者数 般用来控制程序 运行时屏幕的状态。它是一个全局变量,在任何一个程序运行的时候会自动初始化,通过它 可以获得当前屏幕的状态,如当前活动的窗体或者组件、屏幕的大小和分辨率、当前可用的 TScreen 常用的属性、方法如下表,大多数属性都是只读的。 表 5-1 TScreen 类属性/方法说明 属性/方法 说明 者或者只是想完成简单的图像绘制功能 起来非常繁琐。 在 C++Builder 中,这 程 多,这对程序界面的细节描绘 .1 TScreen 类 reen 类可以跟踪程序运行的时候创建了什么 据模块,一 输入法以及字体等。 ActiveControl 件后, 标志当前窗体上哪个组件具有输入焦点,只读属性,可以通过 SetFocusedControl 方法切换焦点到其他组件,焦点被切换到其他组 触发 OnActiveControlChange 事件 ActiveCustomForm 标志当前具有输入焦点的 TCuntomForm 类型的对象或者属性页,如果当 前焦点在 TForm 对象上,那么 ActiveCustomForm 与 ActiveForm 相同 ActiveForm 标志应用程序中具有焦点的窗体,如果当前系统焦点不在应用程序上, 那么 ActiveForm 标记为如果程序被激活后获得输入焦点的窗体。用 SetFocus 切换,切换后触发 OnActiveFormChange 事件 Cursor 用来控制鼠标的图案 Cursors 当前程序可以使用的鼠标图案的列表 DefaultIme 程序默认使用的输入法 DesktopHeight 标志桌面高度 DesktopWidth 标志桌面的宽度 DesktopLeft 屏幕左上角 x 坐标 DesktopTop 屏幕左上角 y 坐标 Fonts 系统可用的字体列表(不同于打印可用的字体) FormCount 当前屏幕上的窗体数目 Forms 当前显示的窗体列表 第 5 章 画图程序 107 Realign(void) 按照窗体的排列属性重新排列窗体 ResetFonts() 更新 Fonts 的字体列表 DisableAlign(void) 忽略窗体的排列属性 EnableAlign(void) 启用窗体排列属性 5.1.2 TCanvas 类 TCanvas 对象一般做为组件的属性出现,是绘图的基本类。组件的 Canvas 属性可以看作 或对图形进行操作控制的接口。 表 5-2 TCanvas 常用属性 是为编程者提供的绘制图形 z TCanvas 常用属性及说明如下表: 属性 说明 Pen 它是一个 TPen 类的对象,决定 Canvas 上画图用的画笔类型,包括画笔的 画笔宽度、线性、模式等属性 颜色、 Brush 它是一个 TBrush 类对象,决定当前画图所用的画刷的颜色、填充方式等 属性 Pixels 它是一个存放有当前画布上所有象元的颜色值的 TColor 类型的二维数组, 的值改变画布上象元的颜色 可以通过更改它 Font 表示画布上输出文本(TextOut 方法)所用的字体 CanvasOrientation 表示输出方向,可以取 coLeftToRight、coRightToLeft,当取 coRightToLeft 时,画布坐标原点在右上角 枚举类型, ClipRect 设定画布上的矩形绘图区域,只有在区域内的绘图才会被显示,区域外的 略 图形操作将被忽 PenPos 记录当前画笔所在位置,是 LineTo 方法的画线起点。通过 Mo 改变 PenPos veTo 方法 的值 TextFlags 文本显示模式 Handle Windows GDI 句柄,访问此属性相当于调用 GetDC 方法 CopyMode 拷贝模式,即使用 CopyRect 方法时的效果,具体取值说明见表 5-3 其中,CopyMode 属性在从另外一个画布上拷贝图像是起作用,设置被拷贝图像在画布 map 对象绘制在画布上的时候,也用到此属性。CopyMode 的取 表 5-3 CopyMode 取值与意义 意义 上的绘制方式。在一个 TBit 值和意义如下表: 取值 cmBlackness 设定目标图形填充颜色为黑色 cmDstInvert 忽略源图像,直接对目标矩形取反 第 5 章 画图程序 108 cmMergeCopy 将源图像和目标图像进行逻辑与操作 cmMergePaint 将源图像和目标图像进行逻辑或操作 cmNotSrcCopy 将源图像取反然后复制到目标矩形 cmNotSrcErase 将源图像和目标图像进行逻辑与操作,然后对混合后的图像取反 cmPatCopy 复制源矩形的模式 cmPatInvert 用异或操作将源矩形的模式与目标矩形的模式混合后再取反 cmSrcCopy 复制源矩形的图像到目标矩形 cmSrcErase 将目标矩形的位图取反,与源图像进行逻辑与操作 cmSrcInvert 将画布上图像与源位图进行逻辑异或操作 cmSrcPaint 将画布上图像与源位图进行逻辑与操作 cmWhiteness 用白色填充目标矩形 上的图像进行剪切操作的时候,需例如,在对画布 然后把画布用 要先把画布上的图像复制到剪贴板, 下语句来实现: ess; //设置用白色填充目标矩形 设置整个画布为目标矩形 Image->Canvas->CopyRect(rec,Image->Canvas,rec); 像复制到目标矩形,但是由于 CopyMode 是白色填充,所以此操作 //会忽略源图像,直接用白色填充目标矩形 ag 调用 Canvas 的绘图方法来完成,所以在这里对 Canvas 的 表 5-4 TCanvas 常用方法 说明 白色填充,就可以通过以 TRect rec; //定义一个矩形 …… //完成对目标矩形的复制操作 Image->Canvas->Cop Mode=cmWhiteny rec=Rect(0,0,Image->Width,Image->Height); // //将画图图像作为源图 Im e->Canvas->CopyMode=cmSrcCopy; //恢复到原来的复制模式 z TCanvas 类常用方法及说明如下表 所有的画图操作基本上全要靠 常用绘图方法做比较详细的说明。 方法 Arc Arc(int X1, int Y1, int X2, int Y2, int X3, int Y3, int X4, int Y4),绘制一段弧, 弧线所在椭圆由(x1,y2)、(x2,y2)确定;起点在椭圆中心与(x3,y3)连线交椭 ,y4)连线交椭圆圆周得到的点,从 起点到终点以逆时针方向绘制弧线 圆圆周得到的点;终点在椭圆中心与(x4 Chord Chord(int X1, int Y1, int X2, int Y2, int X3, int Y3, int X4, int Y4),绘制一个 由 Arc 弧形以及弧形起始点连线组成的封闭图形。参数的意义与 Arc 方法 参数相同 BrushCopy BrushCopy(const Types::TRect &Dest, TBitmap* Bitmap, const Types::TRect 图 Bitmap 中由 Source 指定的区域拷贝到画 布上 Dest 所指定的区域之中,并用画笔的当前颜色替换位图的颜色。 颜色属性,则可实现位图的半透 明拷贝效果。 &Source, TColor Color),把位 设置 Brush 属性的 Color 值为当前画图的 第 5 章 画图程序 109 CopyRect CopyRect(const TRect &Dest, TCanvas* Canvas, const TRect &Source),从另 一个画布 Canvas 上的 Source 区域拷贝图像,复制到当前画布的 Dest 区域。 TCanvas 类的 CopyMode 属性决定复制的方式 Draw(int X, int Y, TGraphic* Graphic),将 Graphic 指定的图像画在画布的 ,Y)坐标处 Draw (X DrawFocusRect DrawFocusRect(const TRect &Rect),在 Rect 区域绘制矩形,特别的是,它 使用异或方式,在第二次调用的时候原矩形将消失 Ellipse Ellipse(int X1, int Y1, int X2, int Y2); Ellipse(TRect Rect),在画布上绘制椭 圆,椭圆的外接矩形左上角和右下角分别为(X1,Y1)和(X2,Y2),也可以用 是正方形,将绘制圆形 矩形区域 Rect 确定。如果指定矩形 FillRect FillRect(const TRect &Rect),对矩形区域 Rect 用当前画刷进行填充。其中 矩形左、上边界也被填充,但是右、下边界不被填充 FloodFill(int X, int Y, TColor Color, TFillStyle FillStyle),用于填充坐标(X,Y) 所在的区域,这个区域由 Color 值和填充方式 FillStyle 确定。如果 FillStyle 那么填充操作在遇到与 Color 颜色不同的点为止;如果取 fsBorder,那么填充操作在遇到与 Color 颜色相同的点为止 FloodFill 取 fsSurface, FrameRect FrameRect(const Types::TRect &Rect),绘制矩形区域 Rect 的边界,线宽为 一个像素 LineTo LineTo(int X, int Y),用当前 Pen 属性绘制一条从 PenPos(当前画笔位置) 到(X,Y)的线段,绘制结束后,PenPos 变为(X,Y)。注意,(X,Y)不在所画线 段上 MoveTo MoveTo(int X, int Y),将当前画笔位置设到(X,Y),一般与 LineTo 结合使用, 用来制定线段的起点 Pie Pie(int X1, int Y1, int X2, int Y2, int X3, int Y3, int X4, int Y4),绘制饼图, 参数意义与 Arc 相同 Polygon(const TPoint * Points, const int Points_Size)Polygon ,用当前 Pen 属性绘制 定,然后用当前 Brush 属性对多边形 点的数组下标(顶点数目-1) 闭合多边形,多边形顶点由 Points 确 区域进行填充;Points_Size 是最后一个定 Polyline(const Types::TPoint* Points, const int Points_Size),绘制由 Points 指 定的点列为定点的多边形。它的作用相当于先 Moveto 到第一个点,然后 次用 LineTo 画线到下一个点,不同的是 Polyline 并不改变 PenPos 的值 Polyline 依 PolyBezier PolyBezier(const TPoint* Points, const int Points_Size),绘制贝塞尔曲线 Rectangle Rectangle(int X1, int Y1, int X2, int Y2);Rectangle(TRect Rect),用 Pen 绘制 矩形,然后用 Brush 填充 一个由(X1,Y1),(X2,Y2)或者 Rect 指定的 StretchDraw StretchDraw(const TRect &Rect, TGraphic* Graphic),将图像 Graphic 做拉 伸以适应 Rect 然后在 Rect 区域中绘制 TextHeight TextHeight(const AnsiString Text),返回文本 Text 在画布中的高度 TextWidth TextWidth(const AnsiString Text),返回文本 Text 在画布中的宽度 TextOut TextOut(int X, int Y, const AnsiString Text),在(X,Y)处用当前字体 Font 输出 第 5 章 画图程序 110 字符串 Text TextRect TextRect(const Types::TRect &Rect, int X, int Y, const AnsiString Text),在矩 形区域 Rect 的(X,Y)处输出字符串 5.1.3 TPen 类 TPen 画笔类用于在画布上绘制各种线段。 可以改变线段颜色,Color 的取值可以是 clBlack(黑色)、clGreen(绿 等。另外,也可以自己指定颜色的 RGB 值,利用 RGB ,如 TColor(RGB(255,0,0))表示红色。 要想从 Color 属性中分离出 RGB 三原色,可以用ColorToRGB 函数来实现。实际上,TColor 三原色(最高位字节表示与系统 色的匹配),因此可以用如下方法分离出 Color 的 RGB 值: olor Color=clRed; r); 8); ); 值:255,0,0。 2.Mode 属性 合当前的颜色、屏幕颜色或着它 的反值对线段的颜色进行重新定义,但这并不改变 Color 的属性。 表 5-5 TPen 类 Mode 属性的取值 象素颜色 1.Color 属性 设置其 Color 属性 色)、clRed(红色)、clBlue(蓝色) 函数来设置颜色值,然后转换为 TColor 类型 是一个四字节的 DWORD 数据,低三个字节分别存放 RGB 颜 BYTE R,G,B; TC R=BYTE(Colo G=BYTE(Color>> B=BYTE(Color>>16 这样,便可得出 Color 的 RGB Mode 属性定义线段在背景上显示颜色的模式。它可结 们 Mode 的取值见下表: Mode 取值 pmBlack 总是黑色 pmWhite 总是白色 pmNop 不改变颜色 pmNot 为 Canvas 背景颜色取反 pmCopy 使用 Color 属性中的颜色 pmNotCopy Color 属性中颜色取反 pmMergePenNot 画笔颜色与画布背景颜色的反色的混合 pmMergeNotPen 画布颜色与画笔颜色反色的混合 pmMerge 画笔颜色与画布背景色的混合 pmNotMerge pmMerge 的反色 画笔颜色与画布颜色的异或pmXor 第 5 章 画图程序 111 pmNotXor PmXor 的反色 3.Style 属性 表 5-6 TPen 类 Style 属性的取值 说明 Style 用于控制线段的线形。取值如下表: 取值 psSolid 实线 psDash 间断线 psDot 虚线 点划线 psDashDot psDashDotDot 双点划线 psClear 不绘线 psInsideFrame 实线,如果线宽大于 1 个象素,则绘制在边框的里面 其中,当线宽大于 1 的时候,点线和虚线类型不可用。 素。 填充,也可以用一个图象填充。 1.Bitmap 属性 个 TBitmap 类的位图,可以用此位图对闭合区域进行填充。位图最大是 8×8 8×8,那么会截取位图左上方 8×8 大小的部分作为填充位图。填充操作 为 TBrush 不会自动释放。 指定填充颜色。 指定填充风格,Style 的取值以及其对应的填充图案如下表: 表 5-7 TBrush 类 Style 的取值 4.Width 属性 Width 属性设置线段宽度,单位为象 5.1.4 TBrush 类 TBrush 画刷类用于对闭合区域的填充,可以用画刷的颜色 它指向一 象素的,如果大于 完成之后要释放 Bitmap,因 2.Color 属性 3.Style 属性 取值 图案 取值 图案 bsSolid bsCross bsClear bsDiagCross 第 5 章 画图程序 112 bsBDiagonal bsHorizontal bsFDiagonal bsVertical 5.2 TImage 组件的使用 TImage 组件提供一个区域,使得绘图在此区域中进行,而组件本身并没有提供更多的绘 anvas 类中。 、指定显示位图等方法,为图象处理提供方 函数 为 X 轴正方向,向下为 Y 绘图函数的说明请参看表 5-4 函数 候为了美化界面,需要图象精致细腻,但是使用前面介绍的画图函数,通常很难得 存成 bmp 格式 oadFromFile 读取显示。也可以用 SaveToFile 函 处理程序,它应该具有读取和保存图象文件的功能,以实现对已有图象的 LoadFromFile 函数 一个图象文件中读取图象文件显示在 TImage 组件中,可以用 LoadFromFile 函数。 面是一个打开图象文件的实例: if (OpenPictureDialog1->Execute()) 图方法,因为画图的操作全被封装在 TC 但是 Image 提供文件操作的方法,以及清屏 便。 5.2.1 绘图 C++Builder 中组件以象素为单位,坐标原点在左上角,向右 轴正方向。绘图的函数基本全都封装在 TCanvas 类中,对 TCanvas 常用方法,在此不再赘述。 5.2.2 文件相关的 有时 到十分精细的图画。我们可以先用其它图象处理工具处理得到画质好的图片, 的文件,然后通过图象组件的文件读取函数 L 数实现对组件中图象的保存,以方便以后的调用。 对于一个图象 修改以及保存现在编辑的图象以备他人使用或以后继续编辑。 z 要从 下 void __fastcallTForm1::Open1Click(TObject *Sender) { { CurrentFile = OpenPictureDialog1->FileName; Image->Picture->LoadFromFile(CurrentFile); 第 5 章 画图程序 113 } } 有时希望把文件中的图象显示在指定的位置,可以用下面方法实现: _fastcallTForm1::Open1Click(TObject *Sender) raphics::TBitmap(); OpenPictureDialog1->Execute()) { CurrentFile = OpenPictureDialog1->FileName; Image->Canvas->Draw(20,20,tmp_bmp); mp_bmp; } r); // 否则,调用另存菜单的处理函数获得文件名 if (SaveDialog1->Execute()) // 存对话框,得到文件名 { CurrentFile = SaveDialog1->FileNam 将文件名保存到 CurrentFile 中 此时文件已经命名,调用保存菜单的函数保存图象 void _ { Graphics::TBitmap *tmp_bmp=new G if ( tmp_bmp->LoadFromFile(CurrentFile); } delete t 上面程序实现读取一个文件之后在 Image 中(20,20)的位置绘制出图象。 z SaveToFile 函数 将 Image 中图象保存到文件,它跟 ReadFromFile 一样,只需要一个文件名参数。实例如 下: void __fastcallTForm1::Save1Click(TObject *Sender) { if (!CurrentFile.IsEmpty()) Image->Picture->SaveToFile(CurrentFile); //如果已经命名,保存图象到文件名 CurrentFile else SaveAs1Click(Sende } void __fastcallTForm1::SaveAs1Click(TObject *Sender) { 保 e; // Save1Click(Sender); // } } 第 5 章 画图程序 114 5.3 界面的创建 MyPaint 绘图程序仿照 Windows 自带的“画图”程序制作,界面与之类 一个主菜单、左边竖立的绘图工具栏、下边的颜色选择框等。设计界面如图 5-1 所 体背景设为 clGray 灰色。注意窗体的 Color 属性设为灰色后,添加 CoolBar 后 CoolBar 个菜单项的设计参照 Windows 画图程序,菜单组件的使用参看第四章 4.1.2 节 5.3.1 窗体外形配置 本章所要制作的 似,包括 示。 主窗 的背景色默认也会变为灰色,需要更改它为 clBtnFace。 主菜单各 的相关内容。 图 5-1 MyPaint 画图程序界面设计 左侧竖立的为一个 TCoolBar 组件,它在选项板“Win32”页中,其使用说明参看第四章 按钮(SpeedButton),并设置它们具有相同的 它们的 AllowAllUp 属性默认应该为 false,如果不是更改为 false, 个按钮在同一时刻只有一个被按下。最后,为它们指定各自的图标, 标的方法为点击属性检查器中 Glyph 属性后边的 4.4.2 节相关内容。在工具栏中添加 16 个加速 GroupIndex 属性(如 1),此时 这么做的目的是使得 16 按钮,会弹出“Picture Editor”设置其图 第 5 章 画图程序 115 对话框,点击“Load”选择指定的图片即可。 右侧虚线部分是一个 TImage 组件,它是图象的显示和处理区域。将 TImage 组件放置在 ,如果 Image 的大小超过窗口的显示范围时,ScrollBox 组件 动栏。设置 ScrollBar 的 Align 属性为 alClient,即填充整个窗口。ScrollBox 组 Additional”页中。 l 面板组件来实现,为了让面板靠在窗体下 om。面板中分三个区域: .左边的部分也是一个小 TPanel 组件,在它上面放置两个 TShape 组件,用来显示当前 ,另外,为了美观,将它的 BevelOuter 属性设为 bvLowered。对 色(Brush 属性的 Color 属性)分别为黑色和 线颜色(Pen 属性的 Color 属 ColorGrid 类型的组件,它在选项卡的 Samples 的属性设置 取值 说明 一个 ScrollBox 组件之上,这样 会自动添加拖 件在选项板“ 下方是颜色选择工具栏,我们使用一个 TPane 方,设置它的 Align 属性为 alBott 1 绘图所用的前景色和背景色 显示颜色的两个 TShape 组件,设置它们填充颜 白色,填充类型(Brush 的 Style 属性)都为 bsSolid;设置它们边 性)为其填充色的反色,边线类型为 bsSolid。 2.中间部分为颜色选择区,是一个 TC 页中。对它的属性设置如下: 表 5-8 TCColorGrid 组件 属性 GridOrdering go8x2 以每行 8 个,共两行的方式显示该组件 BackgroundEnabled true 支持背景色选择 BackgroundIndex 15 初始化背景颜色为白色 支持前景色选择 ForegroundEnabled true ForegroundIndex 0 初始化前景颜色为黑色 3.右边部分为画笔宽度选择区域,它也是一个 TPanel 面板,上面放置一个 TImage 组 个小画笔图形,表示这里设置的值是画 TCSpinEdit 组件在选项卡的 Samples 页 功能相同。设置其初始值(Value 属 (MinValue 属性)为 1,每次增加或减小的间隔(Increment 属性)为 1。 状态信息,如当前画笔所在坐标等。其使用 图片的对话框(TOpenPictureDialog 组件和 TSavePictureDialog lorDialog 组件)以及打印和打印设置对话框(TPrintDialog 组 og 组件)。 置 方法 运行中,我们希望随着程序运行状态以及鼠标的位置的变化,鼠标的形状也能相应 中,在使用放大镜的时候,鼠标变为放大镜的形状;在选择区域的时候, 件和一个 TCSpinEdit 组件。TImage 组件用于显示一 笔的线宽;TCSpinEdit 组件用于改变画笔线宽的值。 跟 TEdit 加 TUpDown(参看 4.4.1)完成的中,它的功能 1,最小值性)为 窗体最下方是一个状态栏,用于帮助或其它 参考 4.4.2。 另外,还添加了打开和保存 选取对话框(TCo组件)、颜色 TPrintSetupDial件和 5.3.2 光标的设 z 光标的使用 程序 改变。比如画图过程 第 5 章 画图程序 116 变为十字形状等等。 对于一个组件,我们可以简单的通过设置其 Cursor 属性来定义鼠标移动到该组件时的光 Cursor 属性可以取的值以及 动态的改变光标,下面的一段程序就示例出如何 TFormMain(TComponent* Owner) mIndex = 0; ender) int cursors[] = { , crNone, crArrow, crCross, crIBeam, rMultiDrag, LWait, crNo, crAppStart, crHelp}; 窗体上的光标会随之改变。 当然,我们也可以使用自己的光标。要用自定义的光 使用光标编辑工具(如 C++Builder 自带的 Image Editor 工具)制作一个光标 来表示该光标,如 const crMyCursor = 5,但是注意不要与系统光 函数获得光标句柄。这样就可以通过设置全局对象 Screen 们分别用于从资源中和文 m1::FormCreate(TObject *Sender) crMyCursor2]=LoadCursorFromFile("NewCursor.cur"); wCursor.cur 中读取光标 crMyCursor1; 标形状。点击属性检查器中 Cursor 属性的下拉菜单,就可以看到 它们对应的光标形状。 但是,往往我们需要在程序运行过程中 改变组件的光标属性: __fastcall TFormMain:: : TForm(Owner) { ComboBox1->Ite } //--------------------------------------------------------------------------- void __fastcall TFormMain::ComboBox1Change(TObject *S { static crDefault crSize, crSizeNESW, crSizeNS, crSizeNWSE, crSizeWE, crUpArrow, crHourGlass, crDrag, crNoDrop, crHSplit, crVSplit, c crSQ Cursor = TCursor(cursors[ComboBox1->ItemIndex]); } 程序中加入了一个组合框,选择组合框中不同的值, 上面用到的都是系统内建的光标, 标,首先,需要 文件;然后,声明一个常量 标值冲突;最后,调用 Windows API 的 Cursor 属性来设置光标了。 其中,读光标的函数有 LoadCursor 和 LoadCursorFromFile,它 件中读取光标。示例程序如下: const crMyCursor1 = 5; const crMyCursor2 = 6; void __fastcall TFor { Screen->Cursors[crMyCursor1] = LoadCursor(HInstance, "NewCursor"); //从资源中读取 NewCursor 名字的光标 Screen->Cursors[ //从文件 Ne Cursor = 第 5 章 画图程序 117 //Cursor=crMyCursor2; l=6, crGetColor=7,crZoom=8, 读取的方式,具体代码参看 5.6.4 节内容。 图功能实现 标来完成的 根据鼠标的状态改变得到需要画图 的 态,进行实际的绘图操作。 。在主窗体的头文件中加 Rect, dtErase, dtFill, dtGetColor, dtZoom, dtPencil, xt, dtLine, dtRectangle, dtEllipse, dtRoundRect}; 然后定义 TDrawingTool ToolState,这样在工具栏中的按钮被点击时,只需要改变 ovePt,分别表示画图区域的起始 中改变它们的值。 时记录 ,然后从 Origin 到 MovePt 绘制线段即可。 在鼠标拖动过程中需要绘制预览的图形,并且在鼠标移动到新的位置是需要擦除之前的 难,只需要在 Canvas 的画图操作开始时设置其画图模式为 复到原 : oint TopLeft,TPoint BottomRight, TPenMode AMode); 以及绘制选择区域的边线等操作, 后根据 ToolState 的不同,用 switch 语句实现各个不同的功能。 销对选定区域的选择,工具栏中大部分的按钮被按下时都要进行 如下: --------------------------------------------------- t TopLeft, TPoint BottomRight,TPenMode AMode) } z 本程序中对光标的设置 在本程序中,对光标的做如下设置。 头文件中加入: const int crSelectPos=crCross, crErase=5,crFil crPencil=9,crBrush=10,crFog=11; 光标文件的读取采取从文件中 5.4 画 5.4.1 设计思路 在绘图程序中,绘图的操作完全是由鼠 。 状的区域的端点坐标,然后根据工具栏中绘图工具按钮 对工具栏,我们要定义一个枚举变量来表示其中被按下的按钮 入: enum TDrawingTool {dtNoEdit, dtSelect sh, dtFog, dtTedtBru ToolState 以及 TImage 组件的光标即可。 对于鼠标位置,需要在头文件中定义 TPoint Origin, M 点和终点(如线段的两断线、举行区域的左上和右下点),在适当的鼠标事件 左键被按下时,记录当前坐标到 Origin,在鼠标左键弹起如对于画线段操作,在鼠标 前坐标到 MovePt当 预览图形。要实现这个功能其实不 逻辑“异或”操作即可,因为对“异或”的绘图区,只需要再“异或”一次就可以恢 来的图象了。 程序中有两个需要频繁调用的函数。它们在头文件中定义如下 void __fastcall DrawShape(TP EraseSelect(void); void __fastcall 其中 DrawShape()函数针对划线、矩形、圆角举行、椭圆 将其合并在一个函数中,然 Select()函数的功能是撤Erase 这个操作。它们的实现代码 -----//------------------- void __fastcall TForm1::DrawShape(TPoin 第 5 章 画图程序 118 { //设置画笔的模式 Image->Canvas->Pen->Mode = AMode; //设置画刷的填充方式 s->Brush->Style=bsClear; se dtLine : mage->Canvas->MoveTo(TopLeft.x, TopLeft.y); eTo(BottomRight.x, BottomRight.y); break; SelectRect: Image->Canvas->Rectangle(TopLeft.x, TopLeft.y, BottomRight.x, BottomRight.y); e(TopLeft.x, TopLeft.y, BottomRight.x, t(TopLeft.x, TopLeft.y, BottomRight.x, tomRight.x)/2, (TopLeft.y - BottomRight.y)/2); nvas 的 mode ->Canvas->Pen->Mode=pmCopy; ----------------------------------------------------- id) 线 ,pmNotXor); 画刷的状态 Image->Canva switch (ToolState){ ca I Image->Canvas->Lin case dt case dtRectangle : break; case dtEllipse : Image->Canvas->Ellips BottomRight.y); break; case dtRoundRect : { Image->Canvas->RoundRec BottomRight.y, (TopLeft.x - Bot break; } } //恢复 Ca Image } //---------------------- void __fastcall TForm1::EraseSelect(vo { if(HaveSelected) { //重新设置画笔和画刷的属性,用于消除选定时候留下的虚 Image->Canvas->Pen->Style=psDot; Image->Canvas->Pen->Width=1; DrawShape(Origin,MovePt HaveSelected=false; //恢复画笔 第 5 章 画图程序 119 Image->Canvas->Pen->Style=psSolid; mage->Canvas->Pen->Width=CSpinEdit1->Value; } --------------------------------------------------------------------------- uble ZoomSize; //放大倍数 否处于放大状态 ect; //标记选定的区域 //标记是否存在选择区 更改 ToolState 的状态,更改 TImage 组件的 其它操作。具体代码如下: ject *Sender) rDefault); rm1::SBtnSelectRectClick(TObject *Sender) ; tPos); -- ender) seSelect(); ToolState=dtErase; Image->Cursor=TCursor(crErase); I } // 最后,在头文件中还要定义下面的变量: int FogSize; //雾化区域的大小 do bool ZoomOut; //标记是 TRect SelectR bool HaveSelected; 5.4.2 工具栏 如前所述,工具栏中按钮被按下时,只需要 光标,以及完成一些相关的 //------------------------------------------ void __fastcall TForm1::SBtnNoEditClick(TOb { //退出编辑命令 ToolState=dtNoEdit; Image->Cursor=TCursor(c } //--------------------------------------------------------------------------- void __fastcall TFo { //矩形区域选择 ToolState=dtSelectRect Image->Cursor=TCursor(crSelec } //------------------------------------------------------------------------- void __fastcall TForm1::SBtnEraseClick(TObject *S { //擦除 Era 第 5 章 画图程序 120 } //--------------------------------------------------------------------------- //填充 lState=dtFill; Im //------------------------------------------ void _ ll TForm1::SBtnGetColorClick(TObject *Se { //吸色管 EraseSelect(); Too dtGetC Image->Cursor=T } //- ----- voi Form { // Er -- lClick(TObject *Sender) //铅笔 (crPencil); ------------------------------------------------------------------------- void __fastcall TForm1::SBtnBrushClick(TObject *Sender) void __fastcall TForm1::SBtnFillClick(TObject *Sender) { EraseSelect(); Too age->Cursor=TCursor(crFill); } --------------------------------- _fastca nder) lState= olor; Cursor(crGetColor); ------------------ --------------------------------------------------- d __fastcall T 1::SBtnZoomClick(TObject *Sender) 缩 放功能 aseSelect(); ToolState=dtZoom; Image->Cursor=TCursor(crZoom); } //------------------------------------------------------------------------- void __fastcall TForm1::SBtnPenci { EraseSelect(); ToolState=dtPencil; Image->Cursor=TCursor } //-- 第 5 章 画图程序 121 { rBrush); CSpinEdit1->Text=13; //设置默认画刷宽度 --------------------- *Sender) rsor(crFog); EraseSelect(); ---------------------------- ineClick(TObject *Sender) tate=dtLine; sor=TCursor(crSelectPos); ) //画刷 EraseSelect(); ToolState=dtBrush; Image->Cursor=TCursor(c } //------------------------------------------------------ void __fastcall TForm1::SBtnFogClick(TObject { //喷雾效果 EraseSelect(); ToolState=dtFog; Image->Cursor=TCu } //--------------------------------------------------------------------------- void __fastcall TForm1::SBtnTextClick(TObject *Sender) { //输入文本 ToolState=dtText; Image->Cursor=TCursor(crSelectPos); } //----------------------------------------------- void __fastcall TForm1::SBtnL { //绘制线段 EraseSelect(); ToolS Image->Cur } //--------------------------------------------------------------------------- void __fastcall TForm1::SBtnRectangleClick(TObject *Sender { //矩形 EraseSelect(); 第 5 章 画图程序 122 ToolState=dtRectangle; Image->Cursor=TCursor(crSelectPos); --------------------- der) Image->Cursor=TCursor(crSelectPos); r) ToolState=dtRoundRect; rSelectPos); -------------------- 色随之变化。但是颜色 Image 组件中的属性做更改,而是在绘图的时候,会从 Shape 组件 直接更改 Image 中 Canvas 的画笔 为宽度。 对颜色栏中组件的响应代码如下: stcall TForm1::CColorGrid1Change(TObject *Sender) //改变前、背景色在 Shape 组件中的显示 //修饰 Shape 组件的边框,将边框颜色取为 Brush->Color 的反色 } //------------------------------------------------------ void __fastcall TForm1::SBtnEllipseClick(TObject *Sen { //椭圆 EraseSelect(); ToolState=dtEllipse; } //--------------------------------------------------------------------------- void __fastcall TForm1::SBtnRoundRectClick(TObject *Sende { //圆角举行 EraseSelect(); Image->Cursor=TCursor(c } //------------------------------------------------------- 5.4.3 颜色选择框 编写 CColorGrid 组件的 OnChange 事件,让左侧 Shape 组件的颜 选择框的颜色改变不对 T 中读取前景色和背景色。 画笔宽度的改变也是这样,CSpinEdit 组件的改变并不 宽度,而是画图是从其中读取其 Value 作 //--------------------------------------------------------------------------- void __fa { FGShape->Brush->Color=CColorGrid1->ForegroundColor; BGShape->Brush->Color=CColorGrid1->BackgroundColor; 第 5 章 画图程序 123 FGShape->Pen->Color=TColor(RGB( 0xFF-BYTE(FGShape->Brush->Color), 0xFF-BYTE(FGShape->Brush->Color>>8), 0xFF-BYTE(FGShape->Brush->Color>>16) )); BGShape->Pen->Color=TColor(RGB( 0xFF-BYTE(BGShape->Brush->Color), 0xFF-BYTE(BGShape->Brush->Color>>8), 0xFF-BYTE(BGShape->B )); ------------------------------------------------------ oid __fastcall TForm1::CSpinEdit1KeyPress(TObject *Sender, char &Key) //禁止输入非数字符号 ace 键 { dit1->Text=="")) 入不合法 -------- 以每次其内容被改变的时候 是已经清空,如果是,把其 Value 置为 1 ext == "" || CSpinEdit1->Text.ToInt()<=0) 0 rush->Color>>16) //设置 Canvas 画笔和画刷的颜色 } //--------------------- v { if(Key!=8) //Key=8 时为 BackSp if((Key < '0' || Key>'9')||(Key=='0' && CSpinE { //Key<'0' || Key>'9'表示非数字的按键; //Key=='0'&&CSpinEdit1->Text==""表示在文本框内容为空的时候按了'0' Key=0; MessageBeep(MB_OK); //让电脑发声,提示输 } } } //------------------------------------------------------------------- void __fastcall TForm1::CSpinEdit1Change(TObject *Sender) { //虽然 OnKeyPress 中已经禁止了非字母字符,但是还有可能用回格键 //将其内容清空,这也会造成错误发生,所 //判断是不 if(CSpinEdit1->T { //不能为 CSpinEdit1->Text=IntToStr(1); } else if(CSpinEdit1->Text.ToInt()>100) {//最大为 100 第 5 章 画图程序 124 CSpinEdit1->Text=IntToStr(100); } } //--------------------------------------------------------------------------- n、OnMouseMove 和 OnMouseUp 个鼠标事件中完成的,它们的入口参数有对象参数 Sender、鼠标按键 Button、Shift 以及鼠 鼠标事件是左键或者右键发出的,而 Shift 可以标记 发生时是否有 Alt、Ctrl、Shift 等键被按下,而且在 OnMouseMove 事件中,Shift 可 态。 绘图函数,而且程序代码中的注释做的很 以我们这里就不再详细说明绘图操作的实现。需要说明的是,在状态栏中输出光标 置的代码中,用到了 Format 函数,它的参数如下: g Format, const System::TVarRec* rgs, const int Args_Size); arRec 类型的变量,它里面存放着需 输出的变量,最后一个参数表示 Args 中变量的数目。其实,简单的看,它跟 printf()差不 一个字符串中返回而已。 入文字时,需要弹出窗口供用户输入一个字符串,并且设置字体。该窗体样式如图 5.4.4 对鼠标事件的响应 所有的绘图操作都是在 TImage 组件的 OnMouseDow 三 标位置参数 X、Y。其中 Button 可以标记 鼠标事件 以标记鼠标移动过程中鼠标按键是否处于被按下的状 绘图实现比较简单,因为 Canvas 提供了足够的 详细,所 位 extern PACKAGE AnsiString __fastcall Format(const AnsiStrin A 第一个参数是格式说明字符串,第二个参数指向一个 TV 要 多,只是把输出的内容存放在 在输 5-2 所示。 图 5-2 文字输入框 鼠标事件的响应代码如下: ---------------------------- *Sender, ShiftState Shift, int X, int Y) //填充,左键用前景色填充,右键用背景色填充 //----------------------------------------------- void __fastcall TForm1::ImageMouseDown(TObject TMouseButton Button, T { 第 5 章 画图程序 125 if(ToolState == dtFill) { { //左键,用前景色填充 or=FGShape->Brush->Color; } { Color; } s->Pixels[X][Y],fsSurface); { ZoomOut==false) tmap(); e; Image->Picture->Graphic); e) mage->Picture->Width/ZoomSize; t=Image->Picture->Height/ZoomSize; Canvas->StretchDraw( TRect(0,0,TmpBitmap->Width,TmpBitmap->Height), Image->Picture->Graphic); aphic=TmpBitmap; delete TmpBitmap; } if(Button == mbLeft) Image->Canvas->Brush->Col else Image->Canvas->Brush->Color=BGShape->Brush-> Image->Canvas->FloodFill(X , Y , Image->Canva return; } //放缩,左键放大,右键缩小 if(ToolState == dtZoom) if(Button == mbLeft && { //左键 ,放大 Graphics::TBitmap *TmpBitmap; TmpBitmap = new Graphics::TBi TmpBitmap->Width=Image->Picture->Width*ZoomSize; TmpBitmap->Height=Image->Picture->Height*ZoomSiz TmpBitmap->Canvas->StretchDraw( TRect(0,0,TmpBitmap->Width,TmpBitmap->Height), Image->Picture->Graphic=TmpBitmap; delete TmpBitmap; ZoomOut=true; } else if(Button==mbRight && ZoomOut==tru {//右键 ,缩小 Graphics::TBitmap *TmpBitmap; TmpBitmap = new Graphics::TBitmap(); TmpBitmap->Width=I TmpBitmap->Heigh TmpBitmap-> Image->Picture->Gr ZoomOut=false; 第 5 章 画图程序 126 //放大操作完成以后,把 ToolState 改为 NoEdit SBtnNoEdit->Click(); Edit->Down=true; return; //颜色吸管,左键吸取前景色,右键吸取背景色 if(Button == mbLeft) 前景色 olor=Image->Canvas->Pixels[X][Y]; //修饰 Shape 组件的边框,将边框颜色取为 Brush->Color 的反色 F 0xFF T (FGShape->Brush->Color>>16) )); } else if(Button==mbRight) ]; //修饰边框 0xFF-BYTE(BGShape->Brush->Color), ush->Color>>8), 0xFF-BYTE(BGShape->Brush->Color>>16) SBtnNo } if(ToolState == dtGetColor) { { //左键 ,把鼠标位置的颜色设置为 FGShape->Brush->C GShape->Pen->Color=TColor(RGB( 0xFF-BYTE(FGShape->Brush->Color), 0xFF-BYTE(FGShape->Brush->Color>>8), -BY E {//右键 ,背景色 BGShape->Brush->Color=Image->Canvas->Pixels[X][Y BGShape->Pen->Color=TColor(RGB( 0xFF-BYTE(BGShape->Br )); } //获得颜色以后,将 ToolState 改为 NoEdit SBtnNoEdit->Click(); SBtnNoEdit->Down=true; return; } //-------------- if(Button != mbLeft) return; switch(ToolState) { case dtNoEdit: break; 第 5 章 画图程序 127 case dtSelectRect: EraseSelect(); //清除其它选定区 //设置画笔画刷属性,准备画一虚线矩形框标示选定区域 ->Canvas->Pen->Width=1; Point(X,Y); Pt=Origin; ->Canvas->Pen->Width=1; //铅笔是线宽为 1 的画笔 age->Canvas->Pen->Color=FGShape->Brush->Color; veTo(X,Y); >Brush->Color; //画刷的宽度由 CSpinEdit1 的值决定 Image->Canvas->MoveTo(X,Y); case dtFog: //喷雾效果 for(int i=0;iCanvas->Pixels[X+delX-FogSize][Y+delY-FogSize]= FGShape->Brush->Color; i++; } Image->Canvas->Brush->Style=bsClear; Image->Canvas->Pen->Style=psDot; Image Origin= Move break; case dtErase: //设置颜色为背景色的画笔,清除当前图象 Image->Canvas->Pen->Color=BGShape->Brush->Color; Image->Canvas->Pen->Width=13; //指定橡皮宽度 Image->Canvas->MoveTo(X,Y); break; case dtPencil: Image Im Image->Canvas->Mo break; case dtBrush: Image->Canvas->Pen->Color=FGShape- Image->Canvas->Pen->Width=CSpinEdit1->Value; break; randomize(); //初始化随机数 { //在以(X,Y)为圆心,FogSize int delY=random(FogSize*2); if((delX-FogSize)*(delX-FogSize)+(delY-Fo FogSize*FogSize) { I } 第 5 章 画图程序 128 break; case dtText: //把当前的字体赋给文字输入框的字体对话框 选取的字体 Image->Canvas->Font=FormInputText->FontDialog1->Font; Image->Canvas->TextOutA(X,Y,FormInputText->Edit1->Text); break; 起点 =Point(X,Y); //记录起点位置到 Origin MovePt=Origin; //初始化移动结束位置为开始位置 break; } ------------------------------------------------------------------------- _fastcall TForm1::ImageMouseMove(TObject *Sender, TShiftState Shift, 坐标: (%d, %d)", tempvar, 2); 过程中,鼠标左键是否处于被按下的状态 果左边不在按下状态,不进行绘图操作,直接返回 if(!Shift.Contains(ssLeft)) //根据不同的 ToolState 状态进行相应的绘图操作 case FormInputText->FontDialog1->Font=Image->Canvas->Font; if(FormInputText->ShowModal()==IDOK) { //从字体对话框读取 //在光标位置输出字符串 } case dtLine: case dtRectangle: case dtEllipse: case dtRoundRect: Image->Canvas->MoveTo(X,Y); //移动到 line Origin default: break; } //-- void _ int X, int Y) { //在状态栏中显示鼠标的位置 TVarRec tempvar[2] = {X, Y}; StatusBar1->Panels->Items[1]->Text = Format("当前 //判断鼠标移动 //如 return; switch(ToolState) { dtNoEdit: break; case dtSelectRect: 第 5 章 画图程序 129 DrawShape(Origin, MovePt, pmNotXor); MovePt = Point(X, Y); break; Image->Canvas->LineTo(X,Y); case dtPencil: break; Image->Canvas->LineTo(X,Y); case dtFog: 喷雾效果 randomize(); //初始化随机数 for(int i=0;iPixels[X+delX-FogSize][Y+delY-FogSize]= FGShape->Brush->Color; SpinEdit1->Value; Xor); = Point(X, Y); DrawShape(Origin, MovePt, pmNotXor); case dtErase: break; Image->Canvas->LineTo(X,Y); case dtBrush: break; //在移动鼠标的过程中,在鼠标移动过的地方实现 { //在以(X,Y)为圆心,Fo int delX=random(FogSize*2); if((delX-FogSize)*(delX-FogSize)+ (delY-FogSize)*(delY-FogSize) < FogSize*FogSiz { Image->Canva FGShape->Brush->Color; i++; } } break; case dtLine: case dtRectangle: case dtEllipse: case dtRoundRect: //读取画笔的颜色和风格 Image->Canvas->Pen->Color= Image->Canvas->Pen->Width=C DrawShape(Origin, MovePt, pmNot MovePt DrawShape(Origin, MovePt, pmNotXor); break; 第 5 章 画图程序 130 default: break; } } //--------------------------------------------------------------------------- void __fastcall TForm1::ImageMouseUp(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) : break; rawShape(Origin, P int(X, Y), pmCopy); //恢复画刷画笔属性 HaveSelected=true; se dtErase: olor=FGShape->Brush->Color; >Canvas->Pen->Width=CSpinEdit1->Value; Color=FGShape->Brush->Color; pmCopy); { if(Button != mbLeft) return; switch(ToolState) { case dtNoEdit case dtSelectRect: D o Image->Canvas->Pen->Width=CSpinEdit1->Value; Image->Canvas->Pen->Style=psSolid; MovePt=Point(X,Y); SelectRect=TRect(Origin,MovePt); break; ca Image->Canvas->Pen->Width=CSpinEdit1->Value; //恢复画笔宽度 break; case dtLine: case dtRectangle: case dtEllipse: case dtRoundRect: //读取画笔的颜色和风格 Image->Canvas->Pen->C Image- Image->Canvas->Pen-> DrawShape(Origin, Point(X, Y), break; default: break; } } 第 5 章 画图程序 131 5.5 图象的复制、粘贴和文件操作 5.5.1 图象的复制、粘贴、剪切等 的。 返回一个剪贴板类 (TC b 表 5-9 TClipboard 类的常用方法 方法 说明 与第四章文本处理程序中一样,对图象的复制和粘贴也是依靠 Windows 的剪贴板来实现 C++Builder 中,全局剪贴板的获得可以使用 Clilpboard()函数,它 lip oard)的指针。TClipboard 类的的方法有: Assign Assign(Classes::TPersistent* Source),用来将图象拷贝到剪贴板或者从剪贴 板拷贝出去。如: Clipboard()->Assign(Bitmap1); Bitmap1->Assign(Clipboard()); Clear Clear(void),清除剪贴板的内容 Close Close(void),关闭打开的剪贴板 Open Open(void),打开剪贴板,放置其它应用程序改变它的内容 SetAsHandle SetAsHandle(Word Format, int Value),把指定格式数据的句柄传给剪贴板 GetAsHandle GetAsHandle(Word Format),从剪贴板中返回指定格式数据的句柄 HasFormat HasFormat(Word Format),判断剪贴板是否支持指定的数据格式 SetTextBuf SetTextBuf(char * Buffer),将 Buffer 中的字符内容读如剪贴板 要注意的是,在使用剪贴板之前需要先加入#include 语句。 程序中,对主菜单“编辑”中的“剪切”、“复制”、“粘贴”都用到了剪贴板。“编辑” 菜单的各个子菜单的相应代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::MEditCutClick(TObject *Sender) { //对选定区域剪切,取消选定区域 //没有被选定区域,不做操作 if(!HaveSelected)return; //复制选定区域到剪贴板 MEditCopy->Click(); //清除选定区域的内容 MEditDelSel->Click(); } //--------------------------------------------------------------------------- 第 5 章 画图程序 132 void __fastca m1::M opyClick(TObject *Sender) {//复制 //没有被选定区 作 if(!HaveSelected)return; map *TmpBitmap=new Graphics::TBitmap(); TmpBitmap->Width=SelectRect.Width(); TmpBitmap->Height=SelectRect.Height(); TmpBitmap->Height),Image->Canvas,SelectRect); p 拷贝到剪贴板 Clipboard()->Assign(TmpBitmap); de } //----- ----------------------- ------- void __fastc k(TObject *Sender) {//粘贴 Graphics: ; if (Clipboard()->HasFormat(CF_BITMAP)){ Bitmap = new Graphics::TBitmap(); try{ Bitmap->Assign(Clipboard()); Image->Canvas->Draw(0, 0, Bitmap); delete Bitmap; } catch(...){ delete Bitmap; } } --------------------------------------------------------------------------- oid __fastcall TForm1::MEditSelAllClick(TObject *Sender) //全选 //清除之前的选定区域 EraseSelect(); ll TFor EditC 选定区域到剪贴板,不更改选定区域 域,不做操 Graphics::TBit //将选定部分图象拷贝到 TmpBitmap 中 TmpBitmap->Canvas->CopyRect(TRect(0,0,TmpBitmap->Width, //将 TmpBitma lete TmpBitmap; ------------------------------------- --- all TForm1::MEditPasteClic :TBitmap *Bitmap } // v { 第 5 章 画图程序 133 //绘制全选的选择虚线 Origin = Point(0,0); icture->Height,Image->Picture->Width); DrawShape(Origin, MovePt, pmNotXor); //恢复画笔属性 Image->Canvas->Pen->Width=CSpinEdit1->Value; Image->Canvas->Pen->Style=psSolid; //更新选定范围和被选定属性 SelectRect=TRect(Origin,MovePt); HaveSelected=true; } //--------------------------------------------------------------------------- void __fastcall TForm1::MEditDelSelClick(TObject *Sender) { //清除选定区域,也即将选定区域置为底色 if(!HaveSelected)return;//没有选定区域, EraseSelect();//清除虚线 //清除选定区域内的图象,置为底色 Image->Canvas->Brush->Style=bsSolid; Image->Canvas->Brush->Color=BGShape->Brush->Color; Image->Canvas->Pen->Style=psClear; //因为 Rectangle 对右边线和下边线不画,所以要对它们+1 Image->Canvas->Rectangle(SelectRect.left,SelectRect.top, SelectRect.right+1,SelectRect.bottom+1); //恢复画笔画刷的属性 Image->Canvas->Brush->Style=bsClear; Image->Canvas->Brush->Color=FGShape->Brush->Color; Image->Canvas->Pen->Style=psSolid; } //--------------------------------------------------------------------------- 5.5.2 文件操作 文件的新建、打开、保存、打印等操作前面讲述过,请参阅第四章 4.5.1 和本章 5.2.2 的 内容。其中,新建操作需要弹出一个窗口,用来输入新建图象的大小,窗体的内容样式如下: MovePt = Point(Image->P 返回 第 5 章 画图程序 134 图 5-3 新建图片大小对话框 用 FormNew->ShowModal()来显示它,根据返回的 ModalValue 判断是否按了“OK”按钮, 如果是,则根据 Edit 组件中的数据合不合法决定是否新建指定大小的图片。 实现代码如下: //---- void __fastcall TForm1::MFileNewClick(TObject *Sender) s::TBitmap *Bitmap; eight); 组件上 FormNew->ActiveControl if (FormNew->ShowModal() != IDCANCEL) 户点击了 OK 按钮 f(For || F || F || F Me 的数据不合法!", mtConfirmation, TM } else Bit Bit Bit = StrToInt(FormNew->HeightEdit->Text); Ima ----------------------------------------------------------------------- { Graphic //在大小设置窗体中显示当前图片的大小 FormNew->WidthEdit->Text = IntToStr(Image->Picture->Graphic->Width); New->HeightEdit->Text = IntToStr(Image->Picture->Graphic->H Form //设定窗口的输入焦点在 WidthEdit = FormNew->WidthEdit; {//用 i mNew->WidthEdit->Text == "" ormNew->WidthEdit->Text.ToInt()<1 ormNew->HeightEdit->Text == "" ormNew->HeightEdit->Text.ToInt()<1) { ssageDlg("你输入 sgDlgButtons() << mbOK,0); { map = new Graphics::TBitmap(); map->Width = StrToInt(FormNew->WidthEdit->Text); map->Height ge->Picture->Graphic = Bitmap; 第 5 章 画图程序 135 Ima //新 过,这样关闭窗口的时候就不会提示保存 Cu } //-- --- k(TObject *Sender) if (OpenPictureDialog1->Execute()) } ---------- void __fastcall TForm1::MFileSaveClick(TObject *Sender) } else { AsC } } //-- ----- void __fastcall TFo { if (SavePictureD { le = veCli } } ge->Picture->Bitmap->Modified=false; 建图片,设置它没有被更改 rrentFile = EmptyStr; //空字符串 } } ------------- --------------------------------------------------------- void __fastcall TForm1::MFileOpenClic { { CurrentFile = OpenPictureDialog1->FileName; Image->Picture->LoadFromFile(CurrentFile); } //----------------------------------------------------------------- { if (CurrentFile != EmptyStr) { Image->Picture->SaveToFile(CurrentFile); MFileSave lick(Sender); --------------- ----------------------------------------------------- rm1::MFileSaveAsClick(TObject *Sender) ialog1->Execute()) CurrentFi SavePictureDialog1->FileName; MFileSa ck(Sender); 第 5 章 画图程序 136 //- --- void __fastcall TFo ender) { ial } //-- ----- vo Fo { B idth, apIm it g w 的 Bitmap->Assi re); izes( fo e ize]; ma = t = B //拉伸,以适应打印页面的大小 Handle, 0, 0, DIBWidth, D Height, 0, 0, DIBWidth, DIBHeight, d } //--------------------------------------------------------------------------- ------------------ ----------------------------------------------------- rm1::MFilePrintSetClick(TObject *S PrinterSetupD og1->Execute(); --------------- ----------------------------------------------------- id __fastcall T rm1::MFilePrintClick(TObject *Sender) unsigned int long DIBW itmapInfoSize, BitmapImageSize; DIBHeight; PChar Bitm age; Windows::PBit Graphics::TB mapInfo BitmapInfo; map *Bitmap; Printer()->Be inDoc(); Bitmap = ne Graphics::TBitmap(); //将 Image 图片拷贝到 Bitmap 里面 gn(Image->Pictu GetDIBS Bitmap->Handle, BitmapInfoSize, BitmapImageSize); BitmapIn = (PBitmapInfo) new char[BitmapInfoSize]; BitmapImag = (PChar) new char [BitmapImageS GetDIB(Bit p->Handle, 0, BitmapInfo, BitmapImage); DIBWidth BitmapInfo->bmiHeader.biWidth; DIBHeigh itmapInfo->bmiHeader.biHeight; StretchDIBits(Printer()->Canvas-> IB BitmapImage, BitmapInfo, DIB_RGB_COLORS, SRCCOPY); delete [] BitmapImage; delete [] BitmapInfo; elete Bitmap; Printer()->EndDoc(); 第 5 章 画图程序 137 void __fastcall TForm1::M { FileExitClick(TObject *Sender) Close(); 比例,由于只需要输入两个值(宽度缩放比例和高度缩放比例),所以可以利用 FormNew 窗体来实现,只需要对窗体中 TLabel 的 Caption 做更改即可。程序运行时弹出的缩 放对话框如图 5-4。 } //--------------------------------------------------------------------------- 5.6 图象缩放、反色及其它 5.6.1 图象的缩放与反色 图象的缩放与反色由“图象”菜单的子菜单触发。缩放操作需要先弹出对话窗供用户输 入缩放 图 5-4 缩放比例对话框 缩放与反色的实现代码如下 void 输入焦点在 WidthEdit 组件上 : //--------------------------------------------------------------------------- __fastcall TForm1::MImageStrawClick(TObject *Sender) { Graphics::TBitmap *Bitmap; //初始化大小设置窗体 FormNew->Caption="输入缩放比例(1-500%)"; FormNew->Label1->Caption="水平(%):"; FormNew->Label2->Caption="竖直(%):"; FormNew->WidthEdit->Text = IntToStr(100); FormNew->HeightEdit->Text = IntToStr(100); //设定窗口的 第 5 章 画图程序 138 FormNew->ActiveControl = if (FormNew->ShowModal( FormNew->WidthEdit; ) != IDCANCEL) {//用户点击了 OK 按钮 Draw(TRect(0,0,Bitmap->Width,Bitmap->Height), Image->Picture->Graphic); ap; } 图片的大小"; 1->Caption="高度:"; 2->Caption="宽度:"; ----------------- nverseClick(TObject *Sender) idth;i++) for(int B( age->Canvas->Pixels[i][j]), if(FormNew->WidthEdit->Text == "" || FormNew->WidthEdit->Text.ToInt()<1 || FormNew->WidthEdit->Text.ToInt()>500 || FormNew->HeightEdit->Text == "" || FormNew->HeightEdit->Text.ToInt()<1 || FormNew->HeightEdit->Text.ToInt()>500) { MessageDlg("请输入 1-500 的整数", mtConfirmation, TMsgDlgButtons() << mbOK,0); } else { Bitmap = new Graphics::TBitmap(); Bitmap->Width = StrToInt(Image->Picture->Width)* FormNew->WidthEdit->Text.ToInt()/100.0; Bitmap->Height = StrToInt(Image->Picture->Height)* FormNew->HeightEdit->Text.ToInt()/100.0; Bitmap->Canvas->Stretch Image->Picture->Graphic = Bitm } //撤销对窗体的修改 Form ->Caption="设置新建New FormNew->Label FormNew->Label } //--------------------------- ------------------------------- void __fastcall TForm1::MImageI { for(int i=0;iW { j=0;jHeight;j++) { Image->Canvas->Pixels[i][j]=TColor(RG 0xFF-BYTE(Im 第 5 章 画图程序 139 } -------------------- 件 CColorGrid 的颜色如果不能满足需要,可以通过“颜色”菜单来自定 ,响应代码如下: tcall TForm1::MColorFGClick(TObject *Sender) Dialog1->Execute()) ColorDialog1->Color; olor=TColor(RGB( F-BYTE(FGShape->Brush->Color), F-BYTE(FGShape->Brush->Color>>16) --------------------------------------------------------------- tcall TForm1::MColorBGClick(TObject *Sender) alog1->Color; -BYTE(BGShape->Brush->Color), TE(BGShape->Brush->Color>>8), TE(BGShape->Brush->Color>>16) 0xFF-BYTE(Image->Canvas->Pixels[i][j]>>8), 0xFF-BYTE(Image->Canvas->Pixels[i][j]>>16) )); } } //------------------------------------------------------- 5.6.2 自定义前景色与背景色 下方颜色选择组 义颜色。菜单中有两个子菜单,分别供设置前景色和背景色 //----------------------------------- void __fas { if(Color { FGShape->Brush->Color= FGShape->Pen->C 0xF 0xFF-BYTE(FGShape->Brush->Color>>8), 0xF )); } } //------------ void __fas { if(ColorDialog1->Execute()) { BGShape->Brush->Color=ColorDi BGShape->Pen->Color=TColor(RGB( 0xFF 0xFF-BY 0xFF-BY )); } 第 5 章 画图程序 140 } //--------------------------------------------------------------------------- ”菜单的响应 工具栏、颜色栏和状态栏。实现代码如下: ------------------ Sender) Tool->Visible; ---------- ender) =!PanelColor->Visible; ----- 序的初始化 始化设置如下: -------------------- orm1::TForm1(TComponent* Owner) rm(Owner) 运行,自动为 Image 创建一个 bitmap ap(); cture->Graphic = Bitmap; Picture->Bitmap->Modified=false; 没有被更改过,这样关闭窗口的时候就不会提示保存 5.6.3 “查看 通过“查看”菜单可以关闭和显示 //--------------------- void __fastcall TForm1::MViewToolClick(TObject * { CoolBarTool->Visible=!CoolBar } //----------------------------------------------------------------- void __fastcall TForm1::MViewColorClick(TObject *S { PanelColor->Visible } //---------------------------------------------------------------------- void __fastcall TForm1::MViewStateClick(TObject *Sender) { StatusBar1->Visible=!StatusBar1->Visible; } 5.6.4 程 程序开始运行时的一些初 //------------------------------------------------------- __fastcall TF : TFo { //程序开始 Graphics::TBitmap *Bitmap; Bitmap = new Graphics::TBitm Bitmap->Width = StrToInt(200); Bitmap->Height = StrToInt(200); Image->Pi Image-> //新建图片,设置它 第 5 章 画图程序 141 CurrentFile = EmptyStr; //空字符串 Fo z 雾化范围的大小 Zo 被选择区域 Screen->Cursors[crErase]=LoadCursorFromFile("image/Erase.cur"); een->Cursors[crGetColor]=LoadCursorFromFile("image/GetColor.cur"); mage/Zoom.cur"); n->Cursors[crPencil]=LoadCursorFromFile("image/Pencil.cur"); ile("image/Brush.cur"); =LoadCursorFromFile("image/Fog.cur"); >DoubleBuffered=true; ---------------------------------------------------------------------- uffered=true 是为了解决画面的闪烁问题。在画图 办法就是用双缓冲的技术,现在内存中把图片画好, 的 API 函数显示画好的图片,但是最简单的办法就是设置组件的 leBuffered 属性为 true。因为 TImage 组件没有 DoubleBuffered 属性,所以把转而设置它 容器的属性。 5.6.5 闭 误的操作导致数据丢失。与第四 中的方法一样,可以通过对主窗体的 OnCloseQuery 事件的响应来实现,代码如下: ----- ject *Sender, bool &CanClose) odified) //文档改动过 修改?", mtConfirmation, Buttons() << mbYes << mbNo << mbAbort,0); er); ap->Modified) 置保存文件名时取消保存操作,则不关闭子窗体 lse; gSi e=15;//初始化 omSize=3.0;//初始化放大倍数 ZoomOut=false; //初始化放大状态 HaveSelected=false; //没有 //读取光标 Screen->Cursors[crFill]=LoadCursorFromFile("image/Fill.cur"); Scr Screen->Cursors[crZoom]=LoadCursorFromFile("i Scree Screen->Cursors[crBrush]=LoadCursorFromF Screen->Cursors[crFog] ScrollBox1- } //-------- 其中,最后一句 ScrollBox1->DoubleB 的过程中,图片总是不停的闪烁,解决的 然后调用 Windows Doub 的 程序的关 在退出程序时,要判断图象是否需要保存图象,以免错 章 //------------------------------------------------------------------------- void __fastcall TForm1::FormCloseQuery(TOb { if(Image->Picture->Bitmap->M { int Choose=MessageDlg("是否保存对图像的 TMsgDlg if(Choose == mrYes) //按 Yes 按钮 { //调用保存函数 MFileSaveClick(Send if(Image->Picture->Bitm { //如果在设 CanClose=fa 第 5 章 画图程序 142 } } { //如果选择 abort,则取消关闭窗口操作 不保存 ------------------------------------------------------------------ 题 在设置窗体在屏幕上的属性(如位置等)时需要用到? 绘制的撤销刚刚做的绘图操作? 可以改善? 的不好,这在喷雾效果且按下并拖动鼠标时表现 else if(Choose == mrAbort) CanClose=false; } //如果选择 No,窗口关闭,文档 } } //------------ 5.7 思考 z TScreen 类中有哪些属性 z 如果知道绘制图形操作的参数,如何将 z MyPaint 程序中的反色处理费时较长,有什么办法 z MyPaint 程序中喷雾效果的随机数取 明显,如何解决? 《C++ Builder 6 编程实例精解 赵明现》 第 06 章 学生成绩管理&资源管理器 本章重点 本章通过学生成绩管理程序,介绍 C/C++/C++Builder 中文件的操作和使用;介绍了 Win31 组件,并利用 Win32 类的组件编写资源管理器。 : 中关于文件的操作 了解关于磁盘文件的 31 组件 TTreeView 组件的使用 复习工具栏的使用 选项卡中关于磁盘文件的 学习目的 通过本章的学习,您可以 ■ 掌握 C/C++/C++Builder ■ 熟悉链表的使用 ■ Win ■ 掌握 TListView 组件的使用 ■ 掌握 ■ 第 6 章 学生成绩管理&资源管理器 144 本章典型效果图 学生成绩管理效果图 第 6 章 学生成绩管理&资源管理器 145 第 6 章 学生成绩管理&资源管理器 146 6.1 文件操作函数 文件是一组保存在存储器中的一组数据集合,用于数据的存储,其重要性不言而喻。任 都必须提供一组完善的文件操作函数。C++Builder 对 C 和 功能更加多,使用也 。但是,要注意的是,在对文件操作的时候前后所用的文件函数必须保持一致,比 的 fopen 函数,关闭文件就要用 fclose 而不能用 C++Builder 中提供的 分别介绍 C,C++以及 C++Builder 中各自的文件操作方式,并对常用的函数做简单 文件“stdio.h”中,文件类型的变量在程序中用“FILE *fp;” 指针,它指向一个 FILE 类型的结构(struct)变量,其中存放着 件名称、文件状态以及当前位置等参数。 关的函数如下: 表 6-1 C 语言中常用文件相关函数 说明 何一种编程语言或者编程环境, C++中的文件操作方式都支持,而且还提供自己的一套文件操作函数, 更加方便 如,打开文件用的 C 中 FileClose 函数。 下面 列举和说明。 6.1.1 C 中文件的操作 C语言中,文件类型包含在头 明。fp 是一个文件的方式声 被打开文件的信息,包括文 C中常用的文件相 函数 fopen fopen(const char *filename, const char *mode),文件打开函数返回值为 FILE 类 LL。第一个参数为文件名,第二个参数 “r”、“w”、“a”等 型指针,如果文件打开失败,返回 NU 是打开文件的方式,可以是 fclose fclose(FILE *stream),关闭一个文件。如果关闭成功,返回 0,否则返回 EOF fprintf fprintf(FILE *stream, const char *format[, argument, ...]),格式化输出数据,仅适 用于文本文件 fscanf fscanf(FILE *stream, const char *format[, address, ...]),从文件中以指定格式读取 ,仅使用于文本文件 数据 feof feof(FILE *stream),文件结尾测试函数,如果遇到文件结束指示则返回 true ferror ferror(FILE *stream),文件读写错误判断函数,如果遇到错误返回 true fread fread(void *ptr, size_t size, size_t n, FILE *stream),从文件中读取 n 个 size(单位 r 指针指示的内存位置,用于二进制文件。如果读取是 byte)大小的数据到 pt 成功返回 n fwrite fwrite(const void *ptr, size_t size, size_t n, FILE *stream) ,向文件中写入 n 个 size 小的数据,数据的起始位置由 ptr 指针指示,用于二进制文(单位是 byte)大 件。如果写入成功返回 n fseek fseek(FILE *stream, long offset, int whence),将文件移动到指定位置 第 6 章 学生成绩管理&资源管理器 147 filelength filelength(int handle),返回文件的大小,单位为 byte fgetc fgetc(FILE *stream),从文件中读取一个字符 fputc fputc(int c, FILE *stream),向文件中输出一个字符 rewind rewind(FILE *stream),将文件指针重新设置在文件开头 remove remove(const char *filename),删除文件。删除成功返回 0,失败返回-1 6.1.2 C++中文件的操作 C++是一种面向对象的语言,其中对文件的操作也封装在流式系统类中,不但包含文件 输入输出设备进行读写的类,C++中流式文件类主要由 用 cin、cout 实现对文件的操作。其使用方法与在屏幕上的输 dos 模式程序的应该都已经很熟悉了。 中文件的操作 件操作方式,但是,也提供 用 C++Builder 提供的函数 定义,这些函数及其说明如下: 目录相关的函数 说明 的操作类,还包括了对其它一些 fstream、ifstream 和 ofstream, 出用法大致相同,常编 6.1.3 C++Builder C++Bui 它依然支持 C/C++中的文lder 是 C++基础上的扩展, 了自己的一套文件操作函数。相比之下,这些函数功能更多,使得编程更加方便简易,所以, 使用习惯的 C/C++方式管理函数固然好, 也是不错的选择。 C++Builder 中文件和目录相关的函数在 SysUtils.hpp 中 表 6-2 C++Builder 中常用文件、 函数 FileOpen FileOpen(const AnsiString FileName, unsigned Mode),打开一个文件,打开模 nReadWrite 等 式有 fmOpenRead、fmOpenWrite、fmOpe DeleteFile DeleteFile(const AnsiString FileName),删除一个文件 FileCreate FileCreate(const AnsiString FileName),创建一个文件 FileRead FileRead(int Handle, void *Buffer, unsigned Count),从句柄为 Handle 的文件 指定的内存 中读取 Count 个字节的数据到 Buffer FileWrite FileWrite(int Handle, const void *Buffer, unsigned Count),向句柄为 Handle 的 Count 字节的数据,这些数据在 Buffer 指向的内存位置 文件中写入 FileSeek FileSeek(int Handle, int Offset, int Origin),文件定位 FileClose FileClose(int Handle),关闭文件 FileAge FileAge(const AnsiString FileName),返回文件的修改日期和时间 FileExists FileExists(const AnsiString FileName),检查文件是否存在,存在返回 true FileGetDate FileGetDate(int Handle),获取文件的 Dos 时间日期 FileSetDate FileSetDate(int Age),设定文件 Handle, int Age),FileSetDate(const AnsiString FileName, int 的 Dos 时间日期 第 6 章 学生成绩管理&资源管理器 148 FileGetAttr FileGetAttr(const AnsiString FileName),获取文件的属性 FileSetAttr FileSetAttr(const AnsiString FileName, int Attr),设定文件的属性 FindFirst FindFirst(const AnsiString Path, int Attr, TSearchRec &F),查找文件或目录 FindNext FindNext(TSearchRec &F),用在 FindFirst 之后查找下一个文件或目录 FindClose FindClose(TSearchRec &F),结束文件查找 RenameFile RenameFile(const AnsiString OldName, const AnsiString NewName),对文件重 命名 ChangeFileExt ChangeFileExt(const AnsiString FileName, const AnsiString Extension),改变文 件的扩展名 ExtractFile tring Path ExtractFilePath(const AnsiS ExtractFileName(co FileName),返回文件的全路径 ExtractFileName nst ExtractFileExt(const An AnsiString FileName),返回文件名(不含路径) ExtractFileExt si Drive ExtractFileDrive(const Ansi String FileName),返回文件的扩展名 ExtractFile String FileName),返回文件所在的驱动器 ExtractFileDir ExtractFileDir(const AnsiString FileName),返回文件所在的目录 FileSearch FileSearch(const AnsiString Name, const AnsiString DirList),在 DirList 目录中 寻找指定文件 DiskFree DiskFree(Byte Drive),返回驱动器的剩余空间大小 DiskSize DiskSize(Byte Drive),返回驱动器的容量大小 GetCurrentDir GetCurrentDir(),返回当前路径 SetCurrentDir SetCurrentDir(const AnsiString Dir),设置当前路径 CreateDir CreateDir(const AnsiString Dir),新建目录 RemoveDir RemoveDir(const AnsiString Dir),删除目录 6.2 学生成绩管理程序 节中讲到的文件管理函数,设计一个学生成绩管理程序,实现对学生信息的可 包括添加、浏览、修改、删除等功能。学生信息包括其学号、姓名、性别、以及 文件中读取保存的学生信息,程序关闭时自动将内存中的学生信 6 位以内的整数)作为其唯一标志,与姓名、性别一起作为 成绩有两种状态,即没有登记成绩和已经登记成绩,如果已经登 中有左上方两个 Edit 组件用于学号和姓名的输入和显示, 示;两个单选按钮用于选择和显示性别;两个 BitBtn 视化管理, 利用 6.1 成绩。每次运程序时自动从 息保存到文件中。 学生信息 在学生信息中,以其学号( 不可缺少的属性。 记,要记录其成绩。 6.2.1 界面设计 如图 6-1 所示为程序界面。其 右边三个 Edit 组件用于成绩的输入和显 第 6 章 学生成绩管理&资源管理器 149 按钮用于程序和用户之间的交互;按钮上方的 Label 用于显示帮助信息;最下方是一个 TabSet 件,用于选择不同的管理模式。其中性别的两个单选按钮以及成绩的三个 Edit 组件分别放 、性别的 GroupBoxSex、成绩的 Grou false,即它们不可用。设定 TabSet 组件 TabSet1 的 TabIndex 属性 。在本程序 中,GroupBox 不但能清晰明了的对学生信息进行分类,而且对界面的美化也都很有好处。另 组 置在两个 GroupBox 上。设定姓名的 Edit 组件 EditName pBoxScore 的 Enabled 属性为 为 1,即默认在“查找”状态;设定 TabSet1 的 Align 属性为 alBottom,即该组件显示在 窗体下部。为两个 BitBtn 设定其图片美化界面。 GroupBox 是用来分组的组件,可以将具有相同类别一些组件放置在它上面 外, 图 6-1 成绩管理程序界面设计 在设置组件不可用的时候,可以直接把 GroupBox 的 Enanled 属性设为 false,省去了对每个 组件设置的麻烦。 RadioButton 组件用于单项选择,它们之间是互斥的,也就是当其中一个按钮被选定时其 它按钮会自动置为未选定状态。 BitBtn 是一种按钮组件,同 Button 组件基本相同,只是它支持在按钮上显示位图。对其 位图的设置通过更改其 Glyph 属性实现。 TabSet 组件是一种分页组件。但是实际上,虽然看起来好像是多个页面,实际上只有一 个页面,只是可以根据 TabSet 的状态改变,设置界面的不同显示状态而已。是一种假象的分 页。C++Build 的代码编辑器用的也是这种技术,用来显示源码文件和头文件。可以对其背 景色(BackgroundColor)、选择 项颜色(UnselectColor)进 置美化界面。设置其 Index 属性(0~分页数目 要编辑 er 项颜色(SelectedColor)、未选择 行设 -1)可以设置显示页面的默认值。 增添或删除页面可以点击属性检查器中 Tabs 属性后的 按钮,然后在弹出的“String List Editor”中更改。 6.2.2 程序逻辑结构 程序运行中,用户对管理模式的选择通过 TabSet 组件的 OnClick 事件来响应,并且其它 第 6 章 学生成绩管理&资源管理器 150 组件的事件处理过程可以通过对 TabSet 的状态(TabIndex 属性)来判断当前的管理模式,根 管 找到,则显示出学生的姓名成绩等详细信息。 如果切换到“添加”模式,那么将所有学生信息的输入组件都设为可用,以供用户添加 信息。并且,程序此时不需要对 EditNum 中的内容进行适时的响应。 若切换到修改模式,现把所 用,只允许用户输入学 号,然后让程序根据学号自动寻找学生信息,如果找到对应的信息,则设 BitBtn 组件可用, ancel”来决定是不是对当前显示的学生信息做修改。如果用户选 择了“OK”,则将所有编辑组件设为可用,供用户对显示的学生信息进行更改。在编辑状态 , “OK”之后执行对当 生信息的删除。 对于 钮,如果按下,程序返回当前模式的初始状态。 int Sex; //0:male ; 1: fema float Math,Chinese,English; t *Pre; t *N } 然后声明全局 双向链表)的第一个元素 的2018香港马会开奖现场。并且声明 如果当前没有学生 z 信息的添 声 数 RePlace 在于区别是“添加” 状态 改” 响应修改的操 作过 调用 调。 据 理模式处理的不同对用户操作进行不同的响应。 因为 TabSet1 的 TabIndex 属性设为 1,所以程序运行开始时默认在“查找”状态,此状 态下除了 TabSet1,只有 EditNum 组件可用,程序根据 EditNum 内容的变化,适时的查找学 生信息,如果 有编辑组件(除 EditNum)设为不可 以供用户点击“OK”或“C 下 如果再按下“OK”,则将当前编辑组件的信息写入内存。 对于“删除”模式,跟“修改”模式一样,只允许用户对 EditNum 进行更改,并适时根 据学号显示学生信息,在按下 前显示的学 “Cancel”按 文件的操作放在程序的开始和结束,即在程序开始时自动读取以前保存的数据,在程序 结束时,自动保存内存中的数据。 6.2.3 学生信息的数据结构 在头文件中定义学生信息的结构体如下: //-----------学生信息结构体------------ struct student { int ID; //六位以内数字 char Name[9]; //四个汉字,或者 8 个字母 l struct studen struct studen ext; ; 变量 struct student *First;作为指向学生信息链表( 全局变量 struct student *TmpStu;指向当前窗体中显示的学生信息的2018香港马会开奖现场, 信息被显示,此值为空。 加 在头文件中 明 bool __fastcall Add(bool RePlace);其中参 还是“修 状态。如果是修改状态,则不判断是不是有相同的学号,在 程中,先 Add 函数添加相同学号的学生信息,然后调用 Del 删除函数把旧信息删除 第 6 章 学生成绩管理&资源管理器 151 Add 函数的代 //--- ----- bool __fastcall TFo { //姓名和学号 可以不填, 为 um- icatio "错误 return fals EditN { o 错误输入 fals Btn Application->Mes 误 return fals else struct stud t student *)malloc(sizeof(struct student)); 应 //则不添加 t stud if(First & 修改 do if(Ed 码如下: -------------- ----------------------------------------------------- rm1::Add(bool RePlace) 必须填写,成绩 //并赋成绩 -1 以标记没有登记成绩 if(EditN >Text=="") { Appl n->MessageBoxA("请填写学生的学号!", 输入",0); e; } else if( ame->Text=="") Applicati n->MessageBoxA("请填写学生的姓名!", " return ",0); e; } else if(!R Male->Checked && !RBtnFemale->Checked) { sageBoxA("请选择学生的性别!", "错 输入",0); e; } { ent *temp=(struc //寻找对 id 的姓名,如果链表中存在 id, 用户 struc ent *temp2=First; & !RePlace)//链表不为空,并且是新增信息,不是 { { itNum->Text.ToInt()==temp2->ID) { //存在此 ID Application->MessageBoxA("此学号已存在,请更改!", "错误输入",0); return false; } 第 6 章 学生成绩管理&资源管理器 152 else { temp2=temp2->Next; } while(temp2!=First); //遍历环形链表 现在可 temp->ID if(RBtnMa mp->Sex=0; //男 mp tem if(EditCh se tem EditE else tem English->Text.ToDouble(); m if(!First) First= First- First- { temp->Pre =First->Pre ; p->Next=First; temp- mp- } return true } //--------------------- 删 定 是需要删除的学 生信息的指针。Del 的实现代码如下: } } // 以添加信息了 =EditNum->Text.ToInt(); StrPCopy(te //记录性 mp->Name,EditName->Text); 别 le->Checked)te else te //记录成 ->Sex=1; //女 绩 if(EditMa else th->Text=="")temp->Math=-1;//不登记成绩 p->Math=EditMath->Text.ToDouble(); ->Texinese t=="")temp->Chinese=-1; p->Chinese=EditChinese->Text.ToDouble(); el if( nglish->Text=="")temp->English=-1; p->English=Edit //把 te p 加入链表中 //没有记录 { temp; >Next=First; >Pre=First; //环形链表 } else tem >Pre->Next=temp; te >Next->Pre=temp; } ; ------------------------------------------------------ z 信息的 除 在头文件中 义 void __fastcall Del(struct student *DelStu);其参数 DelStu 第 6 章 学生成绩管理&资源管理器 153 / --- void __fastcall TF { if(DelStu && F { ==Del { irst-> First=N L; rst=F u->Pre-> Next->Pre=DelS e; elStu); } } --- 信息的查找,也就是 OnChange 事件的响应代码如下: // -- void __fastcall TF {/ 的 / 编辑 if(EditName->E //TmpStu 是全部 //所 , 都要先清空它 Tm RB RBtnFemale->Checked=false; EditMath->Text=""; ->Text=""; >T if(!First)return; 息,则直接返回 /------------------ ---------- orm1::Del(struct student *DelStu) irst) //链表中存在学生信息,而且要删除的信息不为空 if(First Stu) //删除第一条信息 if(F Next==First) //只有一条信息 UL else Fi irst->Next; } DelSt DelS Next=DelStu->Next; tu-> free(D tu->Pr //------------------- --------- z 信息的查找 因为需要对 EditNum 组件的 OnChange 事件进行动态响应,并在查找到对应学号的信息 时显示在窗体中。对 ------------------- ------------------------------------------------------ orm1::EditNumChange(TObject *Sender) /寻找对应 id 姓名,如果链表中存在 id, /如果处在 状态,即新添信息,或者修改信息,那么不处理此事件 nabled)return; 变量,它要对应当前在窗体中显示的信息 以 每次的 EditNum 改变, pStu=NULL; //清空当前窗体内的显示信息,在查找到信息之后重写 EditName->Text=""; tnMale->Checked=false; EditChinese EditEnglish- ext=""; //还没有任何的学生信 第 6 章 学生成绩管理&资源管理器 154 、 除(查找要删除的信息) ude 对应 变量 TmpStu 来指向它 Num { { if(Edi 找到 m di if(T lse if(T else lse if(Tm xt=""; else EditEn lish->Text=TmpStu->English; 找和删除模式下,则把 OK/Cancel 按钮置为可用 if(TabSet1->TabIndex==2 || TabSet1->TabIndex==3) BitBtn2->Enabled=true; 息后跳出 while 循环 } z 信息的读取 在对文件的操作中,我们使用 C++Builder 提供的文件相关的函数。因为要在程序开始的 时候自动读取记录文件,所以把对文件的读取操作放在 TForm1::TForm1(TComponent* Owner): TForm(Owner)中,并且设定记录文件的名字为“StuScore.rcd”,代码如下: //在查找 更改(查找要更改的信息)、删 //都要根据学号的变化适时显示信息,这个功能由这段代码实现 struct st nt *NowTmp=First; //查找 学号的信息,并用全局 if(Edit ->Text.Length()) //学号输入框不为空 do tNum->Text.ToInt()==NowTmp->ID) {// 信息,显示在窗体中 T E pStu=NowTmp; tName->Text=TmpStu->Name; e mpStu->Sex)RBtnFemale->Checked=true; RBtnMale->Checked=true; mpStu->Math==-1)EditMath->Text=""; //未登记成绩 EditMath->Text=TmpStu->Math; if(Tm e pStu->Chinese==-1)EditChinese->Text=""; EditChinese->Text=TmpStu->Chinese; pStu->English==-1)EditEnglish->Te g //如果是在查 { BitBtn1->Enabled=true; } break; //找到信 else { NowTmp=NowTmp->Next; //下一个信息 } }while(NowTmp!=First); //遍历环形链表 } } //--------------------------------------------------------------------------- 第 6 章 学生成绩管理&资源管理器 155 //--------------------------------------------------------------------------- __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { int File; struct student *Tmp=First; struct student *Tmp2; //------设置全局的中间变量----- //TmpStu=(struct student *)malloc(sizeof(struct student)); //----------------------------- //尝试从文件读取记录信息 First=NULL; if(!FileExists("StuScore.rcd")) { Application->MessageBoxA("没找到成绩记录文件!","失败",0); return; } else { { break; //文件读完了 } { First->Pre=First; File=FileOpen("StuScore.rcd",fmOpenRead); if(File==-1) { Application->MessageBoxA("打开记录文件失败!","失败",0); return; } while(true) //文件没有到结尾 { int hehe; Tmp2=(struct student *)malloc(sizeof(struct student)); hehe=FileRead(File,Tmp2,sizeof(struct student)); if(hehe < sizeof(struct student)) free(Tmp2); if(!First) //First==NULL First=Tmp2; First->Next=First; 第 6 章 学生成绩管理&资源管理器 156 Tmp=First; 序退出时要自动保存学生信息到文件,所以把文件保存操作放在 OnCloseQuery 事件 响 ; //自动保存 } //---- - ----------------------------------- 志是自动保存还是用户提交的保存请 ,只是为了适应将来程序的可 能功能扩展 息(如保存失败)。Save if(!Firs ); retur } i 录文件 } else //First!=NULL { Tmp->Next=Tmp2; Tmp->Next->Pre=Tmp; Tmp=Tmp->Next; Tmp->Next=First; First->Pre=Tmp; } } FileClose(File); } } //--------------------------------------------------------------------------- z 信息的保存 在程 的 应中: //--------------------------------------------------------------------------- void __fastcall TForm1::FormCloseQuery(TObject *Sender, bool &CanClose) { Save(false) ----- ------------------------------ Save 函数的入口参数是一个 bool 型变量,用来标 求,当然,在这个程序中并没有提供用户提交保存请求的接口 而设置。是否自动保存的不同在于是否弹出某些提示信 函数的代码如下: //------------------------------- void __fastcall TForm1::Save(bool Auto) { int File; struct student *Tmp=First; t) //没有用户信息 { if(!Auto)Application->MessageBoxA("没有学生信息!","保存失败",0 n; f(FileExists("StuScore.rcd")) //存在记 第 6 章 学生成绩管理&资源管理器 157 { if(!DeleteFile("StuScore.rcd")) //删除失败 { Application->MessageBoxA("删除旧纪录文件失败,不能保存","失败",0); if(File==-1) //创建失败 if(FileWrite(File,Tmp,sizeof(struct student))==-1) 错误了!","提示",0); itBtn1、BitBtn2 及各 Edit 的响应 TabSet1 状态的改变需要根据其 TabIndex 的值,也就是选择的管理模式来对界面进行 ------------------ Object *Sender) ch(TabSet1->TabIndex) {//设置组件可用 GroupBoxScore->Enabled=true; } } File=FileCreate("StuScore.rcd"); { Application->MessageBoxA("创建文件 StuScore.rcd 失败,不能保存","失败",0); } //写文件 do { Application->MessageBoxA("一个 Tmp=Tmp->Next; }while(Tmp!=First); FileClose(File); } //------------------------------- 6.2.4 对 TabSet1、B z 对 TabSet1 的响应 随 一些处理,其代码如下: //--------------------------------------------------------- void __fastcall TForm1::TabSet1Click(T {//界面处理 swit { case 0 : //添加新学生 EditName->Enabled=true; GroupBoxSex->Enabled=true; BitBtn1->Enabled=true; 第 6 章 学生成绩管理&资源管理器 158 BitBtn2->Enabled=true; 号 6 位以内整数."; se; 态显示"; e 2 : //修改 GroupBoxSex->Enabled=false; itBtn1->Enabled=false; 输入学号,点击 OK,修改后按 OK 保存修改"; EditName->Enabled=false; GroupBoxScore->Enabled=false; 置按钮为可用 BitBtn2->Enabled=false; 示:输入学号,点击 OK 完成删除"; lose(); LabelHint->Caption="提示:名字<=8 字符,学 } break; case 1 : //查找 { EditName->Enabled=false; GroupBoxSex->Enabled=false; GroupBoxScore->Enabled=fal BitBtn1->Enabled=false; BitBtn2->Enabled=false; LabelHint->Caption="提示:输入学生学号,其它信息随学号动 } break; cas { EditName->Enabled=false; GroupBoxScore->Enabled=false; //在输入学号被查找到的时候,置按钮为可用 B BitBtn2->Enabled=false; LabelHint->Caption="提示: } break; case 3 : //删除 { GroupBoxSex->Enabled=false; //在输入学号被查找到的时候, BitBtn1->Enabled=false; LabelHint->Caption="提 } break; case 4 : // { C } 第 6 章 学生成绩管理&资源管理器 159 break; ault : break; 的内容 m->Text=""; ale->Checked=false; t=""; nglish->Text=""; -- 和 BitBtn2 的响应 对于“Cancel”按钮 BitBtn2,也就是取消操作,只需要象 TabSet1 的 OnClick 事件一样, //---- - ----------------------------------------- void tn2Click(TObject *Sender) ------------------------------------ 1,需要判断不同的管理模式作出相应的响应,代码如下: ----------------------------------------- void __fastcall TForm1::BitBtn1Click(TObject *Sender) ) 成功 TabSet1->OnClick(this); } //不做处理,等待更改后重新按 OK def } //每次更改状态,都要清空各个组件 EditNu EditName->Text=""; RBtnFemale->Checked=false; RBtnM EditMath->Text=""; EditChinese->Tex EditE } //------------------------------------------------------------------------- z 对 BitBtn1 对界面进行初始化即可,代码如下: ----- ------------------------ __fastcall TForm1::BitB {//Cancel 按钮 TabSet1->OnClick(this); } //--------------------------------------- 对“OK”按钮 BitBtn //---------------------------------- { switch(TabSet1->TabIndex { case 0 : //添加新学生 { if(Add(false))//添加 { TabSet1->TabIndex=0; } else //添加失败 { }//end of case break; 第 6 章 学生成绩管理&资源管理器 160 case 1 : break; // 查找 //不会有这种情况 //设当前的学生信息为要修改的学生信息, ditName->Enabled=true; 中间变量 TmpStu 中 功 需要修改的信息 TabSet1->TabIndex=2; TabSet1->OnClick(this); } 不合法 ak; 生信息 mpStu=NULL; k(this); Edit 组件的响应 case 2 : // 更改 { if(!EditName->Enabled)//还没找到要修改的信息 { //各个组件设为可用,以供修改信息 E GroupBoxSex->Enabled=true; GroupBoxScore->Enabled=true; } else //已经做了修改,现在确认修改 {//修改完毕,将各个组件中的信息写入全局 if(Add(true)) //加入信息成 { Del(TmpStu); //删除 TmpStu 指向的 TmpStu=NULL; else //输入的新信息 { //不做处理,等待更改后再按 OK } } } bre case 3 : // 删除 { Del(TmpStu); //删除当前显示的学 T TabSet1->TabIndex=3; TabSet1->OnClic } break; case 4 : break;//不会有这种情况 default : break; } } //--------------------------------------------------------------------------- z 对各 第 6 章 学生成绩管理&资源管理器 161 对于 Edit 组件,除了上面提到了 EditNum 的 OnChange 事件外,还要对它们的输入进行 只允许输入 6 位以内的数字、成绩输入只允许输入 0~100 的值。它们的处理 体的实现代码这里不再列出。 化组件,其中在选项卡的“Win 3.1”页 DirectoryListBox)、文件列表框 这些组件要实现一个文件浏览、选择的 可。 限制。EditNum 可以参看第四章 4.2.1 和第五章 5.4.3 的内容,具 6.3 可视化文件管理组件 6.3.1 Win31 组件 C++Builder中提供一套用于文件浏览管理的可视 中就有驱动器组合框(DriveComboBox)、目录列表框( (FileList)、文件类型组合框(FliterComboBox)。用 组件的属性稍做设置即程序是非常简单的事情,只需要对几个 图 6-2 Win31 文件浏览器设计界面 器组合框 DriveComboBox1 的 DirList 属性设为 toryListBox1,这样,目录列表框 DirectoryListBox1 的显示内容会自动随 DriveComboBox1 设为 FileListBox1,这样文件列表框 FileListBox1 的 会随 DirectoryListBox1 的改变而改变,设置目录列表框的 DirLabel 属性为 Label5, FileListBox1 的 FileEdit 属性为 中被选定的文件名会动态显示在 Edit1 组件中;设置 FilterComboBox1 t 属性为 FileListBox1,也就是用 FilterComboBox1 中的后缀格式在 FileListBox1 中 所示。 的界面让人很看不惯,还是 Win32 组件的风格与现在 要使用这些 Win32 风 如图 6-2 添加组件,将驱动 Direc 而改变;再将目录列表框的 FileList 属性 文件列表 使得 Label5 自动显示当前目录列表框中被选择的目录;设置 Edit1,使得 FileListBox1 的 FileLis 对文件进行过滤显示。 这个文件浏览器就算做成了,其运行效果如图 6-3 看其运行效果,这种 Win31 风格 Windows 系统的界面风格相似一点,而本章所要讲述的资源管理器正是 第 6 章 学生成绩管理&资源管理器 162 格的组件。 图 6-3 Win31 文件浏览器运行效果 iew ”以及 ntrol”等组件。它们的使用就不像前面 Win31 那样简便了,而且它们也不是专门 设计的组件。 的组件。树结构的基本单位是 标记和小图标,通过点击节点左边的小 除了根节点之外, 同深度的节点显示在相 如,可以用 法来显示和管理一个公司的人事信息,利用目录树中不同的层次表示公司人事编 层次结构;可以用目录树的方法来显示和管理一个餐厅的进货记录,不同种类(肉 ,让人一目了然。而且,树视图子节点的展开和收缩 必再拉这滚动栏麻烦的寻找。 和方法列表如下: 的常用属性/方法 6.3.2 Win32 组件 “HeaderCo 为文件浏览而 z TreeView 树视图组件 树视图是一种用树状形式显示不同层次、不同类别的信息 TtreeNode 类型的树节点,每个节点都有自己的文本 点都可以有自己的子节点,而且, 所有的节点都有自己的父节点。节点和节点之间用虚线连接,并且相 同的列位置(缩进宽度相同),不同的缩进使 层次的信息 目录树的方 制的不同 等)显示在不同的节点 也使得用户在面对大量的种类信息时不 表 6-3 TreeView 组件 属性/方法 说明 AutoExpand bool 量,设为 true 时节点在选中时会自动展开 本章所要制作的资源管理器,要用到选项卡中“TreeView”,“ListV 图标可以展开或收起其子节点。每个节 得不同 能清晰的显示出来。 在本章我们用树结构来显示电脑中的目录,可是,目录树用途不止与此。比 类、蔬菜、米粉 为方便查找参阅,将 TreeView 组件的比较常用的属性 Sel 整型,只读,在 MultiSelect 属性为 ture 时,标志树视图中被选中的节点数ectionCount 第 6 章 学生成绩管理&资源管理器 163 目,在 MultiSelect 为 false 时,此属性始终为-1 Items __property TTreeNodes* Items = {read=FTreeNodes, write=SetTreeNodes}; 指向树视图节点列表类(TtreeNodes),通过此属性可以访问树视图中所有 的节点。 ItemMargin 设置节 Images __property Im 读此属性可 NULL。更改此属性的值可以更改 ShowButtons bool 型,决定是否在父节点的左侧显示“+”“-”按钮 ShowLines 设定是否显示各个节点之间的连线 ShowRoot So 性两者共同决定 ToolTips 决定鼠标在节点上时是 指定的位置 AlphaSort bool __fastcall AlphaSort(void);对树视图中的节点进行排序 FullCollapse void __fastcall FullCollapse(void);将视图中所有节点全部收缩 FullExpand void __fastcall FullExpand(void);将视图中所有节点全 LoadFromFile void __fastcal 点图标周围的空白,单位为象素 Indent 标志子节点展开时和其父节点之间的距离,以象素为单位 glist::TCustomImageList* Images = {read=FImages, write=SetImages};它是一个位图列表指针,用于树视图中各个节点的图标 Selected __property TTreeNode* Selected = {read=GetSelection, write=SetSelection}; 以获得当前被选中的节点,如果树中没有节点被选中,此值为 视图中被选中的节点,并且触发树视图的 OnChange 事件 设定是否显示根节点 rtType 视图中各个节点的排列方式,可以取 stNone(不进行排序操作)、stData(按 )、stText(按节点的标题属性排序)、stBoth(由节点的数 排列顺序) 节点的数据排序 据和标题属 否显示其 Hint TopItem 设定显示在视图最上方的节点。通过对它的改变,可以使得整个树滚动到 CustomSort 支持自定义排序方法,需要自己编写排序函数 部展开 GetNodeAt TTreeNode* __fastcall GetNodeAt(int X, int Y);返回(X,Y)坐标处的节点指 针。其中 X,Y 以树视图左上角为坐标原点,单位是象素 l LoadFromFile(const AnsiString FileName);从文件中读取用 ToFile 方法保存的数据,并显示在树视图中 Save LoadFromStream void __fastcall LoadFromStream(Classes::TStream* Stream);从流文件中读取 数据显示在树视图中 节点 SaveToFile void __fastcall SaveToFile(const AnsiString FileName);将树视图中节点数据 保存到文件中 SaveToStream void __fastcall SaveToStream(Classes::TStream* Stream);将树视图中节点数 据保存到流文件中 SelectAll void __fastcall SelectAll(bool Select); MultiSelect 属性为 true 时此方法用于全 选(Select 为 true)或反选(Select 为 false) InvertSelection void __fastcall InvertSelection(void); MultiSelect 为 true 时,此方法用于对所 有节点进行反选操作 第 6 章 学生成绩管理&资源管理器 164 IsEditing bool __fastcall IsEditing(void);调用此方法 因为树结构是由节点组成的,所以在对树视图的操作中,所有的节点都通过树视图的 而 Items 属性为树视图节点类(TtreeNodes)的一个实例。其声明 Nodes* Items = {read=FTreeNodes, write=SetTreeNodes}; TtreeNode 对象,这些节点可以通过数组索引的方式访问, 图中第二个节点,方法为:MyTreeNode = TreeView1->Items[1]; ddChild 等方法来为树视图的节点实现添加、插 等操作,TTreeNodes 类的重要方法说明如下: 说明 Items 属性来访问和管理, 树视图的每个节点都是一个 例如要访问树视 入、删除 方法 的位置。S 为插入节点的文本标记 将节点作为 Node 的第一个子节点插入树视 TTreeNode* __fastcall 所有的子节点 GetFirstNode TTreeNode* __fastcall GetFirstNode(void);获得树视图的第一个节点,也就是 Items[0]的值 GetNode TTreeNode* __fastcall GetNode(QListViewItemH ItemH);通过节点类句柄,得 到该节点的节点项 树视图中的每个节点是树视图节点类(TTreeNode)的 判断视图中是否有节点处在编辑 ue,否则,返回 false 状态(修改其 Caption),如果有,返回 tr 如下:__property TTree 程序运行中,可以用其 AddChildFirst、A 表 6-4 TTreeNodes 类的方法 TTreeNode* __fastcall Add(TTreeNode* Node, const AnsiString S);作为 Node 节点的最后一个兄弟节点加入树视图,如果视图排过序,节点插入在排序后 Add AddChild TTreeNode* __fastcall AddChild(TTreeNode* Node, const AnsiString S);将节点 作为 Node 节点的最后一个子节点插入树视图,如果视图已排序,则插入在 排序后的位置 AddChildFirst TTreeNode* __fastcall AddChildFirst(TTreeNode* Node, const AnsiString S); 图 AddFirst AddFirst(TTreeNode* Node, const AnsiString S);将节点 作为 Node 的第一个兄弟节点插入视图 Clear void __fastcall Clear(void);清除树视图中所有的节点 Delete void __fastcall Delete(TTreeNode* Node);删除 Node 指定的节点,并且删除其 Insert TTreeNode* __fastcall Insert(TTreeNode* Node, const AnsiString S);节点作为 Node 兄弟节点插入其后 实例,要对每个节点的属性进行 ode 类属性/方法 设置和更改,就需要用到 TTreeNode 的属性和方法。 表 6-5 TTreeN 属性/方法 说明 Count 表示节点的子节点(一级子节点)的数目 Expanded 节点子节点被展开时,为 true,收起时为 false。可以通过对它的更改实现对 第 6 章 学生成绩管理&资源管理器 165 节点的展开和收起 树节点并不是输 Focused 为 true 来实现对节点的更改。当节点 Focused HasChildren 标示节点是否有子节点 ImageIndex 设定正常情况下节点的图标在树视图 Imag IsVisible 标示节点是否显示 没有影响 Level 节点深度。视图中最上方的根结点 Level 为 0,其子节点 Level 为 1,依此类 推 Text Ansi 收起 Expand 打开节点的子节点 GetNext 返回该节点的下一个节点,不论下一个节点是否可见(Visable)或者它是不 是一个其它节点的子节点。如果该节点是最后一个节点,返回 NULL getNextSibling 返回节点的下一个兄弟节点 Ge 者之间的区别 HasAsParent bool __fastcall HasAsParent(TTreeNode* Value);判断 Value 是否为其父节点 IndexOf int __fastcall IndexOf(TTreeNode* Value);返回子节点 Value 的位置,如果 Value 不是其子节点,返回-1。第一个子节点返回 0,第二个是 1,依此类推 IsFirstNode 判断节点是否为根结点(没有父节点并且在它之 或者兄弟节点 z ListView 列表视图组件 列表视图组件用于使用列表的方式显示信息,列表可以通过水平、竖直方式用大图标、 ,每个列表项都有其图标、文本 记 小图标、列表、报告的方式显示。列表的组成单元是列表项 标 以及其它一些属性。 列表视图组件的一些重要属性有: Focused __property bool Focused = {read=GetFocused, write=SetFocused, nodefault}; 入组件,所以不能获得输入焦点。但是可以通过设置节点的 为 true 时,节点被一 个矩形框围绕,并且用户可以修改节点的文本标记 eList 中的位置 SelectedIndex 设定被选定情况下节点的图标在树视图 ImageList 中的位置 Index 该节点在其父节点的子节点数组中的位置 在当前的树视图的图象中。当父节点处在收起状态时,该 动条的拖动对该属性父节点的子节点就处于 IsVisible 为 false 的状态,而滚 Item __property TTreeNode* Item[int Index] = {read=GetItem 指向节点的子节点的指针数组 , write=SetItem}; Parent 指向节点的父节点 Selected 标示节点是否被选择 String 类型,为节点的文本标记 Collapse 节点的子节点 tNextVisible 返回下一个可见节点 GetPrev、GetPrevChild、getPrevSibling、GetPrevVisible 获得节点的前一个节点,区别同 GetNext 等三 前没有兄弟节点) MoveTo virtual void __fastcall MoveTo(TTreeNode* Destination, TNodeAttachMode 位置,Destination 为移动后节点的父节点 ,Mode 为移动后节点与 Destination 之间的关系 Mode);移动节点到树视图中的指定 第 6 章 学生成绩管理&资源管理器 166 表 6-6 TListView 类属性/方法 属性/方法 说明 CheckBoxs 标示列表视图中列表项后是否使用检查框 GridLines 标示是否使用网格线。而且,即使它为 true,也只有在 ViewStyle 是 vsReport 模式时才显示网格线 ItemFocused 标示当前获得焦点的列表项 Items TListItems 类型,是所有列表项的组合,通过此属性可以访问所有的列表项 MultiSelect 决定是否允许多选 Selected 当前被选中的列表项 Type 视图模式,有 vsIcon(大图标)、vsSmallIcon(小图标)、vsList(小图标列 式 View 表)、vsReport(报告)四种方 Arrange void __fastcall Arrange(TListArrangement Code);对列表视图中的图标进行重 新排列,Code 指定排列模式,可以取 arAlignBottom, arAlignLeft, arAlignRight, arAlignTop, arDefault, arSnapToGrid Custom Caption TListItem bool In Scroll DX(水平方向) 直方向)的距离 void __fastcall Scroll(int DX, int DY); , int LastIn 在做了影响到列表视 图显示的操作之后用于刷新视图框 6.4 资源管理器 使用 Win32 组件制作资源管理器,完成文件管理的常用功能,如:生成文件目录树、显 录和文件、文件的拷贝、粘贴、剪切、删除等。 如图 6-4 设计程序的界面。主菜单的结构参考 Windows 资源管理器的菜单,菜单下面是 器组合框(DriveComboBox), 置工具按钮(ToolButton),不同功能种类的工具按钮之间用 Separator(即 Style 属性为 最重要的两个组件是 TreeView 组件和 ListView 组件,它们的放置要使得窗口大小变化 有些技巧,具体操作如下: 示目录中的子目 6.4.1 界面的设计 一个 ToolBar 工具栏,在工具栏左边放一个 Label 和一个驱动 tbsSeparator 的工具按钮)分开。 Sort 支持自定义排序方法,需要自己编写排序函数 Find * __fastcall FindCaption(int StartIndex, AnsiString Value, bool Partial, clusive, bool Wrap);寻找文本标记为 Value 的列表项 GetItemAt TListItem* __fastcall GetItemAt(int X, int Y); 以列表视图左上角为坐标原点,单位为象素 获取(X,Y)位置的列表项。(X,Y) 是列表视图滚动 ,DY(竖 UpdateItem s(int FirstIndex dex);s void __fastcall UpdateItem 放 时能自动适应,所以组件的放置 第 6 章 学生成绩管理&资源管理器 167 首先在窗体上添加一个面板(Panel)组件,设置其 Align 属性为 alLeft,即左侧靠齐; 加一个 Panel 组件,设置然后添加一个 Splitter 组件,用来把窗体分割成两个部分;最后再添 Align 属性为 alClient,这样它就会填满 Spl 这样,窗体上的结构就大体成型了,下面就是在两个 Panel 中 图之前先在 它组件) 这样 运行中,不必再费心去处理界面 的图标读到 ImageList 中,然后将 ToolBar、TreeView、ListView 的 ImageList 属性 加的图象列 6.4.2 z 其 图 6-4 资源管理器界面设计 itter 右边所有空白的窗体部分。 分别添加树视图组件和列 图上方能显示一些信息,需要在添加树视图和列表视 两个 Panel 中分别添加一个 Align 属性为 alTop 的 Panel(当然也可以用 Label 等其 ,设置它们的边框属性(BevelInner、BevelOuter 等)使其显示尽量美观。在两侧的 ,界面的设置工作就完成了,之所以要这么复杂的设置组件的属性,是为了在程序 ,让界面的组件随窗体自动变化到合适的大小和位置。 图标,所以需要事先把做好 都设为所添 表组件,再为工具栏的按钮设置其 ImageIndex 属性以显示图片。而 TreeView 和 结构分析 一下资源管理器要完成的主要的功能,以及用怎样思路去实现它。 表视图组件了。为了在树视图和列表视 Panel 中添加了 TreeView 和 ListView 后,设置它们的 Align 为 alClient。 另外一个重要的组件就是 ImageList,因为本程序需要大量的 ListView 组件对 ImageList 的使用在程序中要考代码实现。 功能的实现 首先,我们分析 第 6 章 学生成绩管理&资源管理器 168 资源管理器,顾名思义,最重要的功能莫过于对电脑上的硬盘和文件进行浏览,然后辅 和删除等其它操作。本程序的主要目的是介绍文件 主要的功能,没有做太多的 现用树视图对电脑磁盘和文件进行浏览,首先需要将电脑上的磁盘以及磁盘中所有 的作为树节点加入树视图中。当然,你可以在程序运行之初遍历电脑中所有 是,很明显的这会使得程序的启动慢很多,而且很多文 会浏览到的,没必要这么一次性全都加入树视图。本程序中是这样设计的, ir 函数),它读取指定的目录中的子目录 加入树视图中,但是对于其子目录的下一级目录,则不做处理,只是判断一下这些 后设置其 HasChildren 属性以在其左边显示“+”图标(当然, 软盘和光驱)调用 AddDir, 。 要处理列表视图中的文件显示。树视图中节点被选择、驱动器组合框的选项改变 stView 组件对指定目录文件的显示。 ir 变量用来存放当前所在的目录路径,这样,树视 tDir 的值,然后调用 ListView 函数要完成两件事,第一就是要将 CurrentDir 目录中的子目录 另外就是要将目录中的各种文件显示出来,并且根据文件类型的不同显示不同的 对文件和文件夹的剪切等操作。 义全局变量 AnsiString TargetFilePath,TargetFileName;分别存放在剪切、复制、粘贴 的绝对路径和其文件名。然后当 CurrentDir 改变到其它目录 者移动操作。对文件和文件 夹函数来很容 Path,TargetFileName; //复制粘贴等文件操作时标记目标文件的文件名和路径 FileOperateMode; //0:NONE 1: Copy 2: Cut 3: Paste 文件操作的模式 oid __fastcall RefreshTreeView(void);//更新树视图 oid __fastcall RefreshListView(void);//更新列表视图 ir(void);//更新列表视图中子目录的显示 视图中文件的显示 Dir(AnsiString Dir,TTreeNode *FatherNode); //将目录 Dir 加入树视图的 Node 节点,Node 为 Dir 所在节点的父节点 bool __fastcall HasSubDir(AnsiString Dir);//判断目录 Dir 中是否含有子目录 HasDirOrFile(AnsiString Dir,AnsiString DOrF); 助的要能实现对文件的剪切、复制、粘贴 细节修饰。 要实 的操作以及树视图、列表视图的使用,所以本程序重点实现这些 的目录逐个层次 的目录,然后将它们加入树视图,但 件夹都是用户不 首先定义一个将指定目录加入树视图的函数(AddD 然后全都 子目录是否还有下一级目录,然 这只是为了美化界面)。在程序初始化时,只是对电脑的磁盘(包括 而在用户选择了树视图中的节点时,在对选中节点对应的目录调用 AddDir 函数 其次, 以及工具栏中向上按钮的事件和刷新事件都会触发 Li 我们在头文件中定义了 AnsiString CurrentD 图、驱动器组合框以及工具栏的向上事件都可以只改变 Curren 的刷新函数。ListView 的刷新 显示出来, 图标。 完成了对树视图和列表视图显示的处理时候,还要实现 我们先定 操作中的对象,也就是目标文件 夹的复制、移动(剪切)、删除操作都可以通过 C++Builder 提供的文件和文件 易的实现。 z 头文件中添加的内容 AnsiString CurrentDir; //标记当前所在的目录 int v void __fastcall RefreshListViewD void __fastcall Add int __fastcall 之后执行粘贴操作就可以用这两个变量来对目标文件进行复制或 下面就逐个的介绍如何实现这些主要的功能。 AnsiString TargetFile v void __fastcall RefreshListViewFile(void);//更新列表 第 6 章 学生成绩管理&资源管理器 169 //判断 Dir 目录中是否含有文件名为 DOrF 的文件或者目录名为 DOrF 的子目录 新树视图 其中,树视图的刷新函数先添加根节点“我的电脑”,然后添加软驱的节点(A:),再根 将 C:、D:等驱动器加入节点。节点加入完毕之后,将“我 开。树视图刷新函数以及它用到的 Add 函数和 HasSubDir 函数代码如下: astcall TForm1::RefreshTreeView(void) ,Add 函数第一个参数为空 puter->ImageIndex=0; //指定其图标 omputer->SelectedIndex=0; //指定该节点被选定后的图标 ves=TreeView1->Items->AddChild(MyComputer,"A:"); ageIndex=1; ; AnsiString(char('D'+i))+":"); yDrives->ImageIndex=2; Index=2; AddDir(AnsiString(char('D'+i))+":",MyDrives); } TreeView1->Items->GetFirstNode()->Expand(false); } //--------------------------------------------------------------------------- z 树视图的显示 树视图和列表视图的初始化如下: //--------------------------------------------------------------------------- __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { CurrentDir="G:"; //设置默认的当前目录 RefreshTreeView(); //刷 } //--------------------------------------------------------------------------- 据驱动器组合框中驱动器的数目 的电脑”节点展 void __f { TreeView1->Items->Clear(); //清空视图树 //根节点 MyCom MyC RefreshListView();//刷新列表视图 //--------------------------------------------------------------------------- TTreeNode *MyComputer,*MyDrives; MyComputer=TreeView1->Items->Add(NULL,"我的电脑"); MyDri MyDrives->Im MyDrives->SelectedIndex=1 AddDir("A:",MyDrives); //加入软驱 for(int i=1;iItems->Count;i++) { M MyDrives=TreeView1->Items->AddChild(MyComputer, MyDrives->Selected //打开根节点的目录树 第 6 章 学生成绩管理&资源管理器 170 void __fastcall TFor { m1::AddDir(AnsiString Dir,TTreeNode *FatherNode) TSearchRec SearchRecord; if( rchRecord)) / if(SearchRecord.Attr==faDirectory && 找到的是一个目录,并且它不是当前目录或上级目录 //如果找到的是目录,则加入树视图中 ode=TreeView1->Items->AddChild(FatherNode,SearchRecord.Name); ChildNode->ImageIndex=3; 不将其子目录加入视图, //而只是将其 HasChildren 设为 true,以便在其左方显示"+"号 if(HasSubDir(Dir+"\\"+SearchRecord.Name)) ChildNode->HasChildren=true; } } while(FindNext(SearchRecord)==0) { if(SearchRecord.Attr==faDirectory && SearchRecord.Name!="." && SearchRecord.Name!="..") { ChildNode=TreeView1->Items->AddChild(FatherNode,SearchRecord.Name); ChildNode->ImageIndex=3; ChildNode->SelectedIndex=3; if(HasSubDir(Dir+"\\"+SearchRecord.Name)) ChildNode->HasChildren=true; } } FindClose(SearchRecord); //释放寻找文件使用的资源 } //---------------------------------------------- bool __fastcall TForm1::HasSubDir(AnsiString Dir) { TSearchRec SearchRecord; if(FindFirst(Dir+"\\*.*",faDirectory,SearchRecord)) {//开始在目录 Dir 中寻找其子目录 if(SearchRecord.Attr==faDirectory && TTreeNode *ChildNode; FindFirst(Dir+"\\*.*",faDirectory,Sea SearchRecord.Name!="." && SearchRecord.Name!="..") //查 ChildN //如果该目录有子目录,并 {/开始在目录 Dir 中寻找其子目录 { ChildNode->SelectedIndex=3; 第 6 章 学生成绩管理&资源管理器 171 SearchRecord.Name!="." && SearchRecord.Name!="..") { FindCl return true; } } t(SearchRecord)==0) { if(SearchRecord.Attr==faDirectory && FindClose(SearchRecord); ; } } FindClose( 使用的资源 return false; } //----- --------------- 列表视图代码如下: --------------------------------------------------------------------------- oid __fastcall TForm1::RefreshListView(void) ListView1->Items->Clear(); RefreshListViewDir(); RefreshListViewFile(); --------------------------------------------------------------------------- 中 RefreshListViewDir 和 RefreshListViewFile 分别完成对 Current 目录中的子目录和文件的 示。具体代码如下: --------------------------------------------------------------------------- oid __fastcall TForm1::RefreshListViewDir(void) //搜索 CurrentDir 中的目录和文件并显示之 TListItem *NewItem; TSearchRec SearchRecord; if(FindFirst(CurrentDir+"\\*.*",faDirectory,SearchRecord)==0) { if(SearchRecord.Attr==faDirectory && SearchRecord.Name!="." && SearchRecord.Name!="..") ose(SearchRecord); while(FindNex SearchRecord.Name!="."&&SearchRecord.Name!="..") { return true SearchRecord); //释放寻找文件 ------------------------------------------------------- z 列表视图的显示 // v { } // 其 显 // v { 第 6 章 学生成绩管理&资源管理器 172 { NewItem=ListView1->Items->Add(); earchRecord.Name; NewItem->ImageIndex=3; NewItem->SubItems->Add(""); NewItem->SubItems->Add("文件夹"); NewItem->SubItems->Add(FileDateToDateTime(SearchRecord.Time)); } while(FindNext(SearchRecord)==0) { if(SearchRecord.Attr==faDirectory && SearchRecord.Name!="." && SearchRecord.Name!="..") { NewItem=ListView1->Items->Add(); NewItem->Caption=SearchRecord.Name; NewItem->ImageIndex=3; NewItem->SubItems->Add(""); NewItem->SubItems->Add("文件夹"); NewItem->SubItems->Add(FileDateToDateTime(SearchRecord.Time)); } } FindClose(SearchRecord); //--- *NewItem; TSearchRec SearchRecord; ring Ext; 0) { NewItem->Caption=S } } ------------------------------------------------------------------------ void __fastcall TForm1::RefreshListViewFile(void) {//搜索 CurrentDir 目录中的文件并显示他们 TListItem AnsiSt if(FindFirst(CurrentDir+"\\*.*",0,SearchRecord)== if(SearchRecord.Attr!=faDirectory) { Ext=ExtractFileExt(SearchRecord.Name).LowerCase(); if(Ext==".exe") { NewItem=ListView1->Items->Add(); NewItem->Caption=SearchRecord.Name; NewItem->ImageIndex=8; NewItem->SubItems->Add(AnsiString(SearchRecord.Size)); 第 6 章 学生成绩管理&资源管理器 173 NewItem->SubItems->Add("应用程序"); >SubItems->Add(FileDateToDateTime(SearchRecord.Time)); .jpg" || Ext==".jpeg" || Ext==".gif") ame; =".doc" || Ext==".txt" || Ext==".rtf") rchRecord.Name; tem->ImageIndex=5; ecord.Time)); cord.Name; me(SearchRecord.Time)); } w { if(SearchRecord.Attr!=faDirectory) em=ListView1->Items->Add(); NewItem- } else if(Ext==" { NewItem=ListView1->Items->Add(); NewItem->Caption=SearchRecord.N NewItem->ImageIndex=4; NewItem->SubItems->Add(AnsiString(SearchRecord.Size)); NewItem->SubItems->Add("图像"); NewItem->SubItems->Add(FileDateToDateTime(SearchRecord.Time)); } else if(Ext= { NewItem=ListView1->Items->Add(); NewItem->Caption=Sea NewI NewItem->SubItems->Add(AnsiString(SearchRecord.Size)); NewItem->SubItems->Add("文档文本"); NewItem->SubItems->Add(FileDateToDateTime(SearchR } else { NewItem=ListView1->Items->Add(); NewItem->Caption=SearchRe NewItem->ImageIndex=8; NewItem->SubItems->Add(AnsiString(SearchRecord.Size)); NewItem->SubItems->Add(Ext+"类型文件"); NewItem->SubItems->Add(FileDateToDateTi } } hile(FindNext(SearchRecord)==0) { Ext=ExtractFileExt(SearchRecord.Name).LowerCase(); if(Ext==".exe") { NewIt 第 6 章 学生成绩管理&资源管理器 174 cord.Name; nsiString(SearchRecord.Size)); NewItem->SubItems->Add("应用程序"); Add(FileDateToDateTime(SearchRecord.Time)); else if(Ext==".jpg" || Ext==".jpeg" || Ext==".gif") ListView1->Items->Add(); SubItems->Add("图像"); NewItem->SubItems->Add(AnsiString(SearchRecord.Size)); NewItem->SubItems->Add("文档文本"); NewItem->SubItems->Add(FileDateToDateTime(SearchRecord.Time)); } else { NewItem=ListView1->Items->Add(); NewItem->Caption=SearchRecord.Name; NewItem->ImageIndex=8; NewItem->SubItems->Add(AnsiString(SearchRecord.Size)); NewItem->SubItems->Add(Ext+"类型文件"); NewItem->SubItems->Add(FileDateToDateTime(SearchRecord.Time)); } } } FindClose(SearchRecord); } //------------------------------------------------------------- z 对树视图和列表视图鼠标事件的响应 当树视图中的节点被点中时,要判断它的子目录是否已经加入目录树,如果没有,则添 NewItem->Caption=SearchRe NewItem->ImageIndex=8; NewItem->SubItems->Add(A NewItem->SubItems-> } { NewItem= NewItem->Caption=SearchRecord.Name; NewItem->ImageIndex=4; NewItem->SubItems->Add(AnsiString(SearchRecord.Size)); NewItem-> NewItem->SubItems->Add(FileDateToDateTime(SearchRecord.Time)); } else if(Ext==".doc" || Ext==".txt" || Ext==".rtf") { NewItem=ListView1->Items->Add(); NewItem->Caption=SearchRecord.Name; NewItem->ImageIndex=5; 第 6 章 学生成绩管理&资源管理器 175 加到目录树中。另外要重新设置当前目录 CurrentDir 的值,然后调用 ListView 的刷新函数来 在列表视图中重新显示被选中的目录中的子目录和文件。 代码如下: //------------------------------------------------------------- void __fastcall TForm1::TreeView1Click(TObject *Sender) { TTreeNode *TmpNode=TreeView1->Selected; AnsiString DirPath=""; //如果被选中的节点为空则返回 if(!TmpNode) return; //如果选中根节点,则返回 if(TmpNode==TreeView1->Items->GetFirstNode()) { return; } //如果选中文件夹,或者驱动器,先获得被选中的目录路径 do { DirPath="\\"+DirPath; DirPath=TmpNode->Text+DirPath; TmpNode=TmpNode->Parent; }while(TmpNode != TreeView1->Items->GetFirstNode()); //判断选中的目录的子目录是否已经加入目录树,因为为了界面美化,如果 //一个目录有子目录,不管子目录有没有加入目录树,都设其 HasChild 为 true //所以这里只能用 getFirstChild()来判断 if(!TreeView1->Selected->getFirstChild()) //子目录还没有加入视图 { //TmpNode 在循环中已经被更改到根节点,所以这里不能用它 AddDir(DirPath,TreeView1->Selected); } //更改被选中目录为当前目录 CurrentDir=DirPath; //更新列表视图的显示 RefreshListView(); } //--------------------------------------------------------------------------- 当列表视图中发生鼠标双击事件时,要判断选中的是文件还是文件夹,如果是文件夹, 则将此文件夹设为 CurrentDir,然后刷新列表视图的显示;如果是文件,则使用 Shell 函数来 让系统打开文件。调用 Shell 函数的效果就相当于在“开始”->“运行”中输入文件和目录路 第 6 章 学生成绩管理&资源管理器 176 径,然后回车的效果,在以后的章节会讲述 Shell 函数的使用。当然对文件夹也可以使用 Shell 函数来打开,但是系统会调用自己的浏览器来打开目录,所以对目录要用我们自己制作的浏 览器来处理,其它文件交给系统处理。代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::ListView1DblClick(TObject *Sender) { TTreeNode *TmpNode; AnsiString TmpFile; if(ListView1->SelCount==1) { if(ListView1->Selected->ImageIndex==3) { //改变当前目录位置 CurrentDir=CurrentDir+"\\"+ListView1->Selected->Caption; RefreshListView(); } else { TmpFile=CurrentDir+"\\"+ListView1->Selected->Caption; ShellExecute(Handle,"open",TmpFile.c_str(),NULL,NULL,SW_SHOWNORMAL); } } } //--------------------------------------------------------------------------- z 菜单和工具栏中组件的响应 菜单中的 View 菜单项中有选择列表显示模式的菜单项,它们被点击是要通过改变 ListView 的 ViewStyle 属性来改变列表的显示模式。而且还要设置菜单项的 Check 状态。其 代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::MenuViewIconClick(TObject *Sender) { ListView1->ViewStyle=vsIcon; MenuViewIcon->Checked=true; MenuViewSmallIcon->Checked=false; MenuViewList->Checked=false; MenuViewReport->Checked=false; } //--------------------------------------------------------------------------- void __fastcall TForm1::MenuViewSmallIconClick(TObject *Sender) { ListView1->ViewStyle=vsSmallIcon; MenuViewIcon->Checked=false; MenuViewSmallIcon->Checked=true; 第 6 章 学生成绩管理&资源管理器 177 MenuViewList->Checked=false; MenuViewReport->Checked=false; } //--------------------------------------------------------------------------- void __fastcall TForm1::MenuViewListClick(TObject *Sender) { ListView1->ViewStyle=vsList; MenuViewIcon->Checked=false; MenuViewSmallIcon->Checked=false; MenuViewList->Checked=true; MenuViewReport->Checked=false; } //--------------------------------------------------------------------------- void __fastcall TForm1::MenuViewReportClick(TObject *Sender) { ListView1->ViewStyle=vsReport; MenuViewIcon->Checked=false; MenuViewSmallIcon->Checked=false; MenuViewList->Checked=false; MenuViewReport->Checked=true; } //--------------------------------------------------------------------------- 工具栏中的刷新和 View 菜单中的刷新都通过调用 RefreshListView()函数来实现,代码如 下: //--------------------------------------------------------------------------- void __fastcall TForm1::TBtnRefreshClick(TObject *Sender) { RefreshListView(); } //--------------------------------------------------------------------------- void __fastcall TForm1::MenuViewRefreshClick(TObject *Sender) { RefreshListView(); } //--------------------------------------------------------------------------- 驱动器组合框的改变也需要 ListView 中响应,只需要根据组合框中被选择的驱动器设置 CurrentDir 然后刷新列表组件即可,代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::DriveComboBox1Change(TObject *Sender) { CurrentDir=AnsiString(DriveComboBox1->Drive); CurrentDir=CurrentDir+":"; RefreshListView(); } 第 6 章 学生成绩管理&资源管理器 178 //--------------------------------------------------------------------------- 工具栏中上一级目录的显示也是通过对 CurrentDir 的改变来实现,也就是将 CurrentDir 字符串中最后一个’\’字符以及其以后的字符全都删除调,就是上级目录的路径,然后刷新 ListView 即可,代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::TBtnUpClick(TObject *Sender) { int i; i=CurrentDir.Length(); while(i>0) { if(CurrentDir[i]=='\\') { CurrentDir.Delete(i,CurrentDir.Length()); RefreshListView(); break; } i--; } Caption=CurrentDir; } //--------------------------------------------------------------------------- z 文件/文件夹剪切、复制、粘贴和删除 对于剪切和复制,它们并不完成具体的文件操作,而是设定 FileOperateMode,然后在粘 贴操作的时候根据 FileOperateMode 的值来判断是指定 CopyFile 函数还是 MoveFile 函数。 复制和剪切的响应代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::MenuEditCutClick(TObject *Sender) { if(ListView1->SelCount==1) { TargetFilePath=CurrentDir+"\\"+ListView1->Selected->Caption; TargetFileName=ListView1->Selected->Caption; FileOperateMode=2; } } //--------------------------------------------------------------------------- 第 6 章 学生成绩管理&资源管理器 179 void __fastcall TForm1::MenuEditCopyClick(TObject *Sender) { if(ListView1->SelCount==1) { TargetFilePath=CurrentDir+"\\"+ListView1->Selected->Caption; TargetFileName=ListView1->Selected->Caption; FileOperateMode=1; } } //--------------------------------------------------------------------------- 对于粘贴操作,稍微复杂一点,因为用户复制或者剪切的对象可能是一个文件,也可能 是一个目录,所以要根据不同的情况分别处理,代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::MenuEditPasteClick(TObject *Sender) { if(FileOperateMode==1) //Copy { if(FileExists(CurrentDir+"\\"+TargetFileName)) //文件存在 { if(MessageBox(Handle, ("文件"+TargetFileName+"存在,是否覆盖?").c_str(), "确认覆盖文件",MB_OKCANCEL|MB_ICONQUESTION)==IDOK) { DeleteFile(CurrentDir+"\\"+TargetFileName); CopyFileA(TargetFilePath.c_str(), (CurrentDir+"\\"+TargetFileName).c_str(),false); RefreshListView(); } } else if(DirectoryExists(CurrentDir+"\\"+TargetFileName)) //目录存在 { if(MessageBox(Handle, ("目录"+TargetFileName+"存在,是否覆盖?").c_str(), "确认覆盖目录",MB_OKCANCEL|MB_ICONQUESTION)==IDOK) { RemoveDir(CurrentDir+"\\"+TargetFileName); CopyFileA(TargetFilePath.c_str(), (CurrentDir+"\\"+TargetFileName).c_str(),false); RefreshListView(); 第 6 章 学生成绩管理&资源管理器 180 } } else { CopyFileA(TargetFilePath.c_str(), (CurrentDir+"\\"+TargetFileName).c_str(),false); RefreshListView(); } } else if(FileOperateMode==2) //Cut { if(FileExists(CurrentDir+"\\"+TargetFileName)) { if(MessageBox(Handle, ("文件"+TargetFileName+"存在,是否覆盖?").c_str(), "确认覆盖文件",MB_OKCANCEL|MB_ICONQUESTION)==IDOK) { DeleteFile(CurrentDir+"\\"+TargetFileName); MoveFileA(TargetFilePath.c_str(), (CurrentDir+"\\"+TargetFileName).c_str()); RefreshListView(); } } else if(DirectoryExists(CurrentDir+"\\"+TargetFileName)) { if(MessageBox(Handle, ("目录"+TargetFileName+"存在,是否覆盖?").c_str(), "确认覆盖目录",MB_OKCANCEL|MB_ICONQUESTION)==IDOK) { RemoveDir(CurrentDir+"\\"+TargetFileName); MoveFileA(TargetFilePath.c_str(), (CurrentDir+"\\"+TargetFileName).c_str()); RefreshListView(); } } else { MoveFileA(TargetFilePath.c_str(), (CurrentDir+"\\"+TargetFileName).c_str()); RefreshListView(); 第 6 章 学生成绩管理&资源管理器 181 } } FileOperateMode=0; } //--------------------------------------------------------------------------- 需要说明的是,上面的代码虽然对不同的情况做了不同的处理,但是实际上还有一些问 题没有解决,比如复制的如果是目录,并没有对应的函数能把目录连同其中的子目录和文件 全都拷贝到别的地方,所以,需要自己做个循环操作,先创建目录,然后将目录中的文件和 目录在拷贝过去。删除操作也是一样,使用 RemoveDir 删除目录时,目录必须为空才能成功 删除,所以需要自己编写循环将目录中的内容清空,然后才能删除这个目录。具体的这里就 不再对它做实现了,请读者自己编写删除非空目录的函数,以及粘贴非空目录的函数。 删除的函数如下: //--------------------------------------------------------------------------- void __fastcall TForm1::MenuEditDelClick(TObject *Sender) { if(ListView1->SelCount==1) { if(FileExists(CurrentDir+"\\"+ListView1->Selected->Caption)) //不是文件夹 { if(MessageBox(Handle, ("确认删除文件"+ListView1->Selected->Caption+"?").c_str(), "确认删除文件",MB_OKCANCEL|MB_ICONQUESTION)==IDOK) { DeleteFile(CurrentDir+"\\"+ListView1->Selected->Caption); } } else { if(MessageBox(Handle, ("确认删除文件夹"+ListView1->Selected->Caption+"?").c_str(), "确认删除文件夹",MB_OKCANCEL|MB_ICONQUESTION)==IDOK) { RemoveDir(CurrentDir+"\\"+ListView1->Selected->Caption); } } RefreshListView(); } } //--------------------------------------------------------------------------- 第 6 章 学生成绩管理&资源管理器 182 6.5 思考题 z 学生信息管理程序中,学生信息项太少,这大大减小了其实用价值,请读者修改其信息 结构,以符合实际使用的需要。 z 资源管理器程序中,对磁盘盘符的确定方法并不是最好的,参看第九章 9.4.1 节的内容, 提出修改方法。 z 资源管理器程序中,在枚举目录下的文件或文件夹时,是使用的 if() {} while() {} 结构,请读者提出一种比它简练些的程序结构。 z 如何实现整个目录的删除? 《C++ Builder 6 编程实例精解 赵明现》 第 07 章 屏幕保护程序的制作 本章重点 本章介绍屏幕保护程序的制作技术,包括屏保的预览、运行设置以及正常运行效果三部 分;还介绍了制作屏保程序需要使用的文字、图象处理和显示等技巧。 学习目的 通过本章的学习,您可以: ■ 掌握获取命令行参数的方法 ■ 掌握注册表的使用方法 ■ 字幕显示相关技巧 ■ 图象的柔化、锐化、浮雕、旋转技巧 ■ 图象的中心扩散、百叶窗、交错等显示技巧 第 7 章 屏幕保护程序的制作 184 本章典型效果图 第 7 章 屏幕保护程序的制作 185 7.1 屏保制作的关键技术 屏幕保护程序的制作要考虑到三个方面的技术:首先是对命令行参数的获取和处理;其 次是要截获键盘鼠标事件,以便退出屏幕保护程序;最后就是文本、图像等技术在屏保中的 应用。本节着重讲述前两个方面,第三个方面的技术在后面的实例中会对各种的技巧逐个详 细讲述。 7.1.1 命令行参数的获取和处理 屏幕保护程序都是以“.scr”为扩展名的 Windows 程序,本质上就是 exe 可执行程序, 只是它对一些系统提交的屏保设置参数进行特别的处理而已。在桌面->属性的屏幕保护程序 页中,会自动搜索 Windows 目录(系统目录)中扩展名为 scr 的程序,显示在其下拉菜单的 列表之中,供用户选择。 z 命令行参数相关的变量与函数 屏幕保护程序有运行模式、预览模式、密码设置以及启动参数设置模式,而选择那种模 式运行是由传递给程序的命令行参数决定的。标准 C 语言中 main 函数的形式如下: int main(int argc, char * argv[]) 其中第一个参数是传递的命令行参数的数目,第二个参数是参数字符串。而在 C++Builder 中,提供下面几个函数用来处理命令行参数: 1. CmdLine extern PACKAGE char *CmdLine;CmdLine 是一个字符指针,指向程序被执行时传递给程 序的命令行参数。 2. FindCmdLineSwitch extern PACKAGE bool __fastcall FindCmdLineSwitch(const AnsiString Switch, const TSysCharSet &SwitchChars, bool IgnoreCase); extern PACKAGE bool __fastcall FindCmdLineSwitch(const AnsiString Switch, bool IgnoreCase); extern PACKAGE bool __fastcall FindCmdLineSwitch(const AnsiString Switch); FindCmdLineSwitch 决定字符串是否作为命令行参数传递给应用程序。其中参数 Switch 是命 令行的字符串;SwitchChar 是区分入口参数值和参数标志的字符,如果 FindCmdLineSwitch 中这个参数被忽略,那么在 Windows 中,将取默认值’/’和’-‘(Linux 下默认为’-‘);第三个参 数默认为 true。 3. ParamCount extern PACKAGE int __fastcall ParamCount(void);返回命令行参数的参数数目。不同的参 数之间用空格或者 Tab 键隔开。 4. ParamStr extern PACKAGE AnsiString __fastcall ParamStr(int Index);返回由 Index 指定的命令行参 第 7 章 屏幕保护程序的制作 186 数,如果 Index 大于 ParamCount,则返回空字符串。例如 Index 为 2,则返回命令行中第二 个参数。如果 Index 为 0,则返回程序自身的所在的绝对路径。 z 屏保的命令行参数 屏保程序都是需要带命令行参数执行的,Windows 使用“/s”或“-s”参数全屏执行屏保 程序、使用“/c”或“-c”参数进行屏保的运行设置、使用“/p”或“-p”参数预览屏保 程序、使用“/a”或“-a”参数设置屏保的密码。 1. 全屏执行 在“显示属性”窗口的“屏幕保护程序”页中,按下“预览”按钮,可以预览 屏幕保护程序的效果。此时,系统传递给屏保程序的参数为“/s”或“-s”。正常执行屏 保、此处的预览的处理都是一样的,一方面执行字母、图象操作,另一方面,要处理用 户的鼠标或者键盘事件,以适时退出屏保程序。 2. 运行设置 在“显示属性”窗口的“屏幕保护程序”页中,按下“设置”按钮,会启动屏 保运行参数的设置窗口。这是系统传递的参数是“/c”或“-c”,屏保程序根据这个参数 弹出对话窗,获得用户进行更改后的运行设置,如文本的内容、图形的旋转速度等,然 后将这些设置写入注册表(或者文件),以供屏保启动时读取。 3. 屏保预览 在“显示属性”窗口的“屏幕保护程序”页中,在下拉菜单中选定一个屏保程 序之后,在上边会有一个小范围显示屏保程序的运行效果。此时系统传递的参数是“/p” 或“-p”,而且,系统传递的参数中还有当前窗口的句柄,也就是 “显示属性”窗口的 句柄,在屏保程序中要根据传递的参数在“显示属性”窗口的显示器形状的范围内预览 屏保效果。 4. 密码设置 密码设置时,系统传递的参数为“-a”,由于屏保密码在 NT 版本中不支持,所 以本章中对这样功能不做讲述。 7.1.2 注册表的使用 对屏保程序运行参数的保存,我们利用 Windows 注册表来实现。 z NT 注册表简介 注册表里面存放着系统硬件配置、Windows 系统、Windows 应用软件的相关信息,这些 信息被分层分类的保存在注册表这个数据库中。注册表树状结构的每一个节点叫做一个键, 每个键都可以有自己的子键和值,应用程序可以通过对注册表键、值的修改来实现程序配置 信息的保存和读取。 运行 regedit 打开注册表编辑器,可以看到注册表的多层次的树状复杂结构。注册表包含 六个根键: HKEY_CLASSES_ROOT:它包括与 OLE、文件关联、快捷方式等有关的信息。 HKEY_CURRENT_USER:它用来管理与当前登录到系统的用户有关的信息。这些信息 包括:用户桌面、屏幕颜色以及系统对用户呈现的界面外观和行为;与所有网络设备的连接; 第 7 章 屏幕保护程序的制作 187 桌面程序项;应用程序参数设置其它一些个人爱好以及安全权限。这些信息都被保存在注册 表中以供用户登录时系统进行检索使用。 HKEY_LOCAL_MACHINE:它包括 NT 系统的系统信息,包括应用程序、驱动程序和 硬件信息。它有 5 个子键,其中 HARDWARE 用来保存系统的硬件信息,在每次计算机启动 的时候都会自动更新,所以用户对这一项的更改不会起作用也没有意义;SAM 用来存放用户 帐户的信息,它只有系统管理员用帐户管理器修改,其它方式都不允许访问;SECURITY 中 存放本地用户以及用户组的权限等与安全相关的信息,当然,它也是不允许访问的; SOFTWARE 存放安装的系统软件和应用程序的信息;SYSTEM 中存放与系统启动、设备驱 动程序、服务和 NT Server 配置有关的信息。 HKEY_USERS:其中存放拥有用户配置文件的未禁用的帐户的信息。 HKEY_CURRENT_CONFIG:在 NT 4 以前的版本中没有这一项,它的信息分别来自于 HKEY_LOCAL_MACHINE\SYSTEM 和 HKEY_LOCAL_MACHINE\SOFTWARE,但它只保 存了其中的一部分信息。 HKEY_DYN_DATA:它包含着系统的即插即用状态。 z TRegistry 类的属性和方法 Windows 提供对注册表进行操作的 API 函数,而在 C++Builder 中提供封装了这些操作的 TRegistry 类,其常用属性和方法如下: 表 7-1 TRegistry 类常用属性/方法 属性/方法 说明 CurrentKey __property HKEY CurrentKey = {read=FCurrentKey, nodefault}; 它决定当前被打开的键。TRegistry 类的所有操作只对当前打开的键 CurrentKey 起作用 CurrentPath __property AnsiString CurrentPath = {read=FCurrentPath}; 获得当前打开的键的路径。注册表是分级的,CurrentPath 是包含从根键到 CurrentKey(包含此键)的字符串,每次 CurrentKey 改变时,CurrentPath 自动 随之改变 RootKey __property HKEY RootKey = {read=FRootKey, write=SetRootKey, nodefault}; 当前操作的根键。当 TResgistry 类的对象被创建后,RootKey 默认在 HKEY_CURRENT_USER。 CloseKey void __fastcall CloseKey(void); CloseKey 被调用时,键值被写入注册表并且当前键被关闭。程序中,如果不需 要,就不要让键处于打开状态。一般在每次读取和写入操作完成之后,都要调 用 CloseKey 操作 CreateKey bool __fastcall CreateKey(const AnsiString Key); 创建一个子键,子键的名称由参数 Key 指定。键的路径可以是绝对路径或者相 对路径,相对路径时,创建当前主键的子键。创建后的键的键值为空 DeleteKey bool __fastcall DeleteKey(const AnsiString Key); 删除指定键以及其键值。删除成功返回 true,否则返回 false 第 7 章 屏幕保护程序的制作 188 DeleteValue bool __fastcall DeleteValue(const AnsiString Name); 删除当前键的键值 KeyExists bool __fastcall KeyExists(const AnsiString Key); 查找名字为 Key 的键是否存在 OpenKey bool __fastcall OpenKey(const AnsiString Key, bool CanCreate); 将 Key 指定的键设置为当前的主键。如果 Key 为空,CurrentKey 由 RootKey 属性决定;如果 CanCreate 为 true,则当 Key 不存在时创建此键 ReadInteger int __fastcall ReadInteger(const AnsiString Name); 读取当前主键中名字为 Name 的键值。类似的还有 ReadBool、ReadBinaryData、 ReadCurrency、ReadDate、ReadDateTime、ReadFloat、ReadString、ReadTime 等 WriteInteger void __fastcall WriteInteger(const AnsiString Name, int Value); 将整形数据 Value 保存在 Name 键中。类似的还有 WriteBool、WriteBinaryData、 WriteCurrency、WriteDate、WriteDateTime、WriteFloat、WriteString、WriteTime 等 7.2 屏保程序的结构 介绍了屏保实现的关键技术之后,下面,我们通过一个图片收藏自动放映效果的屏保程 序演示屏保程序的结构,在之后的小节中我们就不再涉及屏保的技术,而着重讨论字幕、图 像等技巧。 7.2.1 窗体的设计 这里我们不讨论屏保密码相关的技术,而对其它三种模式,即小窗口预览、运行参数设 置和正常运行,可以分别建立三个窗体,然后根据入口参数决定启动哪个窗体。但是由于小 窗口中预览和正常运行的效果是一样的,只是窗口范围不同,所以我们只建立两个窗体,即 屏保正常运行时的窗体和屏保运行设置窗体。但是这样有一个问题,就是在小窗口中预览屏 保时,屏保仍然响应窗口范围内的鼠标和键盘事件而退出程序,解决也很简单,只需要根据 模式的不同,将窗体和 Image 组件对鼠标键盘的响应去掉就可以了,读者可以自己尝试实现, 本章不具体实现。 z 运行设置窗体 设置窗体 Name 为 ConfigForm。 如图 7-1,窗体中分隔线是使用的选项卡中 Additional 页中的 Bevel 组件,将其 Height 设为 2,指定合适的 Width 而得到的。 用选项卡中 Win32 页中的 TrackBar 组件来设置图片的显示频率和图片相对屏幕的大小, 设置 TrackBar 的 Height 为 25,TickStyle 设为 tsNone,TickMask 为 tmBoth。将两个 TrackBar 第 7 章 屏幕保护程序的制作 189 分别命名为 TrackBarFrequence 和 TrackBarPicSize,设置 TrackBarFrequence 的 Min 和 Max 属 性分别为 6 和 180,Position 为 6;设置 TrackBarPicSize 的 Min 和 Max 属性分别为 25 和 100, Position为90;设置 TrackBar上方的两个 Label的Name 分别为LabelFrequence和LabelPicSize。 用一个 Edit 组件(Name 为 EditPicDir)和 Button 组件(Name 为 BtnGetPicDir)来选择浏览 图片所在的文件夹。其中 Edit 组件的内容不能直接更改,只能通过“浏览”按钮选择目录更 改,所以设置 EditPicDir 的 Color 为 clBtnFace,设置其 Enabled 为 false。 图 7-1 屏保运行设置窗体 两个 BitBtn 按钮来确认或取消对运行设置的更改,设它们的 Name 分别为 BitBtnOK 和 BitBtnCancel,并为其 Glyph 属性指定合适的图片。 z 正常运行窗体 第 7 章 屏幕保护程序的制作 190 图 7-2 正常运行窗体 正常运行窗体 Name 为 MainForm,其中两个组件,一个定时器组件和一个 Image 组件, 其名字分别为 Timer1 和 Image1。设置窗体的 Color 为 clBlack。 7.2.2 命令行参数的处理 对命令行参数的处理放在 WinMain 函数中,打开“ViewUnit”窗口,选择 Screen(与工 程名字相同)打开 Screen.cpp,将其内容更改如下: //--------------------------------------------------------------------------- #include #pragma hdrstop //--------------------------------------------------------------------------- USEFORM("Unit1.cpp", MainForm); USEFORM("Unit2.cpp", ConfigForm); //--------------------------------------------------------------------------- WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR p, int) { //p 是指向命令行参数的指针 String StartType; //屏保运行的启动模式 AnsiString Command=p,temp; HWND FatherWindow =NULL; 第 7 章 屏幕保护程序的制作 191 if(Command=="") StartType = "/c"; //默认全屏模式 else StartType=Command.SubString(1,2);//获取命令行的前两个字符 try { Application->Initialize(); //---------------------------------------------------------------- if(StartType=="/c")//启动设置窗口 Application->CreateForm(__classid(TConfigForm), &ConfigForm); else if(StartType=="/s") //正常运行屏保 Application->CreateForm(__classid(TMainForm), &MainForm); else if(StartType=="/p") //将屏保在父窗体指定范围内运行 {//将 MainForm 显示在父窗口的指定区域 Application->CreateForm(__classid(TMainForm), &MainForm); //获得父窗体的句柄 FatherWindow=(HWND)(atol(ParamStr(2).c_str())); //获取窗口的风格 long style=GetWindowLong(Application->MainForm->Handle,GWL_STYLE); //在 Style 中添加子窗体属性 style=style|WS_CHILD; //设置 ManiForm 窗口为子窗口 SetWindowLong(Application->MainForm->Handle,GWL_STYLE,style); //设置屏保预览窗口为 MainForm 的父窗口 SetParent(Application->MainForm->Handle,FatherWindow); //获取屏保预览窗口的客户区 RECT PreviewRect; GetClientRect(FatherWindow,&PreviewRect); //将 MainForm 的窗口覆盖屏保预览窗口的客户区,并显示它 SetWindowPos(Application->MainForm->Handle,HWND_TOP,0,0, PreviewRect.right,PreviewRect.bottom , SWP_NOZORDER|SWP_NOACTIVATE|SWP_SHOWWINDOW); } //------------------------------------------------------------- Application->Run(); } catch (Exception &exception) 第 7 章 屏幕保护程序的制作 192 { Application->ShowException(&exception); } catch (...) { try { throw Exception(""); } catch (Exception &exception) { Application->ShowException(&exception); } } return 0; } //--------------------------------------------------------------------------- 7.2.3 运行设置的功能实现 因为要用到对注册表的操作,还有要使用目录选择对话框,所以需要在 include 中添加如 下内容: #include #include 另外,在头文件中加入全局变量如下: //--------------------------------------------------------------------------- public: // User declarations AnsiString PicDir; //文件目录 int Frequence; //图片更换频率 int PicSize; //图片的显示比例 //--------------------------------------------------------------------------- 程序中使用注册表存储运行设置信息,所以在更改设置时,需要先读取以前保存的设置, 此操作放在下面函数中: //--------------------------------------------------------------------------- __fastcall TConfigForm::TConfigForm(TComponent* Owner) : TForm(Owner) { //初始化运行参数 PicDir="NO"; 第 7 章 屏幕保护程序的制作 193 Frequence=6; PicSize=90; //从注册表得到以前的配置信息 TRegistry *Reg = new TRegistry; try { Reg->RootKey = HKEY_CURRENT_USER; if (Reg->OpenKey("\\Software\\MyScreenSaver", true)) { PicDir=Reg->ReadString("PicDir"); Frequence=Reg->ReadInteger("Frequence"); PicSize=Reg->ReadInteger("PicSize"); Reg->CloseKey(); } } __finally { delete Reg; } //初始化界面 TrackBarFrequence->Position=Frequence; TrackBarPicSize->Position=PicSize; EditPicDir->Text=PicDir; } //--------------------------------------------------------------------------- 两个 TrackBar 组件的改变需要同时改变两个 Label 的内容和全局变量 Frequence 和 PicSize 的值,响应代码如下: //--------------------------------------------------------------------------- void __fastcall TConfigForm::TrackBarFrequenceChange(TObject *Sender) { LabelFrequence->Caption=AnsiString("设置更新速率=")+ AnsiString(TrackBarFrequence->Position)+AnsiString("s"); Frequence=TrackBarFrequence->Position; } //--------------------------------------------------------------------------- void __fastcall TConfigForm::TrackBarPicSizeChange(TObject *Sender) { LabelPicSize->Caption=AnsiString("设置图片大小=屏幕的")+ AnsiString(TrackBarPicSize->Position)+AnsiString("%"); 第 7 章 屏幕保护程序的制作 194 PicSize=TrackBarPicSize->Position; } //--------------------------------------------------------------------------- 浏览目录的响应代码如下: //--------------------------------------------------------------------------- void __fastcall TConfigForm::BtnGetPicDirClick(TObject *Sender) { if(SelectDirectory("选择图片文件夹","",PicDir)) EditPicDir->Text=PicDir; } //--------------------------------------------------------------------------- 设置完毕,按下“确定”按钮,则将更改后的设置写入注册表,如果按“取消”,则直 接关闭窗体,不对注册表内容更改。代码如下: //--------------------------------------------------------------------------- void __fastcall TConfigForm::BitBtnCancelClick(TObject *Sender) { Close(); } //--------------------------------------------------------------------------- void __fastcall TConfigForm::BitBtnOKClick(TObject *Sender) { //把信息写入注册表 TRegistry *Reg = new TRegistry; try { Reg->RootKey = HKEY_CURRENT_USER; if (Reg->OpenKey("\\Software\\MyScreenSaver", true)) { Reg->WriteString("PicDir",PicDir); Reg->WriteInteger("Frequence",Frequence); Reg->WriteInteger("PicSize",PicSize); Reg->CloseKey(); } } __finally { delete Reg; } this->Close(); } 第 7 章 屏幕保护程序的制作 195 //--------------------------------------------------------------------------- 7.2.4 屏保正常运行的功能实现 首先在头文件中加入如下声明: //--------------------------------------------------------------------------- public: // User declarations __fastcall TMainForm(TComponent* Owner); String PicDir; //图片所在目录 int Frequence; //切换图片的时间间隔 int PicSize; //图片的大小相对与屏幕的比例 //此函数用于获得指定目录下的图片数目 jpeg,jpg,bmp 格式 int __fastcall GetPicNum(AnsiString Dir); //此函数用于在窗体中显示序号为 Num 的图片,如果显示成功,返回 true bool __fastcall ShowPicByIndex(AnsiString Dir,int Num); //此函数用于根据图片的绝对路径显示图片,图片的特效都可以放在这个函数中 //而对图片的随即选取,可以放在 ShowPicByIndex 函数中 bool __fastcall ShowPicByName(AnsiString PathName); //--------------------------------------------------------------------------- 当屏保开始运行时,首先要从注册表中读取运行设置信息,以及进行一些初始化的设置, 代码如下: //--------------------------------------------------------------------------- void __fastcall TMainForm::FormCreate(TObject *Sender) { //隐藏光标 Cursor=crNone; //打开双缓冲功能,使得图象显示不会闪烁 //Image组件没有 DoubleBuffer 属性.所以设置 Image 的容器 MainForm 的属性 MainForm->DoubleBuffered=true; //使窗口成为最顶层的窗口 SetWindowPos(this->Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE); //设置窗口覆盖屏幕 this->Width=Screen->Width; this->Height=Screen->Height; this->Top=0; this->Left=0; TRegistry *Reg = new TRegistry; try 第 7 章 屏幕保护程序的制作 196 { Reg->RootKey = HKEY_CURRENT_USER; if (Reg->OpenKey("\\Software\\MyScreenSaver", true)) { //得到图片目录 PicDir=Reg->ReadString("PicDir"); //得到图像显示的频率 Frequence=Reg->ReadInteger("Frequence"); //得到显示图象相对于整个屏幕的比例 //对读取的设置进行处理 PicSize=Reg->ReadInteger("PicSize"); Reg->CloseKey(); if(PicDir=="")PicDir="NO"; if(Frequence<6||Frequence>180)Frequence=6; Timer1->Interval=1000*Frequence; //设置定时器 } } __finally { delete Reg; } //由于定时器要在程序开始后一个时间间隔后才会触发,所以这里先执行一次 Timer1->OnTimer(this); } //--------------------------------------------------------------------------- 定时器的响应事件就是要显示图片,本程序中首先得到目录 PicDir 中的所有图片(JPG、 JPEG、BMP 格式)的数目,然后根据图片的序号循环的显示图片,具体实现代码如下: //--------------------------------------------------------------------------- void __fastcall TMainForm::Timer1Timer(TObject *Sender) { static Index=GetPicNum(PicDir); //静态变量,用于计数 if(Index < 1) Index=GetPicNum(PicDir); //放映一遍后再重头开始 if(!ShowPicByIndex(PicDir , Index))Close(); Index--; } //--------------------------------------------------------------------------- 其中 GetPicNum 返回指定目录中图片的数目,ShowPicByIndex 和 ShowPicByName 两个 函数根据图片的序号,或者绝对路径名在窗体中显示该图片,实现代码如下: 第 7 章 屏幕保护程序的制作 197 //--------------------------------------------------------------------------- int __fastcall TMainForm::GetPicNum(AnsiString Dir) {//搜索 CurrentDir 目录中的图象文件并记录它们的数目 TSearchRec sr; int PicNum = 0; if (FindFirst(Dir+"\\*.*", faAnyFile, sr) == 0) { do { if (ExtractFileExt(sr.Name).LowerCase() == ".jpeg" || ExtractFileExt(sr.Name).LowerCase() == ".jpg" || ExtractFileExt(sr.Name).LowerCase() == ".bmp") { PicNum++; } } while (FindNext(sr) == 0); FindClose(sr); } return PicNum; } //--------------------------------------------------------------------------- bool __fastcall TMainForm::ShowPicByIndex(AnsiString Dir , int Num) {//搜索 CurrentDir 目录中的图象文件并在搜索记录数目达到 Num 时,显示此图片 // 1<=Num<=图片数目 TSearchRec sr; int PicNum = 0; if (FindFirst(Dir+"\\*.*", faAnyFile, sr) == 0) { do { if (ExtractFileExt(sr.Name).LowerCase() == ".jpeg" || ExtractFileExt(sr.Name).LowerCase() == ".jpg" || ExtractFileExt(sr.Name).LowerCase() == ".bmp") { PicNum++; if(PicNum == Num) { //显示图片 return (ShowPicByName(Dir+"\\"+sr.Name)); } 第 7 章 屏幕保护程序的制作 198 } } while (FindNext(sr) == 0); FindClose(sr); } //此处返回,是没有找到指定序号的图片文件,图片显示失败 return false; } //--------------------------------------------------------------------------- bool __fastcall TMainForm::ShowPicByName(AnsiString PathName) { //为了放置图象操作中闪烁,所以对图象操作时把图象设为不可见 Image1->Visible=false; Image1->AutoSize=true; Image1->Picture->LoadFromFile(PathName); //Image1 的 AutoSize 属性为 true,所以此时可以由 Image1 组件的尺寸和显示器的尺寸 //再由拉伸比例 PicSize 来最终决定 Image1 的大小,由于 Image1 的 Stretch 属性为 true //这样,重新改变大小的 Image1 就可以使图象按显示器×一定比例来显示了 Image1->AutoSize=false; float Rate=Image1->Height/Image1->Width; if((Image1->Height / this->Height*PicSize/100) > (Image1->Width / this->Width*PicSize/100) ) { Image1->Height=this->Height*PicSize/100; Image1->Width=Image1->Height/Rate; } else { Image1->Width=this->Width*PicSize/100; Image1->Height=Image1->Width*Rate; } //设定图片的位置 Image1->Left=(this->Width - Image1->Width)/2; Image1->Top=(this->Height - Image1->Height)/2; Image1->Visible=true; return true; } //------------------------------------------------------ 屏保运行时,如果发生键盘事件或者鼠标事件,要退出屏保程序,所以对窗体、Image 组件的鼠标和键盘设置如下: //------------------------------------------------------ 第 7 章 屏幕保护程序的制作 199 void __fastcall TMainForm::FormClick(TObject *Sender) { Close(); } //--------------------------------------------------------------------------- void __fastcall TMainForm::Image1Click(TObject *Sender) { Close(); } //--------------------------------------------------------------------------- void __fastcall TMainForm::FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift) { Close(); } //--------------------------------------------------------------------------- void __fastcall TMainForm::FormMouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { Close(); } //--------------------------------------------------------------------------- void __fastcall TMainForm::Image1MouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { Close(); } //--------------------------------------------------------------------------- void __fastcall TMainForm::FormMouseMove(TObject *Sender, TShiftState Shift, int X, int Y) { static int MouseMoves=0; MouseMoves++; if(MouseMoves > 5) 第 7 章 屏幕保护程序的制作 200 { Close(); } } //--------------------------------------------------------------------------- void __fastcall TMainForm::Image1MouseMove(TObject *Sender, TShiftState Shift, int X, int Y) { static int MouseMoves=0; MouseMoves++; if(MouseMoves > 5) { Close(); } } //--------------------------------------------------------------------------- 图 7-3 屏保运行设置效果 7.2.5 屏保程序的运行效果 这个是一个图片收藏浏览的功能的屏保程序,所以其正常运行效果就是全屏显示图片, 这里不在给出其效果图,只给出选择屏保程序时的预览效果和运行参数的设置界面效果。 运行设置的效果如图 7-3,预览效果如图 7-4 所示。 第 7 章 屏幕保护程序的制作 201 7.3 字幕技巧 屏保实现的技术前面已经介绍的很清楚了,但是毕竟前面制作的屏保只是简单的浏览图 片,如果能加入字幕、图像的特效效果,那屏保就更完美了。 本节就介绍字幕相关的特效制作,后面的小节中还会介绍图像的特效等相关技巧。由于 这些特效只需要简单的放入屏保程序的显示函数中,即可得到屏保程序,所以在此之后的小 节中不在给出完整的屏保程序源码,而只给出实现特效的关键代码。 图 7-4 屏保预览效果 7.3.1 立体文字效果 利用 Image 组件 Canvas 的 TextOut 方法,实现立体文字是很容易的,将文字错位显示, 就可以得到立体阴影效果。如下面的代码: //--------------------------------------------------------------------------- 第 7 章 屏幕保护程序的制作 202 void __fastcall TForm1::Button1Click(TObject *Sender) { int Shadow=2,StartX=0,StartY=0; Image1->Canvas->Brush->Style=bsClear; Image1->Canvas->Font->Style<Canvas->Font->Size=25; Image1->Canvas->Font->Color=clBlack; Image1->Canvas->TextOutA(StartX+Shadow,StartY+Shadow,Edit1->Text); Image1->Canvas->Font->Color=clRed; Image1->Canvas->TextOutA(StartX,StartY,Edit1->Text); } //--------------------------------------------------------------------------- 注意其中一定要设 Brush->Style 为 bsClear,也就是透明显示底层的文字。立体文字效果 如图 7-5。 图 7-5 立体文字效果 7.3.2 旋转字体效果 利用 Canvas 提供的属性和方法,很难实现字体的旋转效果,这时我们考虑利用 GDI 函 数来实现。首先,需要获得 Canvas 的字体句柄,然后利用 GDI 函数对字体的 TLogFont 结构 进行更改,最后,利用 Canvas 的 TextOut 输出文字,即可实现自定义的旋转字体的输出。 不过需要注意的是,并不是所有的字体(Font->Name)都支持字体旋转,所以要对 Font->Name 赋合适的值。 关键代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::Button1Click(TObject *Sender) { 第 7 章 屏幕保护程序的制作 203 TLogFont LF; Image1->Canvas->Font->Name="Arial"; GetObject(Image1->Canvas->Font->Handle,sizeof(LF),&LF); //向下旋转 45 度,此值以 0.1 度为单位 LF.lfEscapement=-450; LF.lfOrientation=-450; LF.lfHeight=25; //背景透明 SetBkMode(Image1->Canvas->Handle,TRANSPARENT); Image1->Canvas->Font->Handle=CreateFontIndirect(&LF); Image1->Canvas->TextOutA(50,10,Edit1->Text); } //--------------------------------------------------------------------------- 旋转文字的效果如图 7-6。 图 7-6 旋转文字效果 7.4 图象处理技巧 7.4.1 图象的柔化 图象的柔化就是将图象尖锐的地方平滑掉,使得整个图象色彩的变化平缓,看上去比较 柔和。具体的,可以将某一点的象素颜色与周围点的象素相加再平均,然后赋给该点,这样 周围点的色彩就对这一点的色彩产生影响,使得图象中任何一点的色彩都不会与相邻的点之 间存在太大的差别。 第 7 章 屏幕保护程序的制作 204 实际的柔化中,可以参加平均的选择周围点的数目以及各个点的权重。这里介绍柔化的 原理,所以取固定的柔化点数和权重,取 3×3 的点格进行象素平均然后赋给中间一点,各 个点的权重相同。实现代码如下: 首先,在头文件添加如下代码: struct RGBPoint { int Red; int Green; int Blue; }; public: // User declarations RGBPoint Points[3000][3000]; 在窗体中添加 Image 组件和一个 BitBtn 组件,并为 Image1 的 Picture 属性指定一个位图。 实现柔化的关键代码放在 BitBtn 的 Click 事件中,代码如下: void __fastcall TForm1::BitBtn1Click(TObject *Sender) { TColor PointColor; int i,j; if(Image1->Height>3000 || Image1->Width>3000) { MessageDlg("图象太大,数组 Points 放不下!", mtInformation, TMsgDlgButtons() << mbOK, 0); return ; } //首先,将 Image1 的图片的所有象素值读入数组 for(i=0;iPicture->Width-1;i++) { for(j=0;jPicture->Height-1;j++) { PointColor=Image1->Canvas->Pixels[i][j]; Points[i][j].Red=GetRValue(PointColor); Points[i][j].Green=GetGValue(PointColor); Points[i][j].Blue=GetBValue(PointColor); } } //新建位图 Graphics::TBitmap *Bitmap; Bitmap=new Graphics::TBitmap; Bitmap->Width=Image1->Picture->Width; 第 7 章 屏幕保护程序的制作 205 Bitmap->Height=Image1->Picture->Height; //将柔化后的图象写入 Bitmap for(i=1;iPicture->Width-1;i++) { for(j=1;jPicture->Height-1;j++) { int r,g,b; r=Points[i-1][j-1].Red+Points[i][j-1].Red+Points[i+1][j-1].Red+ Points[i-1][j].Red+Points[i][j].Red+Points[i+1][j].Red+ Points[i-1][j+1].Red+Points[i][j+1].Red+Points[i+1][j+1].Red; r=r/9; g=Points[i-1][j-1].Green+Points[i][j-1].Green+Points[i+1][j-1].Green+ Points[i-1][j].Green+Points[i][j].Green+Points[i+1][j].Green+ Points[i-1][j+1].Green+Points[i][j+1].Green+Points[i+1][j+1].Green; g=g/9; b=Points[i-1][j-1].Blue+Points[i][j-1].Blue+Points[i+1][j-1].Blue+ Points[i-1][j].Blue+Points[i][j].Blue+Points[i+1][j].Blue+ Points[i-1][j+1].Blue+Points[i][j+1].Blue+Points[i+1][j+1].Blue; b=b/9; Bitmap->Canvas->Pixels[i][j]=TColor(RGB(r,g,b)); } } //将 Bitmap 显示在 Image2 中 Image2->Picture->Bitmap->Assign(Bitmap); } //--------------------------------------------------------------------------- 图象柔化的效果如图 7-7 所示。 第 7 章 屏幕保护程序的制作 206 图 7-7 图象柔化效果 由途中可以比较明显的看出柔化后的树枝和水波明显变的模糊了,也即达到了柔化的效 果。柔化后的图象周围有一圈白线,这是因为为了简洁,在代码中没有对边线赋值。对边线 上的点的柔化,可以将越界的数组元素用需要柔化的象素点自身来代替;或者另外一种方法, 将 Bitmap 的宽和高都设的比原图大两个象素,然后为边线象素指定特定的值,这样就可以不 加修改的用上面代码得到与原图同样大小的位图。请读者自己编写对边线的处理代码。 7.4.2 图象的锐化 图像的锐化处理正好与柔化处理相反,它的目的是突出图像的变化部分。我们可以采用 将要处理的像素与它左对角线的象素之间的差值乘上一个锐化度数的算法来实现(当然也可 以比较与其它相邻象素之间的差值),然后再加上原始的像素值: new_value=original_value+degree*difference,可以通过改变 degree 的值来调节锐化效果。 处理代码如下,其中需要注意的是得到的新的象素值有可能会超出颜色值的有效范围 (0-255),所以代码中要检验结果的有效性。 //--------------------------------------------------------------------------- void __fastcall TForm1::BitBtn1Click(TObject *Sender) { …… //与柔化相同 float degree=0.8 //锐化强度 ……//都原图象到数组等操作,与柔化的代码相同 第 7 章 屏幕保护程序的制作 207 for(i=1;iPicture->Width-1;i++) { for(j=1;jPicture->Height-1;j++) { int r,g,b; r=Points[i][j].Red+degree*(Points[i][j].Red-Points[i-1][j-1].Red); if(r>255)r=255; if(r<0)r=0; g=Points[i][j].Green+degree*(Points[i][j].Green-Points[i-1][j-1].Green); if(g>255)g=255; if(g<0)g=0; b=Points[i][j].Blue+degree*(Points[i][j].Blue-Points[i-1][j-1].Blue); if(b>255)b=255; if(b<0)b=0; Bitmap->Canvas->Pixels[i][j]=TColor(RGB(r,g,b)); } } Image2->Picture->Bitmap->Assign(Bitmap); } //--------------------------------------------------------------------------- 我们用柔化处理得到的位图进行锐化处理,效果如图 7-8 所示。 图 7-8 图象锐化效果 图中可以看出,锐化以后水波更加明显,树与云的边界也更加明显,天空蓝色部分比原 第 7 章 屏幕保护程序的制作 208 图象的蓝色更加浓。 7.4.3 浮雕效果 浮雕效果突出显示图象中颜色变化的边界部分,而颜色变化比较平缓的区域被淡化,从 而使得图象具有纵深感,具备浮雕的效果。 具体的算法是将每个象素点与处于对角线上的另一个像素间的差值作为处理后的图象 的新的像素值,这样一来只有颜色变化区才会出现色彩,而颜色平淡区因差值几乎为零而变 成黑色,你可以通过加上一个常量来增加一些亮度:new_value=difference+const_value,具体 代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::BitBtn1Click(TObject *Sender) { …… //与柔化代码相同 const int ConstValue=128; ……//与柔化效果的代码相同 for(i=0;iPicture->Width-1;i++) { for(j=0;jPicture->Height-1;j++) { int r,g,b; r=abs(Points[i][j].Red-Points[i+1][j+1].Red+ConstValue); if(r>255)r=255; g=abs(Points[i][j].Green-Points[i+1][j+1].Green+ConstValue); if(g>255)g=255; b=abs(Points[i][j].Blue-Points[i+1][j+1].Blue+ConstValue); if(b>255)b=255; Bitmap->Canvas->Pixels[i][j]=TColor(RGB(r,g,b)); } } Image2->Picture->Bitmap->Assign(Bitmap); } //--------------------------------------------------------------------------- 第 7 章 屏幕保护程序的制作 209 图 7-9 图象的浮雕效果 7.4.4 图象的旋转 图象的旋转实现代码如下: //--------------------------------------------------------------------------- float min(float x,float y) { if(x > y)return y; else return x; } float max(float x,float y) { if(x > y)return x; else return y; } //--------------------------- void RotatePic(Graphics::TBitmap *Source,Graphics::TBitmap *NewPic,int Angle) { if(Angle>180)Angle=360-Angle; if(Angle<-180)Angle=360+Angle; float Radians=(2*3.1416*Angle)/360; //转化为弧度 float CosAlpha=(float)cos(Radians); //旋转角度的余弦 第 7 章 屏幕保护程序的制作 210 float SinAlpha=(float)sin(Radians); //旋转角度的正弦 float Point1x=(-Source->Height*SinAlpha); float Point1y=(Source->Height*CosAlpha); float Point2x=(Source->Width*CosAlpha-Source->Height*SinAlpha); float Point2y=(Source->Height*CosAlpha+Source->Width*SinAlpha); float Point3x=(Source->Width*CosAlpha); float Point3y=(Source->Width*SinAlpha); float minx=min((float)0,min(Point1x,min(Point2x,Point3x))); float miny=min((float)0,min(Point1y,min(Point2y,Point3y))); float maxx=max(Point1x,max(Point2x,Point3x)); float maxy=max(Point1y,max(Point2y,Point3y)); int DestBitmapWidth,DestBitmapHeight; if(Angle>90&&Angle<180) DestBitmapWidth=(int)ceil(-minx); //不大于 minx 的最大整数 else DestBitmapWidth=(int)ceil(maxx-minx); if(Angle>-180&&Angle<-90) DestBitmapHeight=(int)ceil(-miny); else DestBitmapHeight=(int)ceil(maxy-miny); NewPic->Height=DestBitmapHeight; NewPic->Width=DestBitmapWidth; for(int x=0;x=0&&SrcBitmapxWidth&&SrcBitmapy>=0&& SrcBitmapyHeight) { NewPic->Canvas->Pixels[x][y]=Source->Canvas->Pixels[SrcBitmapx][SrcBitmapy]; } } } } //--------------------------------------------------------------------------- 第 7 章 屏幕保护程序的制作 211 void __fastcall TForm1::BitBtn1Click(TObject *Sender) { Graphics::TBitmap *Bitmap; Bitmap=new Graphics::TBitmap; RotatePic(Image1->Picture->Bitmap,Bitmap,45); Image2->Picture->Bitmap->Assign(Bitmap); } //--------------------------------------------------- 旋转图象的效果如图 7-10 所示。 图 7-10 图象的旋转效果 7.5 图象显示技巧 在网页、软件和游戏中,我们经常可以看到图像的各种特技显示,如百叶窗帘、左右推 拉、淡入淡出等,这种动感图像显示,往往给人一种赏心悦目的感觉。各种技巧的原理并不 复杂,在 C++ Builder 中,我们可以很容易地实现。 7.5.1 中心扩散效果 中心扩散效果,即图象由小到大,开始显示图象中心一个小区域,之后显示区域逐渐扩 大,直到整个图象被显示出来。实现代码如下: //--------------------------------------------------------------------------- 第 7 章 屏幕保护程序的制作 212 void __fastcall TForm1::CenterOut() { int i,left,top,width,height; left = Image1->Left; top = Image1->Top; width = Image1->Width; height = Image1->Height; for (i = 0; i <= width; i++) { Image1->Left = left + (width - i)/2; Image1->Top = top + height/2 - i*height/width/2; Image1->Width = i; Image1->Height = i*height/width; Image1->Refresh(); } } 其中,Image 组件的 Stretch 属性需要为 false,而且需要 Image 组件中的位图与 Image 组 件的大小是一样的。如果设置 Stretch 为 true,可以得到另外一种效果,即图象由小变大,它 与中心扩散不同的是不管图象范围多大,它显示的都是整幅图象。 7.5.2 百叶窗效果 百叶窗效果,就是将图象分成若干个竖格或者横格,然后对每一个格从原图象中逐个象 素列或行的拷贝,将拷贝后的图象显示在窗体中,直到显示完全,如图 7-11,分别是纵向和 横向的百叶窗效果: 图 7-11 百叶窗效果 第 7 章 屏幕保护程序的制作 213 建立两个 Image 组件,Image1 的Visable 设为 false,并为它的 Picture 指定一个位图;Image2 用来显示从 Image1 中逐列或逐行拷贝后的图形。 实现代码如下: //-------------------------------------------------------------------------- void __fastcall TForm1::BaiYeZongXiang() {//纵向百叶窗效果 Graphics::TBitmap *Bitmap = new Graphics::TBitmap(); Bitmap->Height = Image1->Height; Bitmap->Width = Image1->Width; int w=int((Image1->Picture->Bitmap->Width + 19)/20); //每页的宽度 for(int x=0;x<=w;x++) { for(int y=0;y<20;y++) { Bitmap->Canvas->CopyRect(Rect(y*w+x-1,0,y*w+x, Image1->Picture->Bitmap->Height),Image1->Picture->Bitmap->Canvas, Rect(y*w+x-1,0,y*w+x,Image1->Picture->Bitmap->Height)); } Image2->Picture->Bitmap->Assign(Bitmap); //暂停 ::Sleep(100); Application->ProcessMessages(); } delete Bitmap; } //-------------------------------------------------------------------------- void __fastcall TForm1::BaiYeHengXiang() {//横向百叶窗效果 Graphics::TBitmap *Bitmap = new Graphics::TBitmap(); Bitmap->Height = Image1->Height; Bitmap->Width = Image1->Width; int h=int((Image1->Picture->Bitmap->Height + 19)/20); //每页的宽度 for(int y=0;y<=h;y++) { for(int x=0;x<20;x++) { Bitmap->Canvas->CopyRect(Rect(0,x*h+y-1,Image1->Picture->Bitmap->Width, x*h+y),Image1->Picture->Bitmap->Canvas, Rect(0,x*h+y-1,Image1->Picture->Bitmap->Height,x*h+y)); } 第 7 章 屏幕保护程序的制作 214 Image2->Picture->Bitmap->Assign(Bitmap); //暂停 ::Sleep(100); Application->ProcessMessages(); } delete Bitmap; } //-------------------------------------------------------------------------- 7.5.3 推拉效果 推拉效果有很多种,如从下向上、从上向下、从左到右、从右到左,其原理都一样,我 们这里只列出实现从下向上推出图象效果的代码,其它请读者自己作为练习。 //-------------------------------------------------------------------------- void __fastcall TForm1::DownToUp() { int BitmapHeight=Image1->Picture->Bitmap->Height; int BitmapWidth=Image1->Picture->Bitmap->Width; for(int x=0;x<=BitmapHeight;x++) { Image2->Canvas->CopyRect(Rect(0,BitmapHeight-x,BitmapWidth,BitmapHeight), Image1->Canvas,Rect(0,0,BitmapWidth,x)); ::Sleep(100); Application->ProcessMessages(); } } //-------------------------------------------------------------------------- 7.5.4 交错效果 所谓交错效果,就是奇数条象素列(或行)和偶数条列(或行)分别从上方和下方(和 左方和右方)逐渐进入显示区。竖直交错和水平交错的原理都一样,这里只列出实现竖直交 错效果的代码: //-------------------------------------------------------------------------- void __fastcall TForm1::JiaoCuo() { int BitmapHeight=Image1->Picture->Bitmap->Height; int BitmapWidth=Image1->Picture->Bitmap->Width; 第 7 章 屏幕保护程序的制作 215 for(int x=0;x<=BitmapHeight;) { int y=x; while(y>0) { Image2->Canvas->CopyRect(Rect(0,y,BitmapWidth,y+1),Image1->Canvas, Rect(0,BitmapHeight-x+y,BitmapWidth,BitmapHeight-x+y+1)); Image2->Canvas->CopyRect(Rect(0,BitmapHeight-y,BitmapWidth, BitmapHeight-y+1),Image1->Canvas,Rect(0,x-y,BitmapWidth,x-y+1)); y-=2; } ::Sleep(50); Application->ProcessMessages(); //行间隔可以自己设,这里取 2,但是 x 增加的时候要注意可能增加 //之后大于 BitmapHeight,也就是在间隔为 2 时,需要考虑 BitmapHeight 的奇偶性 x+=2; if(x==BitmapHeight+1)x--; } } //-------------------------------------------------------------------------- 交错显示的效果如图 7-12 所示: 第 7 章 屏幕保护程序的制作 216 图 7-12 交错显示效果 7.5.5 雨滴效果 所谓雨滴效果,就是将原图象一行一行的从上而下的落下,并且留下一条痕迹,就像落 下的雨滴一样。代码如下: //-------------------------------------------------------------------------- void __fastcall TForm1::YuDi() { int BitmapHeight=Image1->Picture->Bitmap->Height; int BitmapWidth=Image1->Picture->Bitmap->Width; for(int i=BitmapHeight;i>0;i--) { for(int j=0;jCanvas->CopyRect(Rect(0,j-1,BitmapWidth,j),Image1->Canvas, Rect(0,i-1,BitmapWidth,i)); } ::Sleep(50); Application->ProcessMessages(); } 第 7 章 屏幕保护程序的制作 217 } //-------------------------------------------------------------------------- 雨滴效果如图 7-13 所示: 图 7-13 雨滴效果 7.6 思考题 z 一般程序在注册表的哪些部分可以写信息? z 如何实现图象的淡入淡出效果? z 如何实现图象的随即块效果? z 图象处理过程中如果出现闪烁现象怎么解决? 《C++ Builder 6 编程实例精解 赵明现》 第 08 章 多媒体播放器 本章重点 本章介绍多媒体技术的使用,并使用 TMediaPlayer 组件制作一个媒体播放器;另外,播 放器的界面完全由自己绘制,这种美化窗体界面的处理方法也是很重要的一部分工作。 学习目的 通过本章的学习,您可以: ■ 了解多媒体技术的概念 ■ 掌握 TMediaPlayer 组件的使用 ■ 掌握资源文件的建立和使用方法 ■ 掌握自定义窗体的实现 第 8 章 多媒体播放器 219 本章典型效果图 激活状态 失去激活状态 视频播放窗口 第 8 章 多媒体播放器 220 8.1 多媒体技术概述 多媒体(Multimedia)技术包括音频、视频、动画、图象与文本等技术,简单的说,多 媒体技术是以计算机为基础,以文字、声音、图片和图象等媒体作为信息来源,实现人机之 间交流的媒体。多媒体的应用几乎涉及电脑应用的各个方面,但它处理的无非是图、文、声、 象四类信息,下面对各个方面做简单的讨论。 8.1.1 音频与视频 典型的音频文件有波形音频(Waveform Andio)、CD Audio、MIDI(Musical Instrument Digital Interface,乐器数字接口)等。 Wav 文件是微软公司的标准音频文件格式,一般用于存储非音乐的声音,如说话的声音 等。波形音频有单声道和双声道之分,与单声道相比,双声道模拟立体的音频输入源,而且 其存储也会占用更多的空间。CD Audio 格式的 CD 光盘通常都可以直接在 CD-Rom 驱动器上 直接播放和控制,它是一种称为 Red Book 的音频标准。MDI 文件可以认为是包含了一系列 的音符,就像一个乐谱,然后这些音符和其它一些指令一起被送到音序合成器,再利用特定 乐器的声音将这些音符合成为声音。 一般来说,MIDI 文件占用空间最小,很适合作为程序的背景音乐,如仙剑的背景音乐 都是 MIDI 格式;而 CD Audio 占用空间一般最大,传输速率一般为 176k/s,但是其音乐再现 质量也最好。 视频是经由视频采集设备将影像数字化并存储在计算机中而得到的。视频的格式常见的 有 MPEG 格式、微软公司的 VFW(Video for Windows)格式、苹果的 QuichTime、DVI 等。 8.1.2 动画、图象与文本 动画就是一系列有连续动作的图形图象,并且一般都伴随着与之同步的音频。按实现原 理区分,动画有两种类型,即基于对象的动画和基于帧的动画。基于帧的动画就像电影胶片 一样是由一个个紧密相连的画面帧构成,通过一帧一帧快速的播放来实现动画;基于对象的 动画通常又称为精灵动画,它里面将图画中的物体当作一个对象,每个对象具有自己的大小、 颜色、速度、形状等属性,通过掩图等技术根据对象自身的属性对之进行移动和变形等操作, 从而实现动画效果。 图象有两种基本格式,位图和矢量图。位图的基本单位是象素,每个象素具有自己的 RGB 颜色属性,很多的象素构成一副完整的图象;矢量图中不包含象素信息,而是包含一系列由 图象内容决定的画图指令,这些指令被系统解释并执行,最后画出图象。一般矢量图占用的 空间比较小,而且矢量图在改变图象大小的时候不容易失真。 在多媒体应用中,字幕也是很常用的。如视频播放时加入透明的字幕,动画、图象中的 字幕使用也都很频繁。 第 8 章 多媒体播放器 221 8.1.3 媒体控制接口(MCI) Windows 操作系统系统控制媒体设备的媒体控制接口 MCI 以及支持多媒体相关服务的 底层 API。MCI 一个与硬件设备无关的接口,用于协调多媒体软件与 MCI 设备驱动程序之间 的通信。 MCI 的编程接口有两种,即命令串(Command-String )接口和命令消息 (Command-Message)接口,前者通过发送字符串形式的命令来控制多媒体设备,而后者发 送消息和数据结构,并且通过此接口接受设备返回的信息。 在 C++Builder 中可以 mciSendString 函数向多媒体设备发送字符串形式的命令,从而控 制媒体设备。其中 mciSendString 函数在 mmsystem.h 中的声明如下: MCIERROR WINAPI mciSendString(LPCSTR lpstrCommand, LPSTR lpstrReturnString, UINT uReturnLength, HWND hwndCallback); 其中第一个参数是字符串形式的控制命令,第二个参数指向一个字符指针,用于存放设 备返回的消息。 命令消息函数 mciSendCommand 的声明如下: MCIERROR WINAPI mciSendCommand(MCIDEVICEID mciId, UINT uMsg, DWORD dwParam1, DWORD dwParam2); MCI 接口提供了丰富的功能,而且在对多媒体高级复杂功能的实现上,MCI 的使用是不 可缺少的。 8.2 TMediaPlayer 组件的使用 C++Builder 中提供的多媒体组件主要有图象组件 TImage、动画组件 TAnimate 以及媒体 播放机组件 TMediaPlayer。Image 组件的使用在以前章节中都有详细的介绍,这里不再讲述。 本章主要使用多媒体播放组件,所以这里只介绍 TMediaPlayer。 对于 TMediaPlayer 组件,在它里面封装了媒体无关的 MCI 接口的大部分功能,因此使 用 TMediaPlayer 组件可以很轻松的实现多媒体的应用。在 TMediaPlayer 组件中主要提供对 CD 音频、Wav 音频、MIDI 音序以及 AVI、动画等文件的支持,主要用于 CD 播放、视频 AVI 播放、动画、Midi 音乐播放、Wav 声音录制等。 图 8-1 TMediaPlayer 组件 如图 8-1,TMediaPlayer 组件包含有一套按钮(播放按钮、停止按钮、弹出按钮等), TMediaPlayer 组件正是通过这些按钮来控制 CD-ROM、MIDI 音序器、录音机等多媒体设备 的。这些按钮是成套的,不能单独分离出来使用。在程序运行过程中,当用户用鼠标点击按 钮时,TMediaPlayer 将返回一个可以标志此按钮的值,然后可以在程序中根据此值对用户操 作进行响应。各个按钮对应的返回值和其功能如下表: 第 8 章 多媒体播放器 222 表 8-1 TMediaPlayer 组件按钮 按钮 返回值 功能 Play btPlay 播放 Pause btPause 暂停播放或录音。如果单击时已经暂停,则恢复播放或录音 Stop btStop 停止播放或录音 Next btNext 跳到下一个音轨,如果媒体不使用音轨,跳到文件末尾 Prev btPrev 同上,跳到上一个音轨或文件开头 Step btStep 向前移动几帧 Back btBack 向后移动几帧 Record btRecord 开始录音 Eject btEject 弹出(如 CD-ROM,VCD 等) TMediaPlayer 组件的主要属性列表如下: 表 8-2 TMediaPlayer 主要属性 属性 说明 AutoEnable 决定是否自动控制按钮的 Enabled 属性。如果为 true,则 TMediaPlayer 组件 的按钮会根据播放的媒体以及当前的状态自动设置按钮的 Enabled 属性,如 处于 Play 状态时,Play 和 Record 按钮会自动失效。如果 AutoEnable 为 false, 那么所有按钮的状态都必须在程序中通过 EnabledButtons 属性来更改 AutoOpen 决定是否自动打开设备,即在程序开始执行时,是否自动使媒体设备处于打 开状态。此属性为 true 时,程序自动打开由 DeviceType 属性决定的多媒体 设备,如果 DeviceType 为 dtAutoSelect,则由 FileName 属性决定媒体设备 AutoRewind 设置媒体播放机是否自动复位 Capabilities 所开启的媒体设备的功能。只读型集合元素,取值有 mpCanEject、mpCanPlay、 mpCanRecord 、 mpCanStep 、 mpUsesWindow 。其中 mpCanStep 仅用于 Animation, AVI Video, Digital Video, Overlay, or VCR 媒体时 ColoredButtons 决定组件中哪些按钮使用彩色显示,默认所有按钮均用彩色 DeviceType 标记 Open 方法打开的媒体设备的类型,可取值有 dtAutoSelect, dtAVIVideo, dtCDAudio, dtDAT, dtDigitalVideo, dtMMMovie, dtOther, dtOverlay, dtScanner, dtSequencer, dtVCR, dtVideodisc, dtWaveAudio,如果取 dtAutoSelect 则根据所 打开文件的扩展名自动选择 Display 指定视频文件(如 AVI、Digital Video、Animation 等)的输出显示窗口。作 为输出窗口的可以是 Form 或 Panel 等 TWinControl 类型的组件。如要设定 Panel1 为播放窗口,则用代码:MediaPlayer1->Display = Panel1; DisplayRect 设定 Display 指定的显示窗体中显示输出的区域,此属性只有在媒体设备被 打开时才能被设定。 第 8 章 多媒体播放器 223 EnabledButtons 控制组件中哪些按钮可用 EndPos 播放或录制时终点的时间位置 FileName 指定用于存储或打开的媒体文件的文件名,如果媒体类型为 CDAudio, FileName 属性必须为 NULL,否则会出现错误 Frames 向前或向后 Step 操作时跳过的帧数,默认为媒体长度(Length)的十分之一 Length 打开的媒体设备的长度 Mode 当前打开媒体的状态,取值有:mpNotReady, mpStopped, mpPlaying, mpRecording, mpSeeking, mpPaused, mpOpen Notify 决定下一个向 MCI 发送的控制动作完成时是否触发一个 OnNotify 事件。一 般,如果下一个操作将费时较多,就设 Notify 为 true,这样程序中可以通过 OnNotify 事件判断操作是否结束 NotifyValue 储存需要返回消息的控制动作(Notify 为 true)的返回消息。取值有 nvSuccessful(控制命令执行成功), nvSuperseded(命令被另一个命令取代), nvAborted(命令被用户取消), nvFailure(命令失败) Position 媒体当前的时间位置 Shareable 决定是否可以与其它程序共用媒体 Start 当前打开媒体的起始位置 TimeFormat 媒体位置所用的时间格式 VisibleButtons 决定组件中哪些按钮可见 Wait 决定是否等待控制命令执行完毕才返回 TMediaPlayer 组件除了提供与组件按钮对应的 Play、Pause、Stop、Next、Previous、Back、 Step、Record、Eject 等方法之外,还提供了其它一些方法,常用的有: Open 用于打开一个媒体设备 Close 用于关闭一个媒体设备 Resume 用于在上一个暂停位置继续播放 PauseOnly 暂停,如果当前是暂停状态,则继续保持暂停 StartRecording 开始录音 Rewind 复位 Save 保存媒体到文件 KeyDown 提供对箭头键和空格键的响应 TMediaPlayer 组件常用的几个事件有: OnClick 事件: 在鼠标点击 TMediaPlayer 组件的按钮时,或者在组件拥有焦点时按空格键时触发。拥有 焦点时,可以通过箭头键或者空格键按下那个按钮。 OnNotify 事件: 在一个发出的媒体控制动作结束时触发,而且,要触发此事件,在控制动作发出之前 Notify 属性必须为 true。在 OnNotify 之后要发出另一个需要触发 OnNotify 事件的控制动作时, 需要将 Notify 属性重设为 true。 OnPostClick 事件: 第 8 章 多媒体播放器 224 在对 OnClick 事件的响应代码被调用以后,触发 OnPostClick 事件。如果 MediaPlayer 组 件的按钮被点击时 Wait 属性为 true,则直到 OnClick 事件的响应代码执行结束时才触发 OnPostClick 事件,而如果 Wait 属性为 false,那么在 OnClick 的响应代码执行结束之前,控 制权就返回给应用程序,这样,就有可能在 OnClick 的相应代码执行结束之前触发 OnPostClick 事件。 8.3 程序界面设计 本章设计的多媒体播放器是以 8.2 节中介绍的 TMediaPlayer 组件为基础的。但是,如果 要设计一个漂亮的播放器,C++Builder 提供的 TMediaPlayer 的界面实在让人不敢恭维。那么, 是不是使用漂亮的界面就不能使用 TMediaPlayer 组件了呢?实际上,我们完全可以这样做, 就是将 TMediaPlayer 的 Visible 属性设为 false,然后制作自己喜欢的漂亮界面,再在界面的 合适组件的事件中调用 TMediaPlayer 相应事件(如 Play 按钮的点击事件等)。如此以来,即 有了自己喜欢的界面,又利用了 TMediaPlayer 的强大功能。 多媒体播放器的主界面设计如图 8-2: 图 8-2 MyPlayer 多媒体播放器主界面设计图 由于组件都比较小,而且相当密集,又为了将不可见组件和其它组件区分看,方便读者 观看,所以将设计窗体拉大了点。我们要绘制比较美观的程序界面,所以窗体中大多都是 TImage 组件。窗体设计细节很多,所以本节分布详细介绍如何设置各个组件的属性。 z 第一步 设置主窗体 Name 为 FormMain,BorderStyle 为 bsNone,BorderWidth 为 0,调整其大小 使得 ClientHeight 为 112,ClientWidth 为 272。当然窗口的大小可以更大和更小都无所谓,但 是一定要和其它组件相协调。 图 8-2 中只列出了主窗体,而如果是视频文件的话,需要新建一个窗体显示视频,所以 在工程中新建窗体,命名为 FormVideo。由于视频显示窗体上没有添加任何组件,所以这里 不再列出其设计图。设置视频窗体的 BorderStyle 为 bsSizeToolWin。大小可以随便,因为打 第 8 章 多媒体播放器 225 开视频的时候可能要根据视频的实际大小调整窗体,而且,视频打开之后也要允许用户更改 窗体的大小的。 z 第二步 在主窗体中添加一个 TShape 组件,设置其 Name 属性为 ShapeClient;Align 属性为 alClient;Brush 属性的填充模式 Style 为 bsClear,即透明;Pen 属性的颜色 Color 为 clGreen, Width 为 1,其实这个不设置也没关系,因为程序中绘制界面的地方也会设置;Shape 属性为 stRectangle。 这个 Shape 组件其实没有什么用,在程序中只是画界面的边界,也就是在窗体四周绘制 绿色的一条线。而其填充填充模式为 bsClear,是为了不掩盖到其它组件。 z 第三步 为窗体添加 TImage 组件,如图 8-2,窗体中有 18 个 TImage 组件,其属性设置、用途各 不相同,为了清楚明了的说明它们的属性设置,列表如下: 表 8-3 主窗体中 TImage 组件设置及说明 Name 属性 取值 功能说明 AutoSize false Height 18 Left 1 Top 1 Width 270 ImageTitle Stretch true 此 Image 位于标题栏下方,与标题栏等宽。 用来作为程序主界面的标题栏,在它上面放置 四个 Image 分别作为系统按钮,一个 Label 作 为窗体标题。它的 Stretch 根据需要设置,本程 序中标题栏的底图大小与 ImageTitle 相同,所 以 Stretch 设为 false 也没关系。AutoSize 属性 也是这样,可以根据底图设置。 以后的 Image 就不再列出这两个属性了。 Height 12 Left 4 Top 4 Width 12 ImageSystem Tag 100 此 Image 位于 ImageTitle 上的左侧。 它用来做为标题栏左侧系统菜单的按钮,双击 此 Image,将关闭应用程序。 Height 12 Left 231 Top 4 Width 12 ImageMinimize Tag 101 Height 12 Left 244 Top 4 Width 12 ImageZoom Tag 102 ImageMinimize 与 ImageZoom 和 ImageClose 均在 ImageTitle 上的右侧。 它们载入图片之后作为最小化、卷起、关闭按 钮。其中所谓的卷起,就是让窗体的“客户区” (之所以加引号是因为其实所有的 Image 都在 主窗体的客户区,这里指的是自己绘制的模拟 的客户区)收起,而只剩下标题栏。 需要特别注意的是它们的 Tag 属性,为它们设 置不同的 Tag,是为了在为它们指定相同的事 件响应函数之后,根据参数 Sender 的 Tag 值来 第 8 章 多媒体播放器 226 Height 12 Left 257 Top 4 Width 12 ImageClose Tag 103 判断调用函数的是哪个组件。 Height 28 Left 11 Top 22 ImageTime1 Width 22 Height 28 Left 32 Top 22 ImageTime2 Width 22 Height 28 Left 53 Top 22 ImageTime3 Width 10 Height 28 Left 62 Top 22 ImageTime4 Width 22 Height 28 Left 83 Top 22 ImageTime5 Width 22 这五个 Image 靠近 ImageTitle 的下方和窗体的 左侧。 本章所做的程序中它们是用来显示时间的,也 就是已经播放的媒体文件的长度。当然也可以 设置为根据用户的选择显示剩下的媒体文件 的播放时间,读者可以作为练习为程序添加这 个功能:当用户用鼠标单击这几个显示时间的 Image 时,自动在显示已经逝去的时间和显示 剩余时间之间切换。 五个 Image 中,除了 ImageTime3 之外都是显 示数字的,ImageTime3 显示分钟和秒之间的冒 号。 当然本程序中没有留下显示小时的位置,因为 一般的小文件不会太长,另一方面,也是为了 讲解时方便。读者可以自己添加。 Height 12 Left 1 Top 70 ImageTrack Width 270 此 Image 位于 ImageControlBar 上方,与窗体 基本等宽,但是两头分别比 ShapeClient 的宽度 少一个象素,为的是不要覆盖 ShapeClient 绘制 的模拟的窗体边界线。 它用来显示播放媒体的位置,通过鼠标点击可 以选择媒体播放的位置。 Height 30 Left 1 Top 80 ImageControlBar Width 270 此 Image 位于 ImageTrack 的下方,程序中的控 制按钮(实际上不是按钮,而是 Image)都在 它上面 Height 15 Left 11 ImagePre Top 89 这六个按钮依次排列在 ImageControlBar 上。 它们分别对应 TMediaPlayer 组件上的 btPrev、 btPlay、btPause、btStop、btNext 和 btOpen 按 第 8 章 多媒体播放器 227 Width 15 Tag 200 Height 15 Left 35 Top 89 Width 15 ImagePlay Tag 201 Height 15 Left 59 Top 89 Width 15 ImagePause Tag 202 Height 15 Left 83 Top 89 Width 15 ImageStop Tag 203 Height 15 Left 107 Top 89 Width 15 ImageNext Tag 204 Height 15 Left 131 Top 89 Width 15 ImageOpen Tag 205 钮,其中 ImageOpen 与 btOpen 有区别,因为 ImageOpen 用来弹出打开文件对话窗,供用户 选择需要打开的媒体文件。 与标题栏上的按钮一样,对它们也设置不同的 Tag 属性,然后为它们指定相同的鼠标事件函 数,在函数中根据 Sender 的 Tag 属性用 switch 语句来区分不同的按钮对应的响应代码。 z 第四步 在 ImageTitle 上添加一个 Label,命名为 LabelTitle,设置其 Caption 为“MyPlayer 媒体 播放器”,其 Height 为 13,Left 为 22,Top 为 3。程序中用它来显示模拟窗体的标题栏。 再添加一个 Label,命名为 LabelFileName,设置其 Height 为 13,Left 为 118,Top 为 25。 程序运行时用来显示打开的文件名。 z 第五步 为窗体添加 TMediaPlayer 组件,并设置其 Visible 属性为 false,即不可见。添加定时器 组件,设置其 Enabled 属性为 false。添加一个打开文件对话窗组件(OpenDialog)。 这三个不可见组件的 Name 分别为 MediaPlayer1、Timer1 和 OpenDialog1。 到现在为止,窗体的设计工作结束,至于如何用上面添加的组件绘制窗体,在下面的小 第 8 章 多媒体播放器 228 节中会详细讲解。 8.4 资源文件的使用 在编写程序的过程中,经常要用到各种资源。比如说,想给程序换个自己的图标,使用 一些活泼的光标来增加趣味性,播放一些声音和动画文件,或者使用别人写好的程序来实现 某些功能。我们往往希望将这些资源直接放到 EXE 文件中以形成一个单独的可执行文件,这 就存在一个如何在运行时访问和使用这些资源的问题。 在这里,我们以多媒体播放器中需要的资源为例,说明建立和使用资源的具体过程。 8.4.1 .rc 文件的建立 .rc 是资源说明文件,它是文本格式,所以可以新建一个 txt 文件,然后将文件后缀改为.rc 即可。对它的编辑也可以用记事本编辑,当然在 C++Builder 中也可以编辑。 .rc 文件的格式很简单,每一行说明一个资源,一行中有三列,第一列为资源的标志名, 第二列为资源的类型,第三列为资源的文件名。如下列代码: srcfile.rc: WAV_RING WAV wav1.Wav CURSOR_HAND CURSOR cursor1.cur ICON_CLOUD ICON icon1.ico EXE_FILE EXEFILE faint.exe 其中第一行定义的是声音文件,第二行是一个光标文件,第三行是图标文件,第四行是一个 exe 可执行文件。 对于本章制作的多媒体播放器,用到的众多图片都采用资源文件的方式加入程序,资源 说明文件代码如下: MyRes.rc: TITLE_ACTIVE BITMAP bk_wood.bmp TITLE_INACTIVE BITMAP bk_wood_inactive.bmp SYSTEM_ACITVE BITMAP mSystem.bmp SYSTEM_INACTIVE BITMAP mSystem_inactive.bmp CLOSE_ACTIVE BITMAP mClose.bmp MINIMIZE_ACTIVE BITMAP mMinimize.bmp ZOOM_ACTIVE BITMAP mZoom.bmp _INACTIVE BITMAP _inactive.bmp FORM_BACKGROUND BITMAP title_active.bmp CONTROLBAR_BACKGROUND BITMAP controlbar_bk.bmp CONTROLBAR_PRE BITMAP control_pre.bmp CONTROLBAR_PLAY BITMAP control_play.bmp 第 8 章 多媒体播放器 229 CONTROLBAR_PAUSE BITMAP control_pause.bmp CONTROLBAR_STOP BITMAP control_stop.bmp CONTROLBAR_NEXT BITMAP control_next.bmp CONTROLBAR_OPEN BITMAP control_open.bmp TIME_0 BITMAP num0.bmp TIME_1 BITMAP num1.bmp TIME_2 BITMAP num2.bmp TIME_3 BITMAP num3.bmp TIME_4 BITMAP num4.bmp TIME_5 BITMAP num5.bmp TIME_6 BITMAP num6.bmp TIME_7 BITMAP num7.bmp TIME_8 BITMAP num8.bmp TIME_9 BITMAP num9.bmp TIME_DOT BITMAP numdot.bmp .rc 文件写好之后,有两种方法加入工程,简单方便的方法就是打开菜单“Project”->“Add to Project”(Shift + F11),选择编写好的.rc 文件,添加到工程即可。C++Builder 会自动将.rc 文件编译为供程序使用的二进制数据.res 文件。编译完成以后,只有当在 C++Builder 中更改 了.rc 文件的内容后,.rc 文件才会重新编译。所以如果自己更改了文件资源(如重新编辑了 图片),只需要在 C++Builder 中使得.rc 文件处于被修改状态(如按下一个字符再删除之), 重新编译程序的时候就会自动重新编译资源文件。 另外一种编译资源文件的方法是自己手动编译。使用的编译工具是 C++Builder 程序目录 bin 中的 brcc32.exe 程序。在命令行模式下,输入 brcc32 /? 可以查看其使用帮助。一般使用 默认选项,也就是在命令行中输入 brcc32 YourRcFileName.rc 即可生成同名的.res 文件。编 译好的.res 文件要在程序中使用,还需要在源文件中添加代码如下: #pragma resource "myres.res" 其中 myres.res 为编辑生成的资源文件的文件名。 资源文件添加之后,就是怎么使用它们的问题了。对于不同类型的文件,使用方法也不 同,下面分别介绍各种类型文件的具体用法。 8.4.2 可以通过 Windows API 函数直接访问的资源 可以直接通过 API 函数访问的资源类型以及对应的 API 函数如下: 加速表 LoadAccelerators(HINSTANCE hInstance,LPCTSTR lpTableName) 位图 LoadBitmap(HINSTANCE hInstance,LPCTSTR lpTableName) 光标 LoadCursor(HINSTANCE hInstance,LPCTSTR lpTableName) 图标 LoadIcon(HINSTANCE hInstance, LPCTSTR lpTableName) 菜单 LoadMenu(HINSTANCE hInstance,LPCTSTR lpTableName) 字符串 LoadString(HINSTANCE hInstance,UINT uID,LPTSTR lpBuffer,int nBufferMax) 第 8 章 多媒体播放器 230 前五个函数的参数相同,其中第一个标示资源的存放的,也就是资源文件的句柄;第二个参 数指定资源文件中的标志名,也就是.rc 文件中第一列的内容。字符串资源的使用函数比其它 函数多了两个参数,它们分别指定字符串缓冲区的2018香港马会开奖现场和大小。 本章制作的多媒体播放程序中,由于仅使用到位图资源,所以资源文件都可以用 API 函 数直接访问。在主窗体的头文件“Main.h”中加入下面内容: private: // User declarations // == 界面代码 ======================================= Graphics::TBitmap *bmpTitleAct; Graphics::TBitmap *bmpTitleInact; Graphics::TBitmap *bmpSysAct; Graphics::TBitmap *bmpSysInact; Graphics::TBitmap *bmpClose; Graphics::TBitmap *bmpMinimize; Graphics::TBitmap *bmpZoom; Graphics::TBitmap *bmp_Inact; //标题栏 Graphics::TBitmap *bmpFormBK; Graphics::TBitmap *bmpControlBarBK; //背景 Graphics::TBitmap *bmpControlPre; Graphics::TBitmap *bmpControlPlay; Graphics::TBitmap *bmpControlPause; Graphics::TBitmap *bmpControlStop; Graphics::TBitmap *bmpControlNext; Graphics::TBitmap *bmpControlOpen; //控制按钮 Graphics::TBitmap *bmpTime0; Graphics::TBitmap *bmpTime1; Graphics::TBitmap *bmpTime2; Graphics::TBitmap *bmpTime3; Graphics::TBitmap *bmpTime4; Graphics::TBitmap *bmpTime5; Graphics::TBitmap *bmpTime6; Graphics::TBitmap *bmpTime7; Graphics::TBitmap *bmpTime8; Graphics::TBitmap *bmpTime9; Graphics::TBitmap *bmpTimeDot; //时间显示用到的数字图片 void __fastcall LoadBitmaps(); //从资源文件中读取图片 void __fastcall FreeBitmaps(); //程序结束时,释放资源 //--------------------------------------------------------------------------------- 其中 LoadBitmaps()和 FreeBitmaps()函数代码如下: //--------------------------------------------------------------------------- 第 8 章 多媒体播放器 231 void __fastcall TFormMain::LoadBitmaps() { HBITMAP h; // 窗体激活时的标题栏背景 bmpTitleAct = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"TITLE_ACTIVE"); bmpTitleAct->Handle = h; //窗体不处于激活状态时的标题栏背景 bmpTitleInact = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"TITLE_INACTIVE"); bmpTitleInact->Handle = h; // Active System Button bmpSysAct = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"SYSTEM_ACITVE"); bmpSysAct->Handle = h; // Inactive System Button bmpSysInact = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"SYSTEM_INACTIVE"); bmpSysInact->Handle = h; // 关闭按钮 bmpClose = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"CLOSE_ACTIVE"); bmpClose->Handle = h; // 最小化按钮 bmpMinimize = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"MINIMIZE_ACTIVE"); bmpMinimize->Handle = h; // 卷起按钮 bmpZoom = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"ZOOM_ACTIVE"); bmpZoom->Handle = h; // Inactive Button bmp_Inact = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"_INACTIVE"); bmp_Inact->Handle = h; // 窗体背景 bmpFormBK = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"FORM_BACKGROUND"); bmpFormBK->Handle = h; // 控制栏背景 第 8 章 多媒体播放器 232 bmpControlBarBK = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"CONTROLBAR_BACKGROUND"); bmpControlBarBK->Handle = h; //绘制控制栏按钮 Btn Pre bmpControlPre = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"CONTROLBAR_PRE"); bmpControlPre->Handle = h; //Button Play bmpControlPlay = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"CONTROLBAR_PLAY"); bmpControlPlay->Handle = h; //Button Pause bmpControlPause = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"CONTROLBAR_PAUSE"); bmpControlPause->Handle = h; //Button Stop bmpControlStop = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"CONTROLBAR_STOP"); bmpControlStop->Handle = h; //Button Next bmpControlNext = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"CONTROLBAR_NEXT"); bmpControlNext->Handle = h; //Button Open bmpControlOpen = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"CONTROLBAR_OPEN"); bmpControlOpen->Handle = h; //LED数字位图 num 0 bmpTime0 = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"TIME_0"); bmpTime0->Handle = h; // num 1 bmpTime1 = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"TIME_1"); bmpTime1->Handle = h; // num 2 bmpTime2 = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"TIME_2"); bmpTime2->Handle = h; // num 1 第 8 章 多媒体播放器 233 bmpTime3 = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"TIME_3"); bmpTime3->Handle = h; // num 4 bmpTime4 = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"TIME_4"); bmpTime4->Handle = h; // num 5 bmpTime5 = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"TIME_5"); bmpTime5->Handle = h; // num 6 bmpTime6 = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"TIME_6"); bmpTime6->Handle = h; // num 7 bmpTime7 = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"TIME_7"); bmpTime7->Handle = h; // num 8 bmpTime8 = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"TIME_8"); bmpTime8->Handle = h; // num 9 bmpTime9 = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"TIME_9"); bmpTime9->Handle = h; // Dot bmpTimeDot = new Graphics::TBitmap(); h = LoadBitmap(HInstance,"TIME_DOT"); bmpTimeDot->Handle = h; } void __fastcall TFormMain::FreeBitmaps() { delete bmpTitleAct; delete bmpTitleInact; delete bmpSysAct; delete bmpSysInact; delete bmpClose; 第 8 章 多媒体播放器 234 delete bmpMinimize; delete bmpZoom; delete bmp_Inact; delete bmpFormBK; delete bmpControlBarBK; delete bmpControlPre; delete bmpControlPlay; delete bmpControlPause; delete bmpControlStop; delete bmpControlNext; delete bmpControlOpen; delete bmpTime0; delete bmpTime1; delete bmpTime2; delete bmpTime3; delete bmpTime4; delete bmpTime5; delete bmpTime6; delete bmpTime7; delete bmpTime8; delete bmpTime9; delete bmpTimeDot; } // ---------------------------------------------------------------- 资源的释放放在窗体关闭时,所以窗体销毁事件的响应代码如下: //--------------------------------------------------------------------------- void __fastcall TFormMain::FormDestroy(TObject *Sender) { FreeBitmaps(); } //--------------------------------------------------------------------------- 8.4.3 能通过 API 使用的资源 对于声音,动画资源,使用 API 函数也可以访问,但是它与位图、光标等资源不同的是, 对它们的访问需要比较严格的格式。需要依次使用 FindResource、LoadResource 和 LockResource,如下面代码,播放资源中的一个声音文件: void __fastcall TForm1::Button2Click(TObject *Sender) 第 8 章 多媒体播放器 235 { //定义资源块 char *wav_handle ; //装入 Wav 文件 ,第二个参数为文件名,第三个参数为文件类型 HRSRC h = FindResource(HInstance," WAV_RING ","WAV"); HGLOBAL h1 = LoadResource(HInstance, h); wav_handle = (char *)LockResource(h1); //播放 Wav 文件。由于 Wav 文件装载在内存中,sndPlaySound 函数要用 SND_MEMORY 参 数 sndPlaySound(wav_handle, SND_MEMORY | SND_SYNC); } 8.4.4 不能通过 API 直接使用的资源 对于外部可执行文件,在程序中可以通过 shell 函数 ShellExecute 来执行它,但是对于资 源文件中的 exe 可执行文件,却不能这么做。虽然资源文件中的 exe 文件不能被程序直接调 用,但是,我们可以先从资源文件中把 exe 文件分离出来,保存在硬盘中,然后通过 ShellExecute 来执行它。 假设资源文件中有一个名字叫 MYEXE 的文件,我们需要将它从当前运行的程序中分离 出来放在系统临时目录中,首先,需要判断临时目录路径+”Myexe.exe”(欲生成的文件名) 是否已经存在,如果不存在,则用该文件名将 exe 文件保存在临时目录。判断文件是否存在 的代码如下: char exefile[100], tmppath[100] ; GetTempPath( 100, tmppath ) ; //获得临时目录路径 //分离之后的 exe 文件名 strcpy( exefile ,(AnsiString(tmppath)+AnsiString("\\Myexe.exe") ).c_str() ) ; if(GetFileAttributes( exefile )== 0xffffffff) //文件不存在 {//从资源中分离可执行文件 TResourceStream &rs = *new TResourceStream( (int)HInstance, AnsiString("MYEXE"),"EXEFILE" ) ; rs.SaveToFile( AnsiString(exefile) ) ; delete &rs ; } 分离出可执行文件之后就可以通过 ShellExecute 执行它了。显然,对文件的分离需要浪费一 些时间,可是却为我们带来很大的方便。 第 8 章 多媒体播放器 236 8.5 自定义窗体实现 完成了对资源文件的设置之后,我们现在在程序中已经可以通过头文件中定义的一系列 TBitmap 类型的指针对各个位图进行访问了,下面介绍具体的界面绘制过程。 8.5.1 窗体界面的绘制 z 窗体背景的绘制 程序中窗体的背景可以通过设置 FormMain 的背景色来改变,或者,让 ShapeClient 的 Brush 的填充方式不取 bsClear,然后为 ShapeClient 设定合适的色彩。但是,简单的背景色调 始终不能满足人们的视觉欲望,所以要是能使用自己定义的图片作为背景就好了。 方法有,添加 Image 组件设其 Align 为 alClient,然后在 Image 中画图。但是,既然窗体 的底图始终是要绘制的,如果添加了 Image 作为背景,那系统重绘窗体时始终还是要绘制它 的背景,然后再绘制 Image,何不定义一个函数代替 Window 绘制窗体底图的过程呢。 实现起来也不难,只需要拦截重绘窗体背景的消息即可,这个消息是 WM_ERASEBKGND,所以,在头文件中添加如下内容: void __fastcall OnWMEraseBkgnd(TWMEraseBkgnd& Msg); BEGIN_MESSAGE_MAP //拦截重画背景的事件 MESSAGE_HANDLER(WM_ERASEBKGND,TWMEraseBkgnd,OnWMEraseBkgnd); END_MESSAGE_MAP(TForm); 然后编写自己的绘制窗体背景的函数如下: //--------------------------------------------------------------------------- void __fastcall TFormMain::OnWMEraseBkgnd(TWMEraseBkgnd& Msg) {//重画背景 TCanvas* canvas = new TCanvas; canvas->Handle = Msg.DC; int cx = ClientWidth/bmpFormBK->Width + 1; int cy = ClientHeight/bmpFormBK->Height + 1; for (int i=0; iDraw(j*bmpFormBK->Width, i*bmpFormBK->Height,bmpFormBK); Msg.Result = true; } delete canvas; 第 8 章 多媒体播放器 237 } //--------------------------------------------------------------------------- 其中使用的背景图片 bmpFormBK 是从资源文件中读取的。 z 对窗体 OnCreate 事件的响应 窗体创建时,需要在各个 TImage 组件中绘制图象,代码如下 // -------------------------------------------------------------------------- void __fastcall TFormMain::FormCreate(TObject *Sender) { //设置窗体的双缓冲功能,为了使绘制图象时尽量不要闪烁 DoubleBuffered = true; //从资源中读取位图 LoadBitmaps(); // 调整 ImageTitle 的大小,如果设计期间窗体的大小与 ImageTitle 等不适合,这几步 //是必要的,也可以是随 ShapeClient 的大小改变位置,这里使用后者 ImageTitle->Width = ShapeClient->Width-2; //Width - 2; // 调整 ImageClose,ImageZoom 和 ImageMinimize 的位置 ImageClose->Left = ShapeClient->Width - 16; // 16 = Image->Width + 4 ImageZoom->Left = ImageClose->Left - 13; // 13 = Image->Width + 1 ImageMinimize->Left = ImageZoom->Left - 13; // 13 = Image->Width + 1 // 绘制控制栏背景 ImageControlBar->Stretch=false;ImageControlBar->AutoSize=false; //ImageControlBar->Picture->Bitmap->Assign(bmpControlBarBK); //下面代码适用于功能栏背景图片比 ImageControl 小的时候,大小相同时可用上一行代 码 int cx = ImageControlBar->Width/bmpControlBarBK->Width + 1; int cy = ImageControlBar->Height/bmpControlBarBK->Height + 1; for (int i=0; iCanvas->Draw(j*bmpControlBarBK->Width, i*bmpControlBarBK->Height,bmpControlBarBK); } //绘制控制栏按钮 ImagePre->Picture->Assign(bmpControlPre); ImagePlay->Picture->Assign(bmpControlPlay); ImagePause->Picture->Assign(bmpControlPause); ImageStop->Picture->Assign(bmpControlStop); ImageNext->Picture->Assign(bmpControlNext); ImageOpen->Picture->Assign(bmpControlOpen); 第 8 章 多媒体播放器 238 //时间显示栏 显示 00:00 ImageTime1->Picture->Bitmap=bmpTime0; ImageTime2->Picture->Bitmap=bmpTime0; ImageTime3->Picture->Bitmap=bmpTimeDot; ImageTime4->Picture->Bitmap=bmpTime0; ImageTime5->Picture->Bitmap=bmpTime0; //清空托动栏,也就是在进度栏中绘制底图 ImageTrack->Canvas->Brush->Color=clMoneyGreen; ImageTrack->Canvas->Rectangle(0,0,ImageTrack->Width,ImageTrack->Height); //初始化参量 IsVideo=false; //头文件中定义的全局变量,标志打开的媒体是否有视频 MediaPlayer1->Notify = true; //触发 OnNotify 事件 mMinutes=0; mSeconds=0; //播放时间,全局变量 //定时器,用于时间的显示和拖动栏的随播放时间的动态变化 Timer1->Enabled=false; } //--------------------------------------------------------------------------- z 对窗体激活和失去激活的响应 与重画窗体背景的方法一样,要对窗体的激活和失去激活的处理,我们采取拦截 Windows 消息的方法,窗体激活状态的改变对应的消息是 WM_ACTIVATE,所以,在头文件中添加如 下内容: void __fastcall OnWMActive(TMessage &Msg); BEGIN_MESSAGE_MAP //拦截窗体激活事件 MESSAGE_HANDLER(WM_ACTIVATE,TMessage,OnWMActive) END_MESSAGE_MAP(TForm); 可以将上面内容与拦截窗体背景重绘的代码放在一起。 象 Winamp 一样,在窗体激活时,对标题栏进行高亮显示,而失去激活时,标题栏颜色 黯淡一些。在本章制作的媒体播放程序中,在窗体激活和失去激活两种状态下,我们在标题 栏位置的 ImageTitle 中绘制不同的图片,而且,标题栏中的按钮也要更改。 对 WM_ACTIVATE 消息的响应函数如下: //-------------------------------------------------------------------------- void __fastcall TFormMain::OnWMActive(TMessage &Msg) { TForm::Dispatch(&Msg); switch(Msg.WParamLo) { case WA_ACTIVE: case WA_CLICKACTIVE: 第 8 章 多媒体播放器 239 ImageTitle->Picture->Bitmap = bmpTitleAct; //标题 Label 的颜色激活时为黄色 LabelTitle->Font->Color = clYellow; //窗体四周的边线,激活时为绿色 ShapeClient->Pen->Color = clGreen; ImageSystem->Picture->Bitmap->Assign(bmpSysAct); ImageMinimize->Picture->Bitmap->Assign(bmpMinimize); ImageZoom->Picture->Bitmap->Assign(bmpZoom); ImageClose->Picture->Bitmap->Assign(bmpClose); break; case WA_INACTIVE: ImageTitle->Picture->Bitmap = bmpTitleInact; //标题 Label 的颜色失去激活时颜色为 MoneyGreen LabelTitle->Font->Color = clMoneyGreen; //窗体边线,失去激活也用 MoneyGreen 颜色 ShapeClient->Pen->Color = clMoneyGreen; ImageSystem->Picture->Bitmap = bmpSysInact; ImageMinimize->Picture->Bitmap = bmp_Inact; ImageZoom->Picture->Bitmap = bmp_Inact; ImageClose->Picture->Bitmap = bmp_Inact; break; } } //-------------------------------------------------------------------------- 激活和失去激活时的窗体界面效果如图 8-3 和图 8-4 所示: 图 8-3 激活状态的 MyPlayer 播放器 第 8 章 多媒体播放器 240 图 8-4 失去激活时的 MyPlayer 播放器 8.5.2 窗体的拖动 正常的窗体,我们知道,只有鼠标在标题栏上时,才能对窗体进行拖动操作,但是我们 设置了主窗体的 BorderStyle 是 bsNone,也就是没有了标题栏,界面上的标题栏是我们自己 绘制的,实际上不是窗体的标题栏。那窗体如何拖动呢? 而且,对于媒体播放程序,如 Winamp,界面都很小巧,所以一般都要求不管鼠标在窗 体中的哪个位置,只要那里不是功能按钮等组件,都需要能够对窗体进行拖动。怎么实现呢? 其实,要实现无标题窗体的拖动也是很简单的。而且实现的方法很多,下面分别介绍三 种方法。 z 拦截消息法 跟窗体背景重绘与窗体激活的处理一样,可以通过拦截消息的方法实现窗体拖动。这要 用到 WM_NCHITTEST 消息,这个消息决定鼠标当前所处位置的信息,我们只要拦截到这个 消息,然后判断鼠标所在的位置是否是需要实现拖动的位置,如果是,则对消息进行更改, 再传回给系统,就可以轻易的让系统以为现在鼠标的位置是在标题栏,从而实现窗体的拖动。 用这种方法需要先在头文件中定义如下内容: void __fastcall MyDrag(TMessage &Msg); BEGIN_MESSAGE_MAP //拦截窗体激活事件 MESSAGE_HANDLER(WM_NCHITTEST,TMessage,MyDrag) END_MESSAGE_MAP(TForm); 然后定义 MyDrag 函数内容如下: void __fastcall TForm1::MyDrag(TMessage& Msg) { TPoint pt; //从消息 Msg 中获得鼠标位置 pt.x=LOWORD(Msg.LParam); pt.y=HIWORD(Msg.LParam); //获得的位置坐标为绝对坐标,也就是相对于屏幕左上角的坐标, //要先转化为窗体中的相对坐标 pt =ScreenToClient(pt); RECT rc; 第 8 章 多媒体播放器 241 //定义 ImageTitle 所在的举行区域 ::SetRect(&rc,0,0,ImageTitle->Width,ImageTitle->Height); if (PtInRect(&rc,pt)) {//如果鼠标在 ImageTitle 中,则在 Msg 中写入鼠标在标题栏的信息,以欺骗系统 Msg.Result = HTCAPTION; } else {//如果鼠标不在 ImageTitle,则不处理消息,使用默认的鼠标位置处理过程 DefaultHandler(&Msg); } } 这种方法虽然可行,而且也不复杂,但是,有一个缺点,就是需要实现拖动功能的位置 太多,实现起来就会很复杂。比如要实现整个窗体中除了一个功能按钮等之外的位置都可以 拖动窗体,这就要对鼠标位置进行很复杂的判断。所以,这种方法最适用于简单矩形区域的 的拖动功能。 z 窗体移动法 复杂的问题有时解决起来很简单。其实想简单一点,鼠标拖动窗体不就是让窗体随鼠标 移动而已吗?让窗体随鼠标移动不是很简单的事情吗! 首先,定义一个全局变量 IsDraging,用来标志鼠标移动过程中是否在拖动窗体。 然后,在发生 OnMouseDown 事件时,记录鼠标的位置,设置 IsDraging 为 true。 关键的是,在鼠标的 OnMouseMove 事件中,如果 IsDraging 为 true,则根据 OnMouseMove 事件传回的鼠标位置相对于 OnMouseDown 时的鼠标位置的改变,对窗体的位置,也就是其 Left 和 Top 属性,进行调整。这样使得鼠标相对与窗体的位置始终不便,随鼠标的移动,窗 体也就实现了移动。 最后,在 OnMouseUp 事件中,要将 IsDraging 设为 false。 这种方法在很多专业的软件(如 Winamp)中都有使用,足见它的简单方便和易用。 实现代码大致如下: //----------------------------------------------------------------------- void __fastcall TFormMain::ImageTitleMouseDown(TObject *Sender, TMouseButton Button,TShiftState Shift,int X,int Y) { IsDraging=true; xx=X; yy=Y; } //--------------------------------------------------------------------------- void __fastcall TFormMain::ImageTitleMouseMove(TObject *Sender,TShiftState Shift,int X,int Y) { if(IsDraging) 第 8 章 多媒体播放器 242 { FormMain->Left+=X-xx; FormMain->Top+=Y-yy; } } //-------------------------------------------------------------------------- void __fastcall TFormMain::ImageTitleMouseUp(TObject *Sender, TMouseButton Button,TShiftState Shift,int X,int Y) { IsDraging=false; } //-------------------------------------------------------------------------- 只需要将需要实现拖动效果的组件的鼠标事件与上面三个函数分别对应,即可实现窗体 任意位置的拖动,而对处于上方的没用对应上面鼠标事件处理过程的组件,它们不能拖动窗 体,这样也就达到了我们制作媒体播放器的要求。但是,本章并不采取这种方法。 z 本章程序中用的方法 这里用到的方法很简单,就是发送一个系统消息,告诉系统鼠标现在在标题栏,所以实 现起来只需要两行的代码: //-------------------------------------------------------------------------------------- void __fastcall TFormMain::ImageTitleMouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { Refresh(); if(Button == mbLeft) { ReleaseCapture(); Perform(WM_SYSCOMMAND, 0xf017, 0); } } //-------------------------------------------------------------------------- 第 8 章 多媒体播放器 243 图 8-5 New Method 对话框 上面的函数是自定义的__published 函数,只有定义的__published 函数,才会出现在属性 检查器的相应事件的下拉菜单中。添加__published 函数的方法如下:在代码编辑器左侧的 Classes 树中打开 TFormMain 类,右键选择“New Method”,在打开的窗口按图 8-5 填写。点 OK 之后会自动跳到代码段,在代码段填写自己的代码即可。 本程序中与组件事件处理有关的函数均是用上面的方法添加的,这样就可以方便的在属 性检查器中为各个组件指定对应的响应函数了。 对于 ImageTitleMouseDown 函数,它对应组件的 OnMouseDown 事件,需要设定 OnMouseDown 的对应函数为 ImageTitleMouseDown 的组件有:ShapeClient、ImageTitle、 ImageControl。 8.5.3 标题栏按钮的事件响应 同 8.5.2 中的方法,定义函数如下: void __fastcall ImageSystemDblClick(TObject *Sender); void __fastcall ImageSystemClick(TObject *Sender); 第一个函数是标题栏中左侧按钮 ImageSystem 的鼠标双击事件响应函数,第二个函数是标题 第 8 章 多媒体播放器 244 栏中所有的按钮 ImageSystem、ImageMinimize、ImageZoom、ImageClose 的鼠标单击事件响 应函数。 两个函数的代码如下: //--------------------------------------------------------------------------- void __fastcall TFormMain::ImageSystemDblClick(TObject *Sender) {//双击关闭程序 Close(); } //--------------------------------------------------------------------------- void __fastcall TFormMain::ImageSystemClick(TObject *Sender) { //标记窗体的状态是卷起的还是正常的,静态变量 static bool bUp = false; TImage *p = dynamic_cast(Sender); //将 Sender 转化为 TImage 指针 switch(p->Tag) {//根据各个 Image 的 Tag 不同,来判断发生单击事件的是哪个 Image case 100: // System Button break; case 101: // Minimize Button 最小化 Application->Minimize(); break; case 102: // Zoom Button 卷起 bUp = !bUp; Height = (bUp)?(ImageTitle->Height + 2):FORMHEIGHT; p->Hint = (bUp)?"还原":"上卷"; break; case 103: // Close Button Application->Terminate(); break; default: break; } } //--------------------------------------------------------------------------- 8.6 媒体播放功能的实现 界面的处理工作完成以后,现在我们讨论与媒体播放相关的内容。其实因为 TMediaPlayer 第 8 章 多媒体播放器 245 组件封装了常用的很多功能,所以编写媒体播放器的主要工作是在界面的处理上,而针对媒 体播放的操作都很简单。下面我们分别介绍程序中对媒体的控制,以及界面上的组件状态对 播放状态的响应。 8.6.1 媒体播放控制按钮的响应 在 ImageControlBar 上的 ImagePre、ImagePlay 等六个按钮,分别完成对应于 TMediaPlayer 组件上的 btPrev、btPlay 等,所以对他们的相应都可以调用 TMediaPlay 对应于这些按钮的方 法,如 Previous()、Play()等。只是要注意每个方法都只有在 TMediaPlayer 的某些 Mode 下才能使用,所以我们要做的工作就是判断 TMediaPlayer 组件的状态(Mode),然后判断是 否调用对应于按钮的函数。 六个按钮中需要特殊处理的就是 ImageOpen,因为我们要把它作为打开文件的按钮,需 要弹出打开文件对话窗,所以不能让它简单的对应到 TMediaPlayer 的 Open()方法上。 跟对标题栏中的按钮对鼠标事件的响应一样,我们在 IDE 管理的__published 中添加函数 (添加方法见 8.5.2)void __fastcall ImageControlClick(TObject * Sender);然后编写此函数的 代码如下: //--------------------------------------------------------------------------- void __fastcall TFormMain::ImageControlClick(TObject * Sender) { TImage *p = (TImage *)Sender; switch(p->Tag) { case 200: // Pre Button if(MediaPlayer1->Mode==mpPlaying || MediaPlayer1->Mode==mpPaused || MediaPlayer1->Mode==mpStopped) MediaPlayer1->Previous(); break; case 201: // Play Button if(MediaPlayer1->Mode == mpStopped || MediaPlayer1->Mode == mpPaused) { if(IsVideo && ! FormVideo->Visible) //视频窗口被关掉了 FormVideo->Show(); MediaPlayer1->Play(); Timer1->Enabled=true; } break; case 202: // Pause Button 第 8 章 多媒体播放器 246 if(MediaPlayer1->Mode == mpPlaying) { MediaPlayer1->PauseOnly(); Timer1->Enabled=false; } break; case 203: // Stop Button if(MediaPlayer1->Mode == mpPlaying || MediaPlayer1->Mode == mpPaused) { MediaPlayer1->Stop(); MediaPlayer1->Position=MediaPlayer1->Start; Timer1->Enabled=false; } break; case 204: // Next Button if(MediaPlayer1->Mode==mpPlaying || MediaPlayer1->Mode==mpPaused || MediaPlayer1->Mode==mpStopped) MediaPlayer1->Next(); break; case 205: // Open Button OpenMedia(); break; default: break; } } //------------------------------------------------------------------ 其中为了简便,将 ImageOpen 的响应过程单独编写为 OpenMedia()函数,先在头文件 中 TFormMain 类中的共有或私有区添加函数声明如下: void __fastcall OpenMedia(void); 然后编写其代码: //------------------------------------------------------------------ void __fastcall TFormMain::OpenMedia(void) { if(MediaPlayer1->Mode == mpPlaying || MediaPlayer1->Mode==mpOpen) MediaPlayer1->Stop(); OpenDialog1->Title="选择打开的文件:"; OpenDialog1->Filter="All files (*.*)|*.*|Wave files (*.wav)|*.wav|MPEG files (*.mpg)|*.mpg| AVI files (*.avi)|*.avi|MIDI files (*.mid)|*.mid|MP3 files(*.mp3)|*.mp3"; if(OpenDialog1->Execute()) 第 8 章 多媒体播放器 247 { //根据媒体文件自动选择 DeviceType MediaPlayer1->DeviceType=dtAutoSelect; MediaPlayer1->FileName=OpenDialog1->FileName; //打开多媒体设备 MediaPlayer1->Open(); //显示打开的文件名 LabelFileName->Caption=ExtractFileName(MediaPlayer1->FileName); //初始化时间显示 //判断是否为视频文件,如果是,显示视频窗口, //并将此窗口设置为 MediaPlayer 的 DisPlay 属性 if(ExtractFileExt(MediaPlayer1->FileName).LowerCase()==".mpg" || ExtractFileExt(MediaPlayer1->FileName).LowerCase()==".avi") { //设定视频窗口的大小为视频的实际大小, FormVideo->Width=MediaPlayer1->DisplayRect.Width()+4; FormVideo->Height=MediaPlayer1->DisplayRect.Height()+4; //显示视频窗体 FormVideo->Show(); MediaPlayer1->Display=FormVideo; FormVideo->Caption=LabelFileName->Caption; MediaPlayer1->DisplayRect=Rect(0,0, FormVideo->ClientWidth,FormVideo->ClientHeight); //视频标志,标记打开的是视频文件 IsVideo=true; } else if(IsVideo) //打开非视频文件之前,视频窗口是打开的 { FormVideo->Close(); IsVideo=false; } //定时器处理,打开时,关闭定时器 Timer1->Enabled=false; //调用 ImagePlay 组件的 Click 函数,播放媒体 ImageControlClick(ImagePlay); } } 第 8 章 多媒体播放器 248 8.6.2 播放时间的显示 程序中使用一个定时器(TTimer)组件,设置其 Intervel 为 1000ms,即 1s,然后编写其 OnTimer 事件对应的代码,使得媒体文件在播放时每秒刷新一次播放时间,Timer1 的 OnTimer 事件对应的代码如下: //-------------------------------------------------------------------------------- void __fastcall TFormMain::Timer1Timer(TObject *Sender) { int TheLength; //媒体播放停止时,关闭定时器 if(MediaPlayer1->Mode==mpStopped) Timer1->Enabled=false; // 设置 MediaPlayer1 的时间单位为毫秒 MediaPlayer1->TimeFormat = tfMilliseconds; //将当前播放的位置由毫秒转换为秒 TheLength=MediaPlayer1->Position/1000; //秒 //本章假设媒体不会超过一小时,所以将小时部分注释掉, //读者添加小时部分的显示时,直接去掉注释即可 //mHours=TheLength/3600; //小时 mMinutes=(TheLength%3600)/60; //分钟 mSeconds=(TheLength%3600)%60; //秒 //调用 Display 函数在 ImageTime1 等组件中显示时间 DisplayTime(mMinutes,mSeconds); //绘制进度条 MediaPlayer1->TimeFormat=tfMilliseconds; //微秒 ImageTrack->Canvas->Brush->Color=clMoneyGreen; ImageTrack->Canvas->Rectangle(0,0,ImageTrack->Width,ImageTrack->Height); ImageTrack->Canvas->Brush->Color=clGreen; //除法运算时注意要转换为 float 型再做除法,不然整形相除结果始终为 0 ImageTrack->Canvas->Rectangle(0,0, ImageTrack->Width*(float(MediaPlayer1->Position)/float(MediaPlayer1->Length)), ImageTrack->Height); } //--------------------------------------------------------------------------- 其中涉及到由 MediaPlayer 的 Position 属性获取当前的位置的时间、根据时间在窗体上显示数 字位图 以及 根据时间绘制进度条。数字位图的显示函数需要先在头文件中手动添加 void __fastcall DisplayTime(int Min,int Sec); void __fastcall TimeLoadBmp(TImage * img,int ch); 第 8 章 多媒体播放器 249 它们的代码如下: //--------------------------------------------------------------------------- void __fastcall TFormMain::DisplayTime(int Min,int Sec) { int time1=0,time2=0,time4=0,time5=0; time1=(Min%60)/10; time2=(Min%60)%10; time4=Sec/10; time5=Sec%10; TimeLoadBmp(ImageTime1,time1); TimeLoadBmp(ImageTime2,time2); TimeLoadBmp(ImageTime4,time4); TimeLoadBmp(ImageTime5,time5); } //--------------------------------------------------------------------------- void __fastcall TFormMain::TimeLoadBmp(TImage * img,int ch) { switch(ch) { case 0: img->Picture->Bitmap=bmpTime0; break; case 1: img->Picture->Bitmap=bmpTime1; break; case 2: img->Picture->Bitmap=bmpTime2; break; case 3: img->Picture->Bitmap=bmpTime3; break; case 4: img->Picture->Bitmap=bmpTime4; break; case 5: img->Picture->Bitmap=bmpTime5; break; case 6: 第 8 章 多媒体播放器 250 img->Picture->Bitmap=bmpTime6; break; case 7: img->Picture->Bitmap=bmpTime7; break; case 8: img->Picture->Bitmap=bmpTime8; break; case 9: img->Picture->Bitmap=bmpTime9; break; case 10: img->Picture->Bitmap=bmpTimeDot; break; default: break; } } //--------------------------------------------------------------------------- 8.6.3 进度条的控制 在 C++Builder 中,进度条可以使用 TTrackBar 组件或者 TScrollBar 来实现,它们都可以 实现进度的显示和拖动控制,另外一个组件 TProgressBar 也常用来显示进度。但是它们都是 Windows 窗体的风格,无论颜色,形状都与我们自己绘制的窗体不相适合,所以我们用一个 TImage 组件 ImageTrack 来实现进度的显示和控制。 当然本章对程序的实现中也只是将进度条的颜色设置与程序界面风格相适应而已,并没 有对进度条的绘制做太细致的工作,读者可以自己改写程序在 ImageTrack 中绘制更精细的图 案。 在程序开始运行时,要绘制 ImageTrack 的底图,这部分代码参看 8.5.1 节中 FormMain 的 OnCreat 事件的响应代码。在媒体文件播放时,有定时器 Timer1 每秒触发一次绘制进度的 过程,绘制时根据 MediaPlayer1 的 Position 占其 Length 的比例在 ImageTrack 中绘制相同比例 宽度的进度条。 重要的是进度条的拖动操作。也就是在拖动条中点击鼠标,则根据鼠标点击位置相对与 进度条宽度的比例,将媒体文件的 Position 设定在相应的位置然后播放。这需要响应 ImageTrack 的 OnMouseDown 事件,用 IDE 在__published 部分添加函数: void __fastcall ImageTrackMouseDown(TObject * Sender, TMouseButton Button, TShiftState Shift, int X, int Y); (其实双击属性检查其中 ImageTrack 组件的 OnMouseDown 事件后的空白即可自动生成其响 第 8 章 多媒体播放器 251 应函数,只是本章很多地方都用 IDE 手动添加,所以这里不改变编程风格) 编写函数代码如下: //------------------------------------------------------------------------ void __fastcall TFormMain::ImageTrackMouseDown(TObject * Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { if(MediaPlayer1->Mode == mpPlaying) { ImageTrack->Canvas->Brush->Color=clMoneyGreen; ImageTrack->Canvas->Rectangle(0,0,ImageTrack->Width,ImageTrack->Height); ImageTrack->Canvas->Brush->Color=clGreen; ImageTrack->Canvas->Rectangle(0,0,X,ImageTrack->Height); MediaPlayer1->TimeFormat=tfMilliseconds; //微秒 MediaPlayer1->PauseOnly(); MediaPlayer1->Position=(float(X)/float(ImageTrack->Width))*MediaPlayer1->Length; MediaPlayer1->Play(); } } //------------------------------------------------------------------------ 8.6.4 OnNotify 事件的响应 介绍 TMediaPlayer 组件时我们提到,媒体文件播放时的状态改变将触发 OnNotify 事件, 媒体播放程序中,通常都需要对这个事件进行响应,根据播放状态做响应的处理。对此事件 的响应,最重要的便是 TMediaPlayer 组件的 Mode 属性和 NotifyValue 属性,它们分别对应于 当前 MCI 的工作模式和从 MCI 返回的控制命令的返回消息。 在本章的程序中,如果媒体处于停止状态,则需要讲时间数字的显示清零,进度条的显 示也要清除,代码如下: //---------------------------------------------------------------------- void __fastcall TFormMain::MediaPlayer1Notify(TObject *Sender) { if(MediaPlayer1->Mode == mpStopped) { Timer1->Enabled=false; mMinutes=0; mSeconds=0; 第 8 章 多媒体播放器 252 DisplayTime(0,0); //清空托动栏 ImageTrack->Canvas->Brush->Color=clMoneyGreen; ImageTrack->Canvas->Rectangle(0,0,ImageTrack->Width,ImageTrack->Height); } MediaPlayer1->Notify=true; } //--------------------------------------------------------------------- 8.6.5 视频显示窗口的功能 视频窗体用来作为视频图象的输出区域,最重要的,莫过于视频图象在窗体中的大小。 在 OpenMedia 函数中,打开一个媒体文件之后,如果该媒体是视频文件,则根据视频的实际 显示区域的大小,调整视频窗体 FormVideo 的大小,之后再显示出视频窗体。但是,用户可 能需要自己调节视频窗体的大小,所以要对 FormVideo 的 OnResize 事件进行响应,响应代码 如下: //--------------------------------------------------------------------------- void __fastcall TFormVideo::FormResize(TObject *Sender) { FormMain->MediaPlayer1->DisplayRect= Rect(0,0,ClientWidth,ClientHeight); } //--------------------------------------------------------------------------- 观赏视频文件时,常常需要全屏显示,通常的媒体播放器用窗体的鼠标双击事件来触发 全屏操作,我们这里也这么做,全屏的实现代码如下: //--------------------------------------------------------------------------- void __fastcall TFormVideo::FormDblClick(TObject *Sender) { static int tmpLeft,tmpTop,tmpWidth,tmpHeight; if(BorderStyle!=bsNone) {//还没有最大化 //纪录最大化之前的窗体位置和大小 tmpLeft=Left; tmpTop=Top; tmpWidth=Width; tmpHeight=Height; //重新设置窗体属性 BorderStyle=bsNone; Left=0;Top=0; 第 8 章 多媒体播放器 253 Width=Screen->Width; Height=Screen->Height; //重新为 MediaPlayer1 指定显示窗体 FormMain->MediaPlayer1->Display=FormVideo; FormMain->MediaPlayer1->DisplayRect=Rect(0,0, FormVideo->Width,FormVideo->Height); } else {//全屏状态,需要返回到正常模式 //恢复最大化之前的窗体位置和大小 Left=tmpLeft; Top=tmpTop; Width=tmpWidth; Height=tmpHeight; //重新设置窗体属性 BorderStyle=bsSizeToolWin; //重新为 MediaPlayer1 指定显示窗体 FormMain->MediaPlayer1->Display=FormVideo; FormMain->MediaPlayer1->DisplayRect=Rect(0,0, FormVideo->ClientWidth,FormVideo->ClientHeight); } } //--------------------------------------------------------------------------- 代码中需要注意的是,对窗体的 BorderStyle 属性更改之后,需要重新为 MediaPlayer1 指定 Display 属性,不然窗体中就显示不出视频图象了。 8.7 思考题 z 可以添加到资源文件中的文件类型有哪些? z 资源文件中的 BMP 图象有何要求? z 要在窗体的任意位置都能拖动窗体,有哪些方法? z 在什么情况下需要重新为 TMediaPlayer 指定视频显示区域? z 本章的 MyPlayer 程序的播放时间以及进度条的绘制都有闪烁现象,如何解决? z TMediaPlayer 并没有提供音量控制功能,如何为程序添加音量控制功能? z 如何实现媒体文件的自动重复播放? 《C++ Builder 6 编程实例精解 赵明现》 第 09 章 系统信息管理程序 本章重点 本章首先讲述 Windows 系统的运行机制以及 C++Builder 中如何处理消息和使用 API 函 数,然后主要通过对 API 函数的使用来制作系统信息管理程序。 学习目的 通过本章的学习,您可以: ■ 理解 Windows 系统的消息驱动机制 ■ 理解 C++Builder 中对消息的处理 ■ 熟悉在 C++Builder 中拦截和利用消息的方法 ■ 熟悉多页组件的使用 ■ 掌握关于窗口、进程的 API 函数的使用 ■ 掌握关于磁盘、内存及其它设备的 API 函数的使用 ■ 掌握环境变量的读写方法 第 9 章 系统信息管理程序 255 第 9 章 系统信息管理程序 256 本章典型效果图 第 9 章 系统信息管理程序 257 9.1 Windows API 使用基础 C++Builder 的编程环境,无疑给我们带来了极大的便利,因为在组件中封装了很多的功 能,就像第八章中的媒体播放器,其实在窗口上添加一个 TMediaPlayer 组建,只需要配合一 个 OpenDailog,为 TMediaPlayer 指定 FileName,不用作其他任何操作,编译之后就是一个 功能强大的媒体播放程序。可是,C++Builder 为我们提供方便的同时,也必定在某些方面会 限制程序开发的灵活性。 C++Builder 的可视化编程环境,本质上是将常用的 Windows API 的调用都封装在组件之 中,这是因为 Windows 程序的编写,本质上都是对 Windows API 函数的调用。最初的 Windows 程序的制作,也都是通过直接跟 API 打交道实现的,但是因为 API 的繁杂,使得大部分的时 间都要浪费在程序的细节上。现在固然不需要知道 API 是什么东西,我们也可以用 C++Builder 很容易的制作出功能强大的 Windows 程序,但毕竟 C++Builder 中不可能封装所有的 API,所 以,很多时候,我们都不得不直接跟 API 打交道。 本节,我们就介绍一下 API,这也是本章制作的系统信息管理程序中需要频繁使用的。 9.1.1 Windows 的运行机制 API,是 Application Programming Interface(应用编程接口)的缩写,C++Builder 中的类、 类库无不是建立在 Windows API 的基础之上,它是 Windows 系统的一部分,同时 Windows 自身的运行,也需要调用这些 API 函数。所以,要理解如何使用 API,必须先了解一些 Windows 的运行机制。 Windows 系统是由消息(或称事件)驱动的操作系统,这是相对于最初的过程驱动来说 的,在 Windows 中,系统为应用程序生成一个消息队列,系统将用户的操作、系统的信息等 都作为消息发送到队列中,供应用程序读取并处理这些消息。消息,也是应用程序与应用程 序以及应用程序和操作系统之间的通信方式。 对于一个 Windows 应用程序,其入口在 WinMain 函数: WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine , int nCmdShow) 在第七章屏幕保护程序的制作中,我们已经用到 WinMain 的入口参数来得到父窗口的句柄以 及其它命令行参数,这里对 WinMain 的入口参数详细说明一下: hCurInstance:实例句柄,是一个数值,在 Windows 中用来唯一标志一个应用程序的实 例。即使同一个应用程序,当它被运行多次的时候,其实例句柄是不同的。 hPrevInstance:前一个实例的句柄,不过基本没什么用,因为它在 Win32 系统中,总是 为 NULL。 lpCmdLine:它是一个指针,指向一个以 0 结束的字符串,在 Win32 中,它里面不仅仅 只是命令行参数,而是整个命令行。 nCmdShow:整形数,决定如何显示窗口。通常窗口显示使用 SW_SHOWNORMAL。 WinMain 函数的返回值在 Windows 系统中并没有被使用,但是在调试程序的时候它很有 第 9 章 系统信息管理程序 258 用,因为,可以根据程序终止情况的不同返回不同的值。 在 WinMain 中,通过三个步骤来实现窗口的显示,首先注册窗口类,之后创建窗口,最 后根据创建的窗口的句柄显示该窗口。 注册窗口使用ATOM RegisterClassEx(CONST WNDCLASSEX *lpwcx),调用该函数之前, 需要先设置入口参数 lpwcx,它是一个指向 WNDCLASSEX 结构的指针,WNDCLASSEX 结 构声明如下: typedef struct _WNDCLASSEX { // wc UINT cbSize; //结构的大小,即 sizeof(WNDCLASSEX) UINT style; //类风格 WNDPROC lpfnWndProc; //窗口类的窗口过程 int cbClsExtra; //在类结构中预留的空间 int cbWndExtra; //在 Windows 内部保存的窗口结构中预留的空间 HANDLE hInstance; //程序的实例句柄 HICON hIcon; //程序图标 HCURSOR hCursor; //程序光标 HBRUSH hbrBackground; //窗口的背景颜色 LPCTSTR lpszMenuName; //菜单 LPCTSTR lpszClassName; //类名,和程序名相同 HICON hIconSm; //也是程序图标,小图标,为空时会用 hIcon 来代替它 } WNDCLASSEX; 注册窗口类之后就可以创建窗口了,创建窗口使用如下函数: HWND CreateWindow( LPCTSTR lpClassName, // 指向注册的窗口类名 LPCTSTR lpWindowName, // 指向窗口标题名 DWORD dwStyle, //窗口风格 int x, // 窗口 x 坐标,水平位置 int y, // 窗口 y 坐标,竖直位置 int nWidth, // 窗口宽度 int nHeight, // 窗口高度 HWND hWndParent, //父窗口句柄 HMENU hMenu, //菜单句柄 HANDLE hInstance, //实例句柄 LPVOID lpParam // 创建窗口所用的参数 ); 窗口被创建以后,需要调用 BOOL ShowWindow( HWND hWnd, // 窗口句柄 int nCmdShow // 窗口状态,即 WinMain 中的 nCmdShow ); 和 BOOL UpdateWindow( HWND hWnd // handle of window 第 9 章 系统信息管理程序 259 ); 调用这两个函数之后,窗口才会被显示出来。之后窗口通过一个循环来从消息队列中读取消 息,循环过程如下: while (GetMessage(&msg, NULL, 0, 0)) //从消息队列中取得消息 { if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg);// 检索并生成字符消息 WM_CHAR DispatchMessage(&msg);// 将消息发送给相应的窗口函数 } } 通过循环不断的从消息队列中读取消息,然后对消息进行处理之后交给窗口函数过程处理。 而当读取的消息是 WM_QUIT 时,GetMessage 返回 0,循环终止,程序运行结束。而处理消 息的窗口函数形式上是一个巨大的 switch 结构,每一个 case 语句对应一种消息,当应用程序 接受到一个消息时,在窗口函数内的相应的case语句就会被激活并执行相应的响应程序模块。 一般来说,窗口函数的格式如下: LRESULT CALLBACK WndProc( HWND hwnd, //窗口的句柄 UNIT message, //所要处理的消息的类型标志 WPARAM wParam, //消息的附加参数 LPARAM lParam //消息的附加参数 ) { switch(message) //message 为消息的类型标志,如 WM_PAINT, WM_DESTROY 等 { case... ... break; ... case WM_DESTROY: PostQuitMessage(0); //发送结束程序的消息 WM_QUIT default: return DefWindowProc(hwnd,message,wParam,lParam);//采用系统默认的消息处理函数 } return(0); } 了解了 Windows 的工作机制之后,使用 API 就只是根据自己的需要调用相应的 API 函 数来处理特定的消息而已,这也是 API 使用的难点,因为往往不知道调用哪些函数来实现自 己想要的功能。查看 API 的相关帮助,可以得到一些编程中常用的 API 函数以及常用的消息 事件。 第 9 章 系统信息管理程序 260 9.1.2 C++Builder 对消息的处理 在 C++Builder 中,新建一个 Application 之后,会自动生成 WinMain 函数如下: WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { try { Application->Initialize(); //初始化操作 Application->CreateForm(__classid(TForm1), &Form1);//创建窗口并显示它 Application->Run();//进入消息循环,直到的到 WM_QUIT 消息 } ………………//略去 return 0; } 可以看到,在 Application->Run()中进入了消息循环,我们便从这里开始一层一层的追踪 C++Builder 是怎样处理消息的。 查看 C++Builder 目录 CBuilder6\Source\vcl 下的 forms.pas 文件,在里面找到 Run 的源码, 其中对消息的循环处理部分代码如下: repeat try HandleMessage; except HandleException(Self); end; until Terminated; 其中,消息首先被 HandleMessage 处理,跟进 HandleMessage 的源码,处理消息的一句为: if not ProcessMessage(Msg) then Idle(Msg); 先交给 ProcessMessage 处理,如果它不做处理则交给 Idle()处理,我们不管它,跟进 ProcessMessage,源码如下: function TApplication.ProcessMessage(var Msg: TMsg): Boolean; var Handled: Boolean; begin Result := False; //调用 API 函数 PeekMessage 从消息队列中读取消息,读取之后删除消息 if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then begin Result := True; if Msg.Message <> WM_QUIT then //如果不是程序结束消息 第 9 章 系统信息管理程序 261 begin Handled := False; //这里要注意,它判断是否定义了 OnMessage 函数,如果有则把消息交给它处理 //通过重载此函数,就可以截获消息,用自定义的函数处理 if Assigned(FOnMessage) then FOnMessage(Msg, Handled); if not IsHintMsg(Msg) and not Handled and not IsMDIMsg(Msg) and not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then begin //这两个就是 9.1.1 节中讨论的消息处理的 API 函数 TranslateMessage(Msg); DispatchMessage(Msg); end; end else FTerminate := True; //消息是 WM_QUIT 时设置程序结束标志 end; end; 由上面的分析知道,TApplication 类处理消息时,先判断是否存在 Application->OnMessage 函数,如果存在,则调用它来处理消息,之后才将消息进行分发,传递给各个组件。 每种组件都会定义一个函数 MainWndProc 来处理从 Application 的消息循环分发下来的 消息,但是 MainWndProc 并不做什么操作,它只是调用 WndProc 函数,然后做些异常处理。 值得注意的是,WndProc 是一个虚拟方法,所以对于所有组件,都可以通过对其 WndProc 的 重载来实现对消息的截获和自定义处理。在 WndProc 函数中,对消息进行各种处理,特别的, 在父类的 WndProc 中可以对分发给子类的消息进行过滤,如在组件正被拖动时,应该忽略掉 键盘事件,所以在 Twincontrol 类的 WndProc 方法中,有代码实现判断当组件不是被拖放状 态时,才继续分发键盘消息这样的功能。最后,WndProc 调用 Dispatch 方法,它继承自所有 组件的祖先 Tobject,而且,它也是虚函数,所以通过对 Dispatch 的重载,也可以实现自定义 的消息处理过程。Dispatch 中如果对消息仍然没有处理,那它会将消息返回给组件的父类处 理,一级一级最终如果 TObject 也没有对消息的处理,则调用缺省的处理方式 DefaultHandler。 9.1.3 消息的截取和处理 上面,我们从 TApplication 到组件,简单说明了它们对消息的传递过程,而明白消息处 理过程的目的无非是想利用它。在 C++Builder 中对消息的处理进行了大量的封装,将消息对 应到组件的事件上,编程者只需要编写响应的事件处理函数就可以了。但是,对于欲望永无 止境的编程者来说,C++Builder 中封装的消息仍然不能满足要求,而且,很多时候,自己编 写消息处理函数来实现某些功能会更加方便快捷,而且可以提高程序的性能。 这一节我们就介绍一下如何实现对消息的截取和处理。其实,在上一节中我们已经提到, 有三种方法,也就是重载 Application 的 OnMessage 函数、重载组件(注意 Application 的 第 9 章 系统信息管理程序 262 WndProc 函数是静态的,不能重载)的 WndProc 函数、重载组件的 Dispatch 函数,当然,你 也可以去重载缺省的消息处理函数 DefaultHandler,但是,既然要截取消息,自然不希望自己 的函数最后一个得到这个消息的处理权,所以恐怕不会有人用到它了。 最常用的消息截取方式是对 TObject 的 Dispatch 重载,这种方法我们在前面几章中屡次 用到,比如第四章中文件拖拉进窗体的处理、第八章中窗体背景的重绘和对窗体激活状态变 化的处理等地方,我们都用到了这种方法。这种方法需要先在窗体类声明声的用户编辑区 (User declarations)添加如下内容(以重画窗体背景为例): void __fastcall OnWMEraseBkgnd(TWMEraseBkgnd& Msg); BEGIN_MESSAGE_MAP //拦截重画背景的事件 MESSAGE_HANDLER(WM_ERASEBKGND,TWMEraseBkgnd,OnWMEraseBkgnd); END_MESSAGE_MAP(TForm); 其中,MESSAGE_HANDLER 中,第一个参数为需要截取的消息的标志,如 WM_ ERASEBKGND 便是窗体擦除背景的消息;第二个参数是消息结构定义,它与消息标志相对 应,不同的消息对应不同的消息结构;第三个参数是自己定义的消息处理函数的函数名。 看到上面的代码可能都会很奇怪,它怎么会是重载 Dispatch 函数呢?我们找到上面代码 的宏定义如下: #define BEGIN_MESSAGE_MAP virtual void __fastcall Dispatch(void *Message) \ { \ switch (((PMessage)Message)->Msg) \ { #define VCL_MESSAGE_HANDLER(msg,type,meth) \ case msg: \ meth(*((type *)Message)); \ break; // NOTE: ATL 定义的宏 MESSAGE_HANDLER 与 VCL 中的宏冲突. 所以 VCL 中将此宏改 // 名为 VCL_MESSAGE_HANDLER.如果你没有使用 ATL, // MESSAGE_HANDLER 宏的功能与以前的 C++Builder 版本中一样. #if !defined(USING_ATL) && !defined(USING_ATLVCL) && !defined(INC_ATL_HEADERS) #define MESSAGE_HANDLER VCL_MESSAGE_HANDLER #endif // ATL_COMPAT #define END_MESSAGE_MAP(base) default: \ base::Dispatch(Message); \ break; \ } \ } 第 9 章 系统信息管理程序 263 我们根据宏定义将上面拦截窗体重绘消息的代码展开,它便为: virtual void __fastcall Dispatch(void *Message) { switch (((PMessage)Message)->Msg) { case WM_ERASEBKGND : OnWMEraseBkgnd (*((TWMEraseBkgnd *)Message)); break; default: TForm::Dispatch(Message); break; } } 这样就很清楚了吧,由于所有的组件都是继承自 TObject 类,而组件的 Dispatch 函数也都继 承自 TObject,所以对所有 TObject 子类都可以通过 Dispatch 的重载,来实现自定义的消息处 理过程。 另外两种方法,也是对函数的重载,我们不再过多讨论,只举例如下: private: void __fastcall WndProc(Messages::TMessage & Message) { static bool j(false); if (blMsgOk&&(Message.Msg==WM_KEYDOWN||Message.Msg==WM_KEYUP) ) switch (Message.WParamLo )//取低位字判断键盘消息 { case VK_LEFT : //光标左键 { } ………………//处理代码略去 } TForm::WndProc(Message); } 在窗体类的头文件中加入上面内容,其功能是截取消息,判断如果是按键按下或弹起,则继 续判断是哪些按键进而作出响应的反应,最后将不处理的消息调用默认的 WndProc 函数处 理。 对 Application OnMessage 的重载我们不再举例,只是提醒读者需要注意的是,OnMessage 只有在消息来自消息队列时才会被调用,而用 SendMessage 函数发送的消息不会经过消息队 列,所以重载 OnMessage 对这类消息没有作用。那怎样在 TApplication 中截获包括 SendMessage 发送的消息在内的所有消息呢?前面已经说过,重载 Application 的 WndProc 是 不可能的,但是,WndProc 中对消息的具体处理中是否仍然让我们有机可乘呢?当然,我们 还要查看源码: 第 9 章 系统信息管理程序 264 begin try Message.Result := 0; for I := 0 to FWindowHooks.Count - 1 do if TWindowHook(FWindowHooks[I]^)(Message) then Exit; //注意这里 CheckIniChange(Message); with Message do case Msg of WM_SYSCOMMAND: …………//略去,默认的消息处理方式 我们看到,一开始,WndProc 便调用 HookMainWindow 挂钩定义的消息处理方法,这样,我 们就可以使用 HookMainWindow 来加入自定义的消息处理函数。加入自定义函数的方法如 下: Application->HookMainWindow(MyMessageFun); 它可以放在窗体初始化的事件处理代码中,其中 MyMessageFun 是自定义的函数名。需要注 意的是,程序结束时需要卸载挂钩程序,如下: Application->UnhookMainWindow(AppHookFunc); 总结一下,我们可以使用的消息拦截实际上可以有 5 种,其中重载 DefaultHandler 的方 法不适用,而 Application->OnMessage 的方法又有拦截不到 SendMessage 消息的缺点,所以 实际上可以使用的拦截方法有三种。其中最先拦截到消息的当然是Application中的挂钩函数, 其次是 TObject 的 WndProc 函数,Dispatch 获得消息的时间在三种方法中最晚。 9.1.4 自定义消息的发送 跟 Windows 向应用程序发送消息一样,也可以在窗口与控件之间发送消息。 常用到的消息相关的 API 函数是 SendMessage 和 PostMessage,它们的声明如下: LRESULT SendMessage( HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam); BOOL PostMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); 它们的作用都是根据一个窗口的句柄向该窗口发送一个消息,参数中 hWnd 是消息到达的窗 口的句柄,Msg 是消息的类型标志,后面两个参数都是 32 位的消息附加信息。 最值得注意的是它们两个之间的区别,其中 SendMessage 直接把一个消息发给窗口过程, 等消息被处理后才返回;而 PostMessage 只是把消息发送到消息队列,完成后即返回。也就 是说 SendMessage 直接把消息发送给消息处理过程,并没有经过消息队列,这也是不能通过 TApplication 的 OnMessage 处理这类消息的原因。 另外,C++Builder 中还提供了一种非 API 的消息传递函数 Perform,VCL 的 Perform()方 法适用于所有的 TControl 派生对象。Perform()可以向任何一个窗件或控件发送消息,只需要 知道窗体或控件的实例,它的声明如下: int __fastcall Perform(unsigned Msg, int WParam, int LParam); Perform()把 3 个参数组装成 TMessage 记录,然后调用 Dispatch()方法把消息传递给 Windows 第 9 章 系统信息管理程序 265 的消息系统,并且等消息得到处理后才返回。 9.1.5 API 的应用 消息的处理是 API 应用的基础,而消息处理过程清楚以后,剩下的工作就只是找到合适 的 API 函数来实现自己的需求了。API 函数相当繁杂,熟悉 API 函数的使用当然不是一天两 天的事,这里面要注意对 C++Builder 的帮助文件的利用,所有的 API 函数都可以在帮助文件 中找到,有不懂的就马上查帮助。 其实在以前的章节我们已经用到了一些 API 函数,比如第六章中查找文件时用的 FindFirst、FindNext 等函数,况且本章的系统信息管理程序要频繁的调用与系统相关的 API 函数,所以这里我们不再举出 API 函数应用的实例。 9.2 界面设计 9.2.1 主窗体界面设计 系统信息相当繁杂,我们分别从操作系统的环境、系统设备等方面对信息进行分类,然 后利用多页组件,将它们显示在不同的页中。 在窗体中添加一个 TPageControl 组件,一个状态栏(TStatusBar)组件。设置 PageControl1 的 Align 为 alClient,Stytle 属性为 tsFlatButtons,TabPosition 为 tpTop(选页按钮在上方)。为 PageControl 新添六个页面,此时,设计窗体如图 9-1 所示: 图 9-1 系统信息管理程序窗体框架图 由于页面较多,所以每个页面上的组件的添加,放在后面代码实现的部分讲述。 在使用 TPageControl 组件的时候,注意下面几点: z 要为 PageControl 添加页面,只需在 PageControl 上单击右键,选择弹出菜单中的“New 第 9 章 系统信息管理程序 266 Page”项即可。 z 鼠标点击 PageControl 中每个页面的标题时,会将点中的页面作为 PageControl 的默认激 活页面,而且此时选中的组件不是 PageControl 中的页面,而是整个 PageControl 组件, 如果要选中各个页面,需要在点击 PageControl 中页面标题之后,再点击标题下方的页面 区域。 z 在选中页面区域之后,才能用鼠标为该页面增添组件。 z 每个页面都有一个 PageIndex 属性,它决定该页面在 PageControl 中的排列顺序。 z PageControl 中的 ActivePage 属性决定当前的激活页面。 9.2.2 程序总体结构 由于使用多页组件,所以程序要根据多页组件中页面的选择而对选定页面的组件进行操 作,来显示系统信息。我们在 PageControl 的 OnChange 事件中编写 switch 结构来实现这个功 能,代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::PageControl1Change(TObject *Sender) { switch(PageControl1->ActivePageIndex) { case 0: ListView1->Clear(); EnumWindows((WNDENUMPROC)MyGetWindows,0); StatusBar1->SimpleText="右键菜单可以清除、刷新列表,终止选定进程"; break; case 1: ListView2->Clear(); MyGetProcess(); StatusBar1->SimpleText="右键菜单可以清除、刷新列表,终止选定进程"; break; case 2: ComboBox1->Items->Clear(); ClearDiskInfo(); MyDiskInfoFun(); StatusBar1->SimpleText="从组合框中选择指定磁盘,查看磁盘信息"; break; case 3: MyMemoryInfo(); StatusBar1->SimpleText="显示内存信息"; 第 9 章 系统信息管理程序 267 break; case 4: MyDeviceInfo(); StatusBar1->SimpleText="显示计算机名、用户名、CPU、显示器信息"; break; case 5: MyEnviInfo(); StatusBar1->SimpleText="显示环境变量,点击下方按钮可修改选定项值"; break; default: break; } } //--------------------------------------------------------------------------- 将每个页面激活时的处理过程单独编写为一个或几个函数,对这些函数的具体实现在后 面的小节中将一一列出。 9.3 窗口和进程 图 9-2 窗口页面设计图 第 9 章 系统信息管理程序 268 图 9-3 进程页面设计图 9.3.1 页面中组件的添加 在窗口和进程页面中,我们要实现对当前运行的窗口和进程的列表显示,所以,在窗口 页面和进程页面分别添加一个 TListView 组件,它们的 Name 分别为 ListView1 和 ListView2, 设置它们的 Align 属性为 alClient,ViewStyle 属性为 vsReport,在 ListView 组件上点击右键, 选择 Columns Editor,为 ListView 添加纵列,并设置它们的标题和宽度,使得界面如图 9-2 所示: 最后,添加一个弹出菜单(TPopumMenu),设置其菜单项如下: 图 9-4 右键菜单设计图 将 ListView1 和 ListView2 的 PopupMunu 属性为 PopupMenu1。 9.3.2 当前窗口的获取 完成界面设计以后,就可以编写函数获取当前的窗口,然后添加到列表视图中。获取窗 口,我们在这里使用 API 函数 EnumWindows,其声明如下: BOOL EnumWindows( WNDENUMPROC lpEnumFunc, //函数指针,指向回调函数 LPARAM lParam // 需要传递给回调函数的数据,我们这里用不到它 ); 第 9 章 系统信息管理程序 269 WNDENUMPROC 是一个宏定义,它定义的回调函数的格式如下: BOOL CALLBACK EnumWindowsProc( HWND hwnd, // 父窗口句柄 LPARAM lParam // 程序传递回来的值 ); EnumWindows 函数遍历所有的窗口,然后对每个窗口调用回调函数,并将窗口的句柄和一个 传递值传给回调函数,此遍历的结束条件是回调函数返回值为 false。知道了窗口的句柄,我 们就可以利用其它 API 函数轻松的得到窗口的标题,窗口类等信息。 由上面分析,我们的工作就是在 9.2.2 节中的 switch 结构中调用 EnumWindows 函数,并 且编写它的回调函数,在回调函数中,根据传递回来的窗口句柄获得窗口的标题等信息,然 后添加到 ListView1 中。 需要注意的是,EnumWindows 会传回所有可见、不可见的窗口,一般情况下,我们在窗 口浏览中只想看到可见的窗口,所以在回调函数中,我们可以使用 IsWindowVisible 这个 API 函数将不可见窗体过滤掉。 我们定义回调函数的函数名为 MyGetWindows,其代码如下: //--------------------------------------------------------------------------- bool MyGetWindows(HWND hWnd,LPARAM lp) { unsigned long* pPid; //LPDWORD unsigned long result; //DWORD void *hg; //HGLOBAL unsigned long id; if(hWnd==NULL) return false; hg = GlobalAlloc(GMEM_SHARE,sizeof(unsigned long)); pPid = (unsigned long *)GlobalLock(hg); result = GetWindowThreadProcessId(hWnd,pPid); if(result) { // 获得窗口的 Caption int Length = (int)SendMessage(hWnd,WM_GETTEXTLENGTH,0,0); if(Length) { char *buf = new char[Length+2]; buf[Length] = '\0'; buf[Length+1] = '\0'; SendMessage(hWnd,WM_GETTEXT,Length+1,(LPARAM)buf); //GetWindowText(hWnd,buf,Length+1); 第 9 章 系统信息管理程序 270 if(AnsiString(buf)!=EmptyStr && AnsiString(buf)!="Default IME") {//过滤掉窗口标题为空和输入法的窗口 char *ClassnameBuf=new char[256]; if(IsWindowVisible(hWnd)) {//过滤掉不可见的窗口 //在 ListView 添加窗口的标题 TListItem *NewItem=Form1->ListView1->Items->Add(); id=*pPid; NewItem->Caption=AnsiString(buf); NewItem->SubItems->Add(AnsiString(id)); //获得并在 ListView 中添加窗口类名 GetClassName(hWnd,ClassnameBuf,255); NewItem->SubItems->Add(AnsiString(ClassnameBuf)); } } delete buf; } } else { GlobalUnlock(hg); GlobalFree(hg); return false; } GlobalUnlock(hg); GlobalFree(hg); return true; } //-------------------------------------------------------------- 为了美化显示效果,可以为每个列表项添加小图标,即添加一个 ImageList,然后设置列 表项的 NewItem->ImageIndex 属性。可以参看第六章中的相关内容。 第 9 章 系统信息管理程序 271 图 9-5 窗口列表效果图 在视图中点击右键菜单的选项,可以实现对窗口列表的清除和刷新,以及对选定窗口进 程的终止,对于右键菜单的代码,放在 9.3.3 中。 窗口列表的运行效果如图 9-5 所示。 9.3.3 当前进程的获取 进程信息的获取需要用到一组 ToolHelp API 函数,使用这组函数需要先添加头文件: #include 使用 ToolHelp API 函数之前,需要先创建一个当前进程、模块、堆、线程等信息的快照, 即调用 CreateToolhelp32Snapshot 函数,这个函数的声明如下: HANDLE WINAPI CreateToolhelp32Snapshot(DWORD dwFlags, DWORD th32ProcessID); 其中 dwFlags 指定需要获得哪类信息的快照,经常用到的有: TH32CS_SNAPHEAPLIST 快照中包含指定进程的堆列表 TH32CS_SNAPMODULE 快照中包含指定进程的模块的列表 TH32CS_SNAPPROCESS 快照中包含当前激活的所有进程 TH32CS_SNAPTHREAD 快照中包含当前所有线程的列表 TH32CS_SNAPALL 相当与上面四种值的综合 另一个参数 th32ProcessID 用来指定需要获得堆、模块信息的进程,如果取 0,则标示当前所 第 9 章 系统信息管理程序 272 有的进程。它只在 dwFlags 指定需要堆或模块列表时才有效。 获得快照之后,需要使用一组 First、Next 函数来获取快照中更详细的信息。如果要查询 堆的相关信息,应该用 Module32First、Module32Next 函数;如果要查询进程信息,应使用 Process32First、Process32Next 函数;如果要查询线程相关的信息,应使用 Thread32First、 Thread32Next 函数等。 在快照中的查询操作结束以后,需要使用 CloseHandle 函数释放快照所占用的资源。 本程序中需要获取进程信息,并显示在 ListView2 中,这步操作放在函数 MyGetProcess 中,此函数的代码如下: //-------------------------------------------------------------- void MyGetProcess(void) { TListItem *NewItem; AnsiString ExeFile; //存放进程对应的 exe 文件的文件名 PROCESSENTRY32 processinfo; //进程信息结构体 processinfo.dwSize = sizeof(processinfo); //获取系统快照 HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0); if(snapshot == NULL) return; bool flag = Process32First(snapshot,&processinfo); while (flag) { NewItem = Form1->ListView2->Items->Add(); ExeFile = AnsiString(processinfo.szExeFile); NewItem->Caption = ExeFile; //进程的 ID,即 PID NewItem->SubItems->Add(IntToStr(int(processinfo.th32ProcessID))); //父进程的 PID NewItem->SubItems->Add(IntToStr(int(processinfo.th32ParentProcessID))); DWORD dwSize,dwSize2; //获取可执行文件的版本等信息 dwSize = GetFileVersionInfoSize(ExeFile.c_str(),&dwSize2); if(dwSize != 0) { Pointer pt = malloc(dwSize); 第 9 章 系统信息管理程序 273 Pointer pt2; unsigned int s; GetFileVersionInfo(ExeFile.c_str(),NULL,dwSize,pt); if(VerQueryValue(pt,"\\StringFileInfo\\040904E4\\FileVersion",&pt2,&s)) NewItem->SubItems->Add(PChar(pt2)); if(VerQueryValue(pt,"\\StringFileInfo\\040904E4\\CompanyName",&pt2,&s)) NewItem->SubItems->Add(PChar(pt2)); if(VerQueryValue(pt,"\\StringFileInfo\\040904E4\\FileDescription",&pt2,&s)) NewItem->SubItems->Add(PChar(pt2)); free(pt); } flag = Process32Next(snapshot,&processinfo); } CloseHandle(snapshot); } //---------------------------------------------------------------- 函数中用到了 GetFileVersionInfo 这个 API 函数,它可以根据 exe 文件的文件名,获取与 该程序相关的版权厂商等信息,具体各个信息的分离提取通过函数 VerQueryValue 函数实现。 进程列表的运行效果如图 9-6: 图 9-6 进程列表效果图 第 9 章 系统信息管理程序 274 9.3.4 右键菜单和进程的终止 由于 ListView1 和 ListView2 使用同一个右键弹出菜单,所以,在菜单项的 OnClick 事件 的处理中,需要根据当前激活页面的不同做相应的处理。 其中,清除列表视图操作的代码如下: //---------------------------------------------------------------- void __fastcall TForm1::PopMenuClearClick(TObject *Sender) { switch(PageControl1->ActivePageIndex) { case 0: ListView1->Clear(); break; case 1: ListView2->Clear(); break; default : break; } } //--------------------------------------------------------------------- 刷新列表视图的操作实现起来很简单,因为只需要调用 PageControl 的 OnChange 事件即 可,代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::PopMenuRefreshClick(TObject *Sender) { PageControl1->OnChange(Sender); } //--------------------------------------------------------------------------- 对于杀死进程的操作,我们先定义函数 void __fastcall KillPID(TListView *tmpLV),函数 中根据 tmpLV 中被选择的列表项中 PID 字段的值得到需要杀死的进程的 PID,然后调用 API 函数 OpenProcess 通过 PID 来获得该进程的句柄,再由 API 函数 TerminateProcess 来杀死进 程,由于窗口列表和进程列表中都给出了 PID,所以这里可以用同一个函数 KillPID 来实现 杀死进程的操作,只需要根据激活页的不同,给 KillPID 传递列表试图的指针即可。 KillPID 的代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::KillPID(TListView *tmpLV) 第 9 章 系统信息管理程序 275 { if(tmpLV->SelCount==0) { MessageDlg("请先选定一个进程!", mtWarning, TMsgDlgButtons() << mbOK, 0); return; } int pPid=StrToInt(tmpLV->Selected->SubItems->Strings[0]); HANDLE ps = OpenProcess(1,false,pPid); if(ps) { if(!TerminateProcess(ps,-9)) { MessageDlg("进程不能被终止", mtInformation, TMsgDlgButtons() << mbOK, 0); } else { MessageDlg("进程已经被终止", mtInformation, TMsgDlgButtons() << mbOK, 0); } } else { MessageDlg("选定进程不允许访问或没有响应", mtInformation, TMsgDlgButtons() << mbOK, 0); } } //--------------------------------------------------------------------------- 鼠标右键的响应代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::PopMenuKillClick(TObject *Sender) { switch(PageControl1->ActivePageIndex) { case 0: KillPID(ListView1); break; case 1: 第 9 章 系统信息管理程序 276 KillPID(ListView2); break; default : break; } } //--------------------------------------------------------------------------- 9.4 系统与设备 本节讲述系统与设备信息的获取和管理,包括磁盘信息、内存、CPU、显示器等。 9.4.1 磁盘驱动器 本页面中组件的添加如图 9-7。 在此页面被激活时,需要搜索计算机上可用的磁盘,包括软驱、光驱、本地硬盘、可移 动硬盘以及网络硬盘等,将它们的盘符添加到组合框 ComboBox1 中。其中 ComboBox1 的 Style 属性设为 csDropDownList,即只允许从下拉菜单中选择选项,不允许用户输入内容。 需要注意的一个组件时绘制饼图的 TChart 组件,它在选项卡的 Additional 页中,添加组 图 9-7 磁盘驱动器页面设计图 第 9 章 系统信息管理程序 277 件之后,双击该组件,弹出“Editing Chart1”对话框,在对话框中 Chart->Series 页中选择“Add” 添加一个 Pie 图,然后再在 Axis、Legend 等页面中将 Chart 的坐标轴、图例等去掉,得到如 图 9-7 所示的效果。注意,图 9-7 中的图例不是 Chart 组件中的,而是我们自己用 TShape 和 TLabel 组件实现的,之所以不用 Chart 的图例,是因为我们需要图例后面不仅仅是出来数值, 而且需要数值的输入按照一定的格式,并且数值后面还有单位。 本程序中各个 Label 的名字都是以 Label 开头,后面是它需要显示的内容的名字,在代 码中很容易看书来是哪个 Label,所以这里不再多说。 如前所述,当本页面激活时,先用函数 MyDiskInfoFun 更新组合框中的选项,然后根据 用户对组合框中盘符的选择所触发的 ComboBox1 的 OnChange 事件,改变页面中磁盘大小等 信息的显示。需要注意的是,选择盘符的时候,如果选择了软驱,而且软驱中没有软盘,那 么就会使得某些项的信息没有,如果选择软驱时没有先将之前选择的磁盘的信息都清空,会 对信息显示造成影响,所以我们单独定义一个用于清空显示信息的函数 ClearDiskInfo。 这三个函数的代码如下: //--------------------------------------------------------------------------- void ClearDiskInfo(void) { //清空所有 Label、Edit 组件的显示(组合框的清空在每次竟如选项页时执行, //Chart1 的清空在 Chart1 绘图中进行) Form1->LabelUsed->Caption="已用空间:"; Form1->LabelFree->Caption="可用空间:"; Form1->LabelTotal->Caption="总容量:"; Form1->Chart1->Series[0]->Clear(); Form1->EditDiskVolume->Text=""; Form1->LabelStyle->Caption="磁盘类型:"; Form1->LabelFileSystem->Caption="文件系统:"; Form1->LabelSN->Caption="序列号:"; Form1->LabelMaxFileName->Caption="文件名最大长度:"; Form1->LabelLongFileName->Caption="是否支持长文件名:"; Form1->LabelSectorsPerCluster->Caption="每簇的扇区数:"; Form1->LabelBytesPerSector->Caption="每扇区的字节数:"; Form1->LabelClusters->Caption="簇总计:"; Form1->LabelFreeClusters->Caption="簇剩余:"; } void MyDiskInfoFun(void) { static char * Drive_Letter[]={"a:\\","b:\\","c:\\","d:\\","e:\\","f:\\", "g:\\","h:\\","i:\\","j:\\","k:\\","l:\\","m:\\","n:\\","o:\\","p:\\", "q:\\","r:\\","s:\\","t:\\","u:\\","v:\\","w:\\","x:\\","y:\\","z:\\"}; //获得系统中可使用的磁盘的盘符,添加到组合框中 for(int x =0; x <= 25; x++ ) 第 9 章 系统信息管理程序 278 { if(GetDriveType(Drive_Letter[x])!=1) Form1->ComboBox1->Items->Add(AnsiString(Drive_Letter[x])); } //组合框默认情况下没有选择项,所以这里不再做处理,而是等待组合框的选择消息 //选择了磁盘之后,根据所选择的磁盘对其它组件进行调整 } //--------------------------------------------------------------------------- void __fastcall TForm1::ComboBox1Change(TObject *Sender) { AnsiString path=ComboBox1->Text; if(GetDriveType(path.c_str())==1) { MessageDlg("选取的磁盘不存在!", mtWarning, TMsgDlgButtons() << mbOK, 0); ComboBox1->Text=""; return; } //清除之前显示的其它盘的信息 ClearDiskInfo(); //判断磁盘的类型 switch(GetDriveType(path.c_str())) { case DRIVE_REMOVABLE://可移动硬盘 LabelStyle->Caption=AnsiString("磁盘类型:")+"可移动磁盘"; break; case DRIVE_FIXED: LabelStyle->Caption=AnsiString("磁盘类型:")+"本地磁盘"; break; case DRIVE_REMOTE: LabelStyle->Caption=AnsiString("磁盘类型:")+"网络磁盘"; break; case DRIVE_CDROM: LabelStyle->Caption=AnsiString("磁盘类型:")+"光驱"; break; case DRIVE_RAMDISK: LabelStyle->Caption=AnsiString("磁盘类型:")+"RAM 磁盘"; break; case 0: 第 9 章 系统信息管理程序 279 LabelStyle->Caption=AnsiString("磁盘类型:")+"未知类型磁盘"; break; } //获得磁盘卷标、文件系统、序列号、是否支持长文件名等信息 String volume; volume.SetLength(256); DWORD serialnumber,maxcomponentlength,flags; String filesystem; filesystem.SetLength(256); if(GetVolumeInformation(path.c_str(),volume.c_str(),volume.Length(), &serialnumber,&maxcomponentlength,&flags, filesystem.c_str(),filesystem.Length() ) ) { EditDiskVolume->Text=volume; LabelSN->Caption=AnsiString("序列号:")+serialnumber; LabelFileSystem->Caption=AnsiString("文件系统:")+filesystem; LabelMaxFileName->Caption=AnsiString("文件名最大长度:")+ AnsiString(maxcomponentlength)+"个字符"; if(filesystem == "FAT" && maxcomponentlength != 255) { LabelLongFileName->Caption=AnsiString("是否支持长文件名:")+"否"; } else { LabelLongFileName->Caption=AnsiString("是否支持长文件名:")+"是"; } } //磁盘簇、扇区等信息的获取 DWORD sectorspercluster,bytespersector,clusters,freeclusters; GetDiskFreeSpace(path.c_str(),§orspercluster,&bytespersector, &freeclusters,&clusters); LabelSectorsPerCluster->Caption=AnsiString("每簇的扇区数:")+ AnsiString(sectorspercluster); LabelBytesPerSector->Caption=AnsiString("每扇区的字节数:")+ AnsiString(bytespersector); LabelClusters->Caption=AnsiString("簇总计:")+AnsiString(clusters); LabelFreeClusters->Caption=AnsiString("簇总计:")+AnsiString(freeclusters); //_ULARGE_INTEGER FreeBytesAvailableToCaller, // TotalNumberOfBytes,TotalNumberOfFreeBytes; //GetDiskFreeSpaceEx(path.c_str(),&FreeBytesAvailableToCaller, 第 9 章 系统信息管理程序 280 // &TotalNumberOfBytes,&TotalNumberOfFreeBytes); unsigned char num=ComboBox1->Items->Strings[ComboBox1->ItemIndex][1]-'a'+1; __int64 total=DiskSize(num); __int64 free=DiskFree(num); if(total<=0)total=0; if(free<=0)free=0; __int64 use=total-free; LabelTotal->Caption=AnsiString("总容量:")+ FormatFloat((AnsiString)"0,000'字节",total)+ FormatFloat(" 0.00GB ",((double)total)/1024.0/1024.0/1024.0); LabelUsed->Caption=AnsiString("已用空间:")+ FormatFloat((AnsiString)"0,000'字节",use)+ FormatFloat(" 0.00GB ",((double)use)/1024.0/1024.0/1024.0); LabelFree->Caption=AnsiString("可用空间:")+ FormatFloat((AnsiString)"0,000'字节",free)+ FormatFloat(" 0.00GB ",((double)free)/1024.0/1024.0/1024.0); //绘制饼图 Chart1->Series[0]->Clear(); Chart1->Series[0]->Add(use,"已用空间",clBlue); Chart1->Series[0]->Add(free,"可用空间",clFuchsia); } //--------------------------------------------------------------------------- 其中用到的 FormatFloat 是指定数据的输出格式。API 函数 GetDriveType 根据磁盘盘符 判断磁盘的类型,如果磁盘不存在,则返回 1,否则,返回一个代表不同磁盘类型的值,各 个取值及其代表的磁盘类型,在程序中都可以看到。GetVolumeInformation 和 GetDiskFreeSpace 等 API 函数的使用,根据代码中对其的应用,相信也都很容易看出它们的 作用和使用方法。 页面中卷标显示后边有个按钮,用它可以对磁盘卷标进行更改,其实现也是依靠对 API 函数的调用,代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::Button1Click(TObject *Sender) { //更改卷标 if(ComboBox1->Text=="") { MessageDlg("请选择磁盘!", mtWarning, TMsgDlgButtons() << mbOK, 0); return; } 第 9 章 系统信息管理程序 281 if(!SetVolumeLabel(ComboBox1->Text.c_str(),EditDiskVolume->Text.c_str())) { MessageDlg("更改卷标失败!", mtWarning, TMsgDlgButtons() << mbOK, 0); return; } else { MessageDlg("卷标更改成功!", mtInformation, TMsgDlgButtons() << mbOK, 0); return; } } //--------------------------------------------------------------------------- 磁盘驱动器页面的运行效果如图 9-8: 图 9-8 磁盘驱动器页面运行效果 第 9 章 系统信息管理程序 282 9.4.2 内存 内存页面的组件添加后,如图 9-9 所示。 这个页面做的比较简陋,当然你也可以象磁盘空间显示那样用一个 Chart 饼图来显示它, 这里重点讲述如何取得内存信息,所以就用一些 Label 来显示信息。 本页面激活时,调用 MyMemoryInfo 函数来获得和显示内存信息,其代码如下: //--------------------------------------------------------------------------- void MyMemoryInfo(void) { int total,use,avail; MEMORYSTATUS memory; memory.dwLength = sizeof (memory); GlobalMemoryStatus(&memory); Form1->LabelMemoryLoad->Caption=AnsiString(int(memory.dwMemoryLoad))+"%"; total=(int)(memory.dwTotalPhys/1024); avail=(int)(memory.dwAvailPhys/1024); Form1->LabelTotalPhys->Caption=FormatFloat((AnsiString)"0,000'KB",total); Form1->LabelAvailPhys->Caption=FormatFloat((AnsiString)"0,000'KB",avail); 图 9-9 内存页面设计图 第 9 章 系统信息管理程序 283 total=(int)(memory.dwTotalVirtual/1024); avail=(int)(memory.dwAvailVirtual/1024); use=total-avail; Form1->LabelTotalVirtual->Caption=FormatFloat((AnsiString)"0,000'KB",total); Form1->LabelAvailVirtual->Caption=FormatFloat((AnsiString)"0,000'KB",avail); Form1->LabelUsedVirtual->Caption=FormatFloat((AnsiString)"0,000'KB",use); Form1->LabelTotalPageFile->Caption=FormatFloat((AnsiString)"0,000'KB", (int)(memory.dwTotalPageFile/1024)); Form1->LabelAvailPageFile->Caption=FormatFloat((AnsiString)"#,###'KB", (int)(memory.dwAvailPageFile/1024)); } //----------------------------------------------------------------------- 其中用到 API 函数 GlobalMemoryStatus,它的参数是一个结构指针,返回信息就存储在 此结构中。 内存页面的运行效果如图 9-10。 9.4.3 设备 设备页面中,显示 CPU 的类型和频率,显示器的分辨率,刷新频率,色彩,以及计算机 名称和当前的用户名。 设备页面的设计如图 9-11。 第 9 章 系统信息管理程序 284 图 9-10 内存页面的运行效果 其中大部分是用 Label 来显示设备信息,计算机名一项用 Edit 组件,允许用户通过更改 按钮对计算机名进行更改。 获得计算机名的 API 函数是 GetComputerName ,更改计算机名的 API 函数为 SetComputerName。 由 GetSystemInfo 函数可以得到一个 SYSTEM_INFO 结构类型的信息,从其中可以得到 与 CPU 相关的很多信息,其定义如下: typedef struct _SYSTEM_INFO { // sinf union { DWORD dwOemId; struct { WORD wProcessorArchitecture; WORD wReserved; }; }; DWORD dwPageSize; LPVOID lpMinimumApplicationAddress; LPVOID lpMaximumApplicationAddress; DWORD dwActiveProcessorMask; DWORD dwNumberOfProcessors; 第 9 章 系统信息管理程序 285 图 9-11 设备页面设计图 DWORD dwProcessorType; DWORD dwAllocationGranularity; WORD wProcessorLevel; WORD wProcessorRevision; } SYSTEM_INFO; 各个项的意义由其名称可以大概看得出来,但是它们的值的意义随操作系统等环境的不 同而不同,所以,具体请自己参看 C++Builder 的帮助文件。 显示器相关信息的获取使用 EnumDisplaySettings 函数,其声明如下: BOOL EnumDisplaySettings( LPCTSTR lpszDeviceName, DWORD iModeNum, LPDEVMODE lpDevMode ); 其中第一个参数定义显示设备,如果为 NULL,则默认指当前程序的输出设备;第二个参数 指定图形模式,第三个参数是储存程序返回信息结构指针,它是_devicemode 结构类型,此 结构中元素很多,这里不一一列举。 响应设备页面激活事件的函数为 MyDeviceInfo,其代码如下: void MyDeviceInfo(void) { //获得计算机名和当前用户名 第 9 章 系统信息管理程序 286 char name[MAX_COMPUTERNAME_LENGTH + 1]; DWORD size = MAX_COMPUTERNAME_LENGTH + 1; if(GetComputerName(name,&size)) Form1->EditComputerName->Text=AnsiString(name); if(GetUserName(name,&size)) Form1->LabelUser->Caption=AnsiString(name); //获得 CPU 的类型, SYSTEM_INFO 含有很多的信息,这里只用了处理器的类型一项 SYSTEM_INFO systeminfo; GetSystemInfo(&systeminfo); switch(systeminfo.dwProcessorType) { case PROCESSOR_INTEL_386: Form1->LabelCPU->Caption="Intel 386"; break; case PROCESSOR_INTEL_486: Form1->LabelCPU->Caption="Intel 486"; break; case PROCESSOR_INTEL_PENTIUM: Form1->LabelCPU->Caption="Pentium"; break; case PROCESSOR_MIPS_R4000: Form1->LabelCPU->Caption="MIPS"; break; case PROCESSOR_ALPHA_21064: Form1->LabelCPU->Caption="Alpha"; break; default: Form1->LabelCPU->Caption="未知类型"; break; } //计算 CPU 的速度 int CPURate=Frequence_MHz(); Form1->LabelCPURate->Caption=AnsiString(CPURate)+"MHz"; //获取显示器相关的信息 TDeviceMode lpDevMode; EnumDisplaySettings(NULL,ENUM_CURRENT_SETTINGS,&lpDevMode); Form1->LabelFenBianLv->Caption=String(lpDevMode.dmPelsWidth) + " x " + String(lpDevMode.dmPelsHeight); Form1->LabelRefresh->Caption=String(lpDevMode.dmDisplayFrequency)+" Hz"; switch(lpDevMode.dmBitsPerPel) { case 2: Form1->LabelColor->Caption="4 色 "; break; case 4: Form1->LabelColor->Caption="16 色 ";break; case 8: 第 9 章 系统信息管理程序 287 Form1->LabelColor->Caption="256 色";break; case 16: Form1->LabelColor->Caption="16 位增强色";break; case 24: Form1->LabelColor->Caption="24 位真彩色";break; case 32: Form1->LabelColor->Caption="32 位真彩色";break; default: break; } } //--------------------------------------------------------------------------- 更改计算机名操作的代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::Button2Click(TObject *Sender) { if(!SetComputerName(EditComputerName->Name.c_str())) { MessageDlg("更改计算机名失败!", mtWarning, TMsgDlgButtons() << mbOK, 0); } } //--------------------------------------------------------------------------- 其中,计算 CPU 频率的函数中,用到汇编语言,其函数代码如下: //----------------------------------------------------------------------- int __fastcall Frequence_MHz() { LARGE_INTEGER ulFreq, ulTicks, ulvalue, ulResult; __int64 ulEAX_EDX, ulStartCounter; DWORD PriorityClass, Priority; PriorityClass = GetPriorityClass(GetCurrentProcess()); Priority = GetThreadPriority(GetCurrentThread()); SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); QueryPerformanceFrequency(&ulFreq); QueryPerformanceCounter(&ulTicks); ulvalue.QuadPart = ulTicks.QuadPart + ulFreq.QuadPart; 第 9 章 系统信息管理程序 288 _asm { rdtsc mov DWORD PTR ulEAX_EDX, EAX mov DWORD PTR (ulEAX_EDX + 4), EDX } ulStartCounter = ulEAX_EDX; // 循环 1 秒 do { QueryPerformanceCounter(&ulTicks); } while (ulTicks.QuadPart <= ulvalue.QuadPart); _asm { rdtsc mov DWORD PTR ulEAX_EDX, EAX mov DWORD PTR (ulEAX_EDX + 4), EDX } ulResult.QuadPart = ulEAX_EDX - ulStartCounter; SetThreadPriority(GetCurrentThread(), Priority); SetPriorityClass(GetCurrentProcess(), PriorityClass); return (int)ulResult.QuadPart / 1000000; } //------------------------------------------------------------------------------- 对显示器设置的更改也可以通过调用 API 函数来实现,这个函数是 LONG ChangeDisplaySettings(LPDEVMODE lpDevMode, DWORD dwflags );该函数中也需要一 个 DEVMODE 结构指针作为参数,调用它时,需要填写 DEVMODE 结构的 dmFields 域,告 诉 ChangeDIsplaySettings 函数需要改变显示器哪方面的属性。 设备页面的运行效果如图 9-12: 第 9 章 系统信息管理程序 289 图 9-12 设备页面运行效果 9.5 环境变量 在编程过程中经常需要对环境变量进行读取和更改,比如安装程序对系统目录的判断 等。这里我们介绍 GetEnvironmentStrings 和 SetEnvironmentStrings 两个读写环境变量的 API 函数的使用。 环境变量页面的设计如图 9-13。 其中添加一个 TListBox 组件,设置它的 Align 属性为 alTop,然后在其下方添加一个 Edit, 用来显示选中的环境变量以及其值并供用户对其做出修改,最后的按钮用来确认对环境变量 的修改。 环境变量的显示由 MyEnviInfo 函数来完成,另外要对 ListBox1 的 OnClick 事件进行处 理使得下方的 Edit 组件适时显示被选中的环境变量,对环境变量的修改由确认修改按钮的 OnClick 过程完成。 代码如下: //--------------------------------------------------------------------------- void MyEnviInfo(void) { Form1->ListBox1->Items->Clear(); char *strings=GetEnvironmentStrings(); for(int index=0;strings[index]!=0;index+=strlen(&strings[index])+1) 第 9 章 系统信息管理程序 290 图 9-13 环境变量页面设计图 Form1->ListBox1->Items->Add(&strings[index]); } void __fastcall TForm1::ListBox1Click(TObject *Sender) { EditNewEnvi->Text=ListBox1->Items->Strings[ListBox1->ItemIndex]; } //--------------------------------------------------------------------------- void __fastcall TForm1::Button3Click(TObject *Sender) { SetEnvironmentVariable(ListBox1->Items->Names[ListBox1->ItemIndex].c_str(), EditNewEnvi->Text.c_str()); } //--------------------------------------------------------------------------- 另外,如果指向取得某一指定变量名的环境变量值,可以使用 GetEnvironmentVariable 函数,其声明如下: 第 9 章 系统信息管理程序 291 DWORD GetEnvironmentVariable( LPCTSTR lpName, // 字符串指针,指向环境变量名 LPTSTR lpBuffer, // 返回的环境变量的值所储存的内存指针 DWORD nSize // Buffer 的空间大小 ); 系统信息页的运行效果如图 9-14: 图 9-14 环境变量运行效果 9.6 思考题 z 在程序中,使用哪个函数广播一个消息最好,为什么? z 如何终止一个进程?哪些进程是不允许被终止的? z 本章的程序最对窗口的显示都做了哪些过滤? z 你还知道哪些 CPU 频率的计算方法?比较一下哪种方法的效果最好。 《C++ Builder 6 编程实例精解 赵明现》 第 10 章 FTP工具制作 本章重点 本章通过 FTP 工具的制作讲述 TNMFTP 组件的使用,并复习 TListView 组件的使用和工 具栏的使用。 学习目的 通过本章的学习,您可以: ■ 了解 FTP 的的使用 ■ 掌握 TNMFTP 组件的使用 ■ 复习 TListView 组件的使用 ■ 复习文件、文件夹等的操作 第 10 章 FTP工具制作 293 本章典型效果图 登陆信息对话框 从 FTP 服务器下载文件 第 10 章 FTP工具制作 294 10.1 FTP 概述 FTP 是互联网中至今仍在使用的最古老的协议之一,在互联网发展的早期,用 FTP 传输 文件可以占到整个互联网通信量的三分之一,就是现在,FTP 仍然是网络传输中一个很重要 的技术。 FTP 适用与异构体系之间的文件传输,它减少和消除了在不同操作系统下处理文件的不 兼容性。它为用户提供了一个交互接口,当用户通过 FTP 与一台计算机通信时,实际上是在 与 FTP 提供的命令接口进行通信,FTP 向用户发出一条提示,用户参照提示发出新的命令, FTP 执行接受到的命令并发出新的一条提示。 在登陆到一个 FTP 之前,需要向服务器发送有效的用户名和密码,不同的用户拥有对服 务器上文件的不同的访问权限,如果不能提供有效的用户名和密码,FTP 将拒绝与客户机之 间的连接。很多 FTP 都支持匿名登陆,即登录名为 anonymous,且需要用户提交自己的电子 邮箱作为密码,以便出现问题时,FTP 服务器可以通过电子邮件与客户进行联系。 FTP 协议虽然统一了计算机之间的通信机制,但是并没有提供统一的操作界面,不同的 FTP 软件为用户提供的可用命令不尽相同,不过,大多软件都支持 BSD UNIX 系统的 FTP 版本提供的用户命令。BSD 支持的命令列表如下: 表 10-1 FTP 命令列表 ! cr macdef proxy sendport $ delete mdelete put status account debug mdir pwd struct append dir mget quit sunique ascii disconnect mkdir quote tenex bell form mid recv trace binary get mode remotehelp type bye glob mput rename user case hash nmap reset verbose cd help ntrans rmdir ? cdup lcd open runique close ls prompt send 其中 cd 和 cdup 用于目录控制,cd 命令将当前目录切换到指定的目录,cdup 是将当前目 录的上级目录切换为当前目录。pwd 命令让服务器列出当前所在的目录。ls 命令用于列出服 务器上当前目录下的文件和子目录。close 和 disconnect 用于终止与当前服务器之间的连接, 但并不退出 FTP 程序,而 quit 和 bye 则是在终止连接之后退出 FTP 程序。help 和?功能相同, 用于列出所有的可以使用的命令。 用于文件传输的命令有 get、mget、put、mput 和 send。put、send 和 get 用于处理单个文 第 10 章 FTP工具制作 295 件的上传和下载,如果需要为上传或下载的文件重新命名,只需要在命令行中输入第二个文 件名作为新的文件名即可。mput 和 mget 用于多个文件的上传和下载,命令中需要给出一个 文件列表,也可以使用通配符,而 put、get、send 中使用通配符不起作用,它仍只是传输符 合通配符要求的第一个文件而已。 10.2 TNMFTP 组件 10.2.1 TNMFTP 组件的功能 TNMFTP 组件用于通过 FTP 协议在互联网或局域网上的 FTP 服务器之间传输文件。使 用该组件需要使用 WSOCK32.DLL,该动态链接库在大多数的操作系统中都已经包含了,例 如 Windows 95,Windows 98,以及 Windows NT 等。 要使用 TNMFTP 组件的传输文件等功能,必须先与 FTP 服务器建立连接。首先,需要 为 TNMFTP 组件设置 Host 和 Port 属性,指定 FTP 服务器的2018香港马会开奖现场和端口;然后,设置 TNMFTP 的UserID和Password属性,提供FTP服务器上一个有效的用户名和密码;最后,调用TNMFTP 的 Connect 方法即可实现与服务器之间的连接。 与服务器建立连接之后,即可使用 TNMFTP 的方法来实现文件的传输等功能。用 TNMFTP 等实现的功能主要有: z 从远程服务器上获得目录列表 与服务器建立连接之后,调用 TNMFTP 的 List 方法,可以获得服务器上当前目录(工 作目录)中的文件和子目录的列表。而且,每列出一项都会触发一次 OnListItem 事件, 可以为此事件编写处理代码实现特定的功能。 z 改变在远程服务器上的目录位置 调用 TNMFTP 的 ChangeDir 方法可以切换工作目录到另一个指定的合法目录。 z 向服务器上载文件 调用 Upload 方法,可以向当前连接的服务器中的工作目录上载文件。Upload 方法需要 两个参数,一个是本地的文件名,另一个是上载到服务器上的文件名。Upload 方法需要 用户具有足够的权限才能执行成功,一般 FTP 服务器的 incoming 目录都允许上载。如 果服务器上已经存在一个同名的文件,此方法将覆盖原来的文件,要实现断点续传可以 使用 UploadUnique 方法,但有的服务器不支持这个功能。 z 从服务器下载文件 要从服务器下载文件,一般需要先调用 TNMFTP 的 List 方法获得文件列表,然后从文 件列表中选择需要下载的文件,将此文件名和下载后保存在本地的路径、文件名一起作 为参数传递给 Download 方法。用 Download 方法下载也需要用户有下载权限,一般服务 器的 Pub 目录都允许下载。如果本地已经存在指定的文件名,那么它将被覆盖。 z 在服务器上创建目录 要在服务器上创建目录,需要用户有相关的权限。要执行创建目录操作,通过调用 MakeDirectory 方法,它需要新创建目录的目录名作为参数。 第 10 章 FTP工具制作 296 z 删除服务器上的目录 如果用户具有删除目录的权限,则可以通过调用 TNMFTP 的 RemoveDir 方法来删除指 定目录,此方法需要指定删除的目录名作为参数。另外,RemoveDir 方法只能删除空目 录,所以要实现对非空目录的删除,需要自己编写代码来实现。 10.2.2 TNMFTP 的属性、方法和事件 TNMFTP 继承自 TPowersocket,其主要的属性有: 表 10-2 TNMFTP 的主要属性 属性 说明 CurrentDir 标示在服务器上的工作目录,当 ChangeDir 方法调用成功时会改变它的值 FTPDirectoryList 只有当 ParseList 为 true 时此属性才能被使用。它存放由 List 方法得到的目 录列表的信息,其中列表信息的各个元素(文件名、大小、属性、修改时 间等)被自动解析,存放在单独的数组中。如果 ParseList 为 false,那么对 列表项的处理应该放在 OnListItem 事件中 ParseList 决定是否将列表信息进行解析并存放在 FTPDirectoryList 属性中 Password 登录到 FTP 服务器的密码。如果没有为 TNMFTP 组件提供密码,则触发 OnAuthenticationNeeded事件;如果密码错误,则触发OnAuthenticationFailed 事件 UserID 登陆远程 FTP 服务器的用户名。如果没有提供用户名,则触发 OnAuthenticationNeeded事件;如果用户名错误,触发OnAuthenticationFailed 事件。UserID 和 Password 必须是相应的 Vendor 指定服务器类型,可以取 NMOS_UNIX、NMOS_WINDOWS 等。如果此 属性取 NMOS_AUTO,则 TNMFTP 组件会自动判断服务器类型 BeenCanceled 只读,标志当前操作是否被取消 BeenTimedOut 只读,标志当前操作是否已超时 BytesRecvd 长整型,只读,从服务器收到的数据大小,单位字节 BytesSent 长整型,只读,向服务器发送的数据大小,单位字节 BytesTotal 长整型,只读,当前与服务器之间传送文件的大小,单位字节 Connected 标志是否已与服务器建立连接 Host FTP 服务器的域名或 IP 2018香港马会开奖现场 LocalIP 本地计算机 IP Port 服务器的端口,FTP 端口默认一般为 21 Proxy 使用的代理服务器的域名或 IP ProxyPort 代理的端口 RemoteIP 服务器的 IP。只有在与服务器建立连接之后此属性才被设定 Status 存放状态信息,OnStatus 事件读取它作为参数 第 10 章 FTP工具制作 297 TimeOut 超时设置,单位微秒。为 0 时表示不进行超时判断 TransactionReply 存放上一个命令后从服务器返回的信息 WSAInfo 包含 Winsock 版本和服务器类型 TNMFTP 组件的主要方法有: 表 10-3 TNMFTP 主要方法 方法 说明 Allocate procedure Allocate(FileSize: Integer);在服务器上为存储文件分配空间, FileSize 为分配的空间大小,单位字节 ChangeDir procedure ChangeDir(DirName: string);改变工作目录。参数 DirName 可以时 当前目录的子目录或者一个完整的目录路径。调用成功之后,CurrentDir 将随之改变,且触发 OnSuccess 事件;如果调用失败,触发 OnFailure 事件。 Trans_Type 为 cmdChangeDir Delete procedure Delete(Filename: string);从服务器上删除文件,参数可以是当前目 录中的文件,或者完整的文件路径。删除成功则触发 OnSuccess 事件,失 败则触发 OnFailure 事件,Trans_Type 为 cmdDelete DoCommand procedure DoCommand(CommandStr: string);给服务器发送命令行命令 Download procedure Download(RemoteFile, LocalFile: string);从服务器上下载文件到 本地计算机。RemoteFile 指定服务器工作目录下需要下载的文件名, LocalFile 指定下载文件存储在本地计算机的文件名。如果本地有同名文件, 将被覆盖。下载成功触发 OnSuccess 事件,否则触发 OnFailure 事件, Trans_Type 为 cmdDownload DownloadRestore procedure DownloadRestore(RemoteFile, LocalFile: string);继续从上次传送 文件时的断点处下载文件。其中 LocalFile 必须是本地计算机已经存在的文 件名,且需要下载的文件已经部分保存在其中。需要注意的是,并不是所 有的 FTP 服务器都支持断点续传功能 List procedure List;获得服务器上工作目录中的子目录和文件的列表,每列出一 项都触发一次 OnListItem 事件。如果 ParseList 属性被设为 true,列表的每 一项都会被解析为文件名、文件大小等元素存放在 FTPDirectoryList 属性 中。如果 List 方法调用成功,则触发 OnSuccess 事件,否则触发 OnFailure 事件,Trans_Type 为 cmdList MakeDirectory procedure MakeDirectory(DirectoryName: string);在服务器工作目录下创建 目录,也可以由 DirectoryName 指定一个完整的路径,在服务器指定位置 创建目录。如果方法调用成功,触发 OnSuccess 事件,否则触发 OnFailure 事件,Trans_Type 为 cmdMakeDir Mode procedure Mode(TheMode: Integer);标志文件在本地计算机和 FTP 服务器之 间传输的模式,TheMode 可取: MODE_ASCII:ASCII 文本方式 MODE_IMAGE:8 为二进制方式 第 10 章 FTP工具制作 298 MODE_BYTE:可变长二进制方式 Nlist procedure Nlist;显示当前工作目录下的文件和目录。每列出一项触发一次 OnListItem 事件。方法调用成功触发 OnSuccess 事件,否则触发 OnFailure 事件,Trans_Type 为 cmdNList Reinitialize procedure Reinitialize;重置与服务器之间的连接,连接重置以后,需要重新 发送用户名和密码,否则所有的操作都会失败。 RemoveDir procedure RemoveDir(DirectoryName: string); 删除服务器上的目录。 DirectoryName 为要删除的目录名,可以是当前工作目录的子目录或者完整 的路径。执行成功,触发 OnSuccess 事件,否则触发 OnFailure 事件, Trans_Type 为 cmdRemoveDir Rename procedure Rename(Filename, FileName2: string);为服务器上的文件或目录重 命名。Filename 是需要服务器上需要重命名的文件,FileName2 是重命名 后的新文件或目录名。它们可以是当前目录下的文件或子目录或者服务器 上完整的路径名。方法调用成功触发 OnSuccess 事件,否则触发 OnFailure 事件,Trans_Type 为 cmdRename Upload procedure Upload(LocalFile, RemoteFile: string);向服务器上传本地计算机上 的文件。方法调用成功触发 OnSuccess 事件,否则触发 OnFailure 事件, Trans_Type 为 cmdUpload UploadAppend procedure UploadAppend(LocalFile, RemoteFile: string);从本地计算机向 FTP 服务器传送文件,如果服务器上已经存在此文件,则将上传的文件追加到 此文件之后。LocalFile 是本地的文件名,RemoteFile 为服务器上的文件名。 方法调用成功触发 OnSuccess 事件,否则触发 OnFailure 事件,Trans_Tyep 为 cmdAppend UploadRestore procedure UploadRestore(LocalFile, RemoteFile: string; Position: Integer);继续 上次中断的文件上载。LocalFile 文本地文件名,RemoteFile 为服务器上的 文件名,此文件须存在且是之前传送中断留下的文件。Position 指定传送操 作的起始位置,字节为单位(RemoteFile 的大小+1byte)。方法调用成功, 则触发 OnSuccess 事件,否则触发 OnFailure 事件,Trans_Type 为 cmdUpRestore UploadUnique procedure UploadUnique(LocalFile: string);向服务器上载同名文件,参数 LocalFile 指定将要上载的文件在本地和服务器上的文件名。如果服务器上 已经存在同名文件,则上载的文件将被自动指定另一文件名。方法调用成 功触发OnSuccess事件,否则触发OnFailure事件,Trans_Type 为cmdUpLoad Cancel procedure Cancel;取消当前的操作,并断开与服务器之间的连接 Connect procedure Connect; virtual;与服务器建立连接 Disconnect procedure Disconnect; virtual;与服务器断开连接 GetLocalAddress function GetLocalAddress: String;获得本地计算机的 IP 2018香港马会开奖现场 TNMFTP 组件的事件在编程中很重要,因为与服务器之间的操作随网络等状况的不同会 有延迟,所以对用户操作是否成功以及何时成功都只能通过 TNMFTP 的事件来判断,主要的 第 10 章 FTP工具制作 299 事件有 OnAuthenticationFailed 、 OnAuthenticationNeeded 、 OnFailure 、 OnSuccess 、 OnTransactionStart、OnTransactionStop 和 OnUnSupportedFunction 等。这些事件在本章的程序 中基本都有使用,对它们的使用请参看程序源码的相关部分。 10.3 界面设计 程序主界面设计图如图 10-1 所示: 图 10-1 FTP 工具程序设计图 需要说明的是,由于本章的主要目的是介绍 FTP 组件的使用,所以只完成了程序主体部 分的设计工作,对于细节的部分,如主菜单、工具栏、界面美化等都没有做非常细致的工作。 相信学习了之前几章的内容之后,读者自己对这些工作已经都可以完成,所以本章不对细节 的功能做太多修饰,而是针对 FTP 工具所需要具有的文件浏览、上传、下载等功能以及 TNMFTP 组件的使用等方面详细叙述。 虽然没有做很多的细节修饰,但是我们要实现对本地文件和 FTP 服务器文件的浏览,所 以界面仍然比较复杂,分步叙述如下: z 第一步 在新建窗体中添加 StatusBar1、MainMenu1,它们都是一个完整的应用程序不可缺少的 部分,但是我们在本章并没有对它们做细致的工作。读者可以自己将各个操作功能添加 到主菜单中。在程序合适的地方添加显示在状态栏中的提示内容。 添加 ImageList1,并在其中添加几幅合适的图片;添加 NMFTP1,它是本程序中的关键 组件了,需要特别注意的是,需要设置其 ParseList 属性为 true,即自动解析 List 方法返 第 10 章 FTP工具制作 300 回的列表项。 z 第二步 在窗体中添加 Panel1,设置其 Align 属性为 alBottom,即贴近窗体下部。然后在其上方 添加一个水平的分割条 Splitter1,并设置分割条的 Height 到合适的值。 添加一个进度条 ProgressBar1,设置其 Align 为 alBottom,即贴近 Splitter1。在程序中, 它用来显示文件上传或者下载的进度。为其设置合适的 Height。 在窗体中添加 Panel2,设置其 Align 为 alLeft,则它占据窗体上方空闲位置的左方部分, 再在其右侧添加一个竖直分割条 Splitter2,并为 Splitter2 设置合适的宽度。 最后在窗体中再添加 Panel3,并设置其 Align 为 alClient。 这样,整个窗体便被分割为三个部分:下方的 Panel1 用来作为提示信息等的显示区域; 上部左方的 Panel2 用来浏览本地的文件;右方的 Panel3 用来浏览 FTP 服务器上的文件。 z 第三步――Panel1 上的组件添加 添加 Memo1,设置其 Align 为 alClient,即占满整个 Panel1;ScrollBars 为 ssBoth,即使 用左右和上下的滚动条;ReadOnly 为 true,显示文本为只读;最后设置 Lines 属性为它 添加一些提示文本信息。 在程序运行过程中,各种提示信息,如连接成功,传送文件成功等信息都会显示在 Memo1 中,为了用户对之前的信息浏览方便,所以添加滚动条。 z 第四步――Panel2 上的组件添加 在 Panel2 上添加两个工具栏 ToolBar3 和 ToolBar4。位于上方的 ToolBar3 上可以添加一 些诸如删除文件、上载文件等的按钮,本程序中并没有使用它,只为程序的扩展留下空 间。ToolBar4 用来显示当前的本地目录位置,其上放置一个按钮用来跳到当前目录的上 一级目录,另一个 DriveComboBox1 用来选择驱动器。其实这个驱动器组合框很不美观, 但是由于本章重点在 FTP 组件的使用,所以贪图它的方便才使用它。实际制作软件的时 候可以使用选项卡中 Win32 页中的 ComboBoxEx 组件,它可以在选项前加图片,很实用 也很美观。设置 ToolBar3 和 ToolBar4 的 ImageList 为 ImageList1。 在 Panel2 中添加一个列表视图命名为 ListViewLocal,设置其 ViewStyle 为 vsReport,然 后如图 10-1 添加文件名、大小、类型、时间四个列。设置 ListViewLocal 的 ImageList 为 ImageList1。设置 ListViewLocal 的 Align 为 alClient,即占满 Panel2 剩下的所有空间。 添加一个弹出菜单 PopupMenuLocal,并设置 ListViewLocal 的弹出菜单(PopupMenu 属 性)为 PopupMenuLocal。 这里,设置 PopupMenuLocal 如图 10-2 所示: 图 10-2 PopupMenuLocal 菜单设计 第 10 章 FTP工具制作 301 其中的 Queue 和 Queue As 是为队列上载而留,本程序中并没有使用。另外菜单中还应 该有对文件的删除、重命名等操作,但是由于在第六章中我们已经介绍了相关内容,所 以这里忽略掉,读者可以参考第六章内容自己添加。 z 第五步――Panel3 上的组件添加 与 Panel2 上的组件类似,这里要添加两个工具栏 ToolBar1 和 ToolBar2,其中 ToolBar1 上放置按钮用来完成连接、断开服务器,删除、下载、重命名文件等操作。因为文件操 作会放在右键菜单中,所以这里为了简洁在 ToolBar1 中只放置按钮用来连接和断开服务 器。ToolBar2 上放置一个按钮用来跳至上一级目录,另一个 EditPath 用来显示当前的工 作路径,并且改变它的Text 并按回车之后,完成跳转到指定目录的功能。最后,为ToolBar1 和 ToolBar2 指定 ImageList 为 ImageList1。 在 Panel2 上再添加列表视图组件 ListViewRemote,设置其 ViewStyle 为 vsReport,然后 如图 10-1 添加文件名、大小、时间、属性四个列。设置 ListViewRemote 的 ImageList 为 ImageList1。设置 ListViewRemote 的 Align 为 alClient,即占满 Panel3 剩下的所有空间。 添加一个弹出菜单 PopupMenuRemote,并设置它为 ListViewRemote 的弹出菜单。 设置 PopupMenuRemote 的菜单项如图 10-3 所示: 图 10-3 PopupMenuRemote 菜单设计 与 PopupMenuLocal 一样,其中的 Queue 和 Queue As 在本程序中都没有用到。 z 第六步――服务器登陆信息对话窗 在按下 ToolBar1 上的连接服务器的按钮之后,需要弹出对话框,供用户输入 FTP 服务 器的域名或 IP 2018香港马会开奖现场以及用户名和密码等信息。对话窗的设计图如 10-4 所示。 其中端口默认添为 21,匿名的 CheckBox 默认为被选定,在输入用户名时,自动去掉匿 名选择。添加两个 TBitBtn 按钮,设置它们的 Kind 属性分别为 bkOK 和 bkCancel。 第 10 章 FTP工具制作 302 图 10-4 登陆信息对话窗设计 10.4 功能实现 10.4.1 登录信息对话窗 登陆信息对话窗中 CheckBox1 默认是处于被选定的状态,所以,在 EditUser 中填写了用 户名后,需要取消它的选定状态,代码如下: //--------------------------------------------------------------------------- void __fastcall TForm2::EditUserChange(TObject *Sender) { if(EditUser->Text!="") CheckBox1->Checked=false; } //--------------------------------------------------------------------------- 另外,在与服务器建立连接之后,需要将登录信息对话窗中的内容清空,以供下次填写 其它服务器2018香港马会开奖现场等信息之用。代码见连接按钮的处理代码。 10.4.2 ListViewLocal 的实现 定义全局变量 AnsiString CurrentDir 用来标志当前所在的本地目录。刷新 ListViewLocal 视图的时候便根据 CurrentDir 实现,改变当前所在目录有三种方式:选择驱动器组合框、点 击组合框左边的上级目录按钮、双击视图中的目录列表项。另外,本程序为了简便,用于文 件和目录图标显示的图片之后三个,显示上级目录(即文件名为“..”的目录)用序号 0,其 它文件夹用序号 1,而文件的图标都用序号 2。 因为鼠标右键菜单多位与 NMFTP 操作相关的功能,所以放在后边讲述。 这部分代码实现与第六章中可以互相参考,这里不再过多解释,列出代码如下: 第 10 章 FTP工具制作 303 //--------------------------------------------------------------------------- //这是自定义的函数,用来根据 CurrentDir 刷新 ListViewLocal 视图 void __fastcall TForm1::RefreshViewLocal(void) { TSearchRec sr; TListItem *NewItem; AnsiString Ext; ListViewLocal->Items->Clear(); if (FindFirst(CurrentDir+"\\*.*",faAnyFile, sr) == 0) { do { if(sr.Name==".") continue; NewItem=ListViewLocal->Items->Add(); if (sr.Attr==faDirectory) //目录 { if(sr.Name=="..") { NewItem->ImageIndex=0; NewItem->Caption="..上级目录"; NewItem->SubItems->Add(""); NewItem->SubItems->Add(""); NewItem->SubItems->Add(""); } else { NewItem->ImageIndex=1; NewItem->Caption=sr.Name; NewItem->SubItems->Add(""); NewItem->SubItems->Add("文件夹"); NewItem->SubItems->Add(FileDateToDateTime(sr.Time)); } } else { Ext=ExtractFileExt(sr.Name).LowerCase(); if(Ext==".rm") { 第 10 章 FTP工具制作 304 NewItem->ImageIndex=2; NewItem->Caption=sr.Name ; NewItem->SubItems->Add(sr.Size); NewItem->SubItems->Add("rm电影"); NewItem->SubItems->Add(FileDateToDateTime(sr.Time)); } else { NewItem->ImageIndex=2; NewItem->Caption=sr.Name ; NewItem->SubItems->Add(sr.Size); NewItem->SubItems->Add(Ext+"类型文件"); NewItem->SubItems->Add(FileDateToDateTime(sr.Time)); } } } while (FindNext(sr) == 0); FindClose(sr); } } //---------------------------------------------- //驱动器组合框的响应 void __fastcall TForm1::DriveComboBox1Change(TObject *Sender) { CurrentDir=AnsiString(DriveComboBox1->Drive)+":"; RefreshViewLocal(); } //--------------------------------------------------------------------------- //视图中鼠标双击事件的响应 void __fastcall TForm1::ListViewLocalDblClick(TObject *Sender) { if(ListViewLocal->SelCount==1) { if(ListViewLocal->Selected->ImageIndex==0) { //上级目录 int i; i=CurrentDir.Length(); while(i>0) { if(CurrentDir[i]=='\\') { 第 10 章 FTP工具制作 305 CurrentDir.Delete(i,CurrentDir.Length()); RefreshViewLocal(); break; } i--; } } else if(ListViewLocal->Selected->ImageIndex==1) { CurrentDir=CurrentDir+"\\"+ListViewLocal->Selected->Caption; RefreshViewLocal(); } } } //--------------------------------------------------------------------------- //组合框左边的上级目录按钮的响应 void __fastcall TForm1::ToolButton10Click(TObject *Sender) { //上级目录 int i; i=CurrentDir.Length(); while(i>0) { if(CurrentDir[i]=='\\') { CurrentDir.Delete(i,CurrentDir.Length()); RefreshViewLocal(); break; } i--; } } //--------------------------------------------------------------------------- ListViewLocal 视图的效果运行效果如图 10-5。 第 10 章 FTP工具制作 306 图 10-5 ListViewLocal 运行效果 10.4.3 与服务器的连接 点击 ToolBar1 上的连接按钮 ToolButtonConnect 之后,弹出登陆信息对话窗,确认信息 之后,通过填写的信息设置 NMFTP1 的属性,并连接服务器,连接服务器之后,需要将当前 工作目录显示在 EditPath 中,并且调用 NMFTP1 的 List 方法,由于对视图的刷新放在 OnSuccess 事件的响应中,所以,List 方法成功返回之后,ListViewRemote 视图会刷新。 代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::ToolButtonConnectClick(TObject *Sender) { if(Form2->ShowModal()==mrOk) { NMFTP1->Host=Form2->EditHost->Text; NMFTP1->Port=StrToInt(Form2->EditPort->Text); if(Form2->CheckBox1->Checked) //匿名 NMFTP1->UserID="anonymouse"; else NMFTP1->UserID=Form2->EditUser->Text; NMFTP1->Password=Form2->EditPass->Text; NMFTP1->ReportLevel=Status_Basic; NMFTP1->Connect(); //随后的处理操作,如调用 List 方法,清空 Form2 中的各个 Edit 的内容等操作 //方在 NMFTP1 的 OnConnect 事件中 } } 第 10 章 FTP工具制作 307 //--------------------------------------------------------------------------- 断开与服务器连接之后,需要清空 ListViewRemote 视图和 EditPath 的内容,置上级目录 按钮和断开连接按钮以及 EditPath 为不可用,恢复连接按钮为可用,具体代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::ToolButtonDisconnectClick(TObject *Sender) { NMFTP1->Disconnect(); ListViewRemote->Items->Clear(); EditPath->Text=""; ToolButtonConnect->Enabled=true; ToolButtonDisconnect->Enabled=false; ToolButtonUp->Enabled=false; EditPath->Enabled=false; } //--------------------------------------------------------------------------- 10.4.4 ListViewRemote 的实现 视图的显示刷新放在 RefreshViewRemote 函数中,它是自定义的函数,在 NMFTP1 触发 cmdList 类型的 OnSuccess 事件时调用。所以,在程序中,只需要设定合适的工作目录,然后 调用 NMFTP1 的 List 方法即可实现对视图的刷新。 因为我们设置了 NMFTP1 的 ParseList 属性为 true,所 以 List 成功以后,工作目录下各个 文件和目录的信息都会保存在 NMFTP1 的 FTPDirectoryList 属性中,在函数中,正是通过对 此属性的读取,实现对视图的显示。首先根据 FTPDirectoryList 中的 Attribute 的第一个字符 判断列表项是否是目录,在根据文件名去掉目录名为“.”的项,并且对“..”项做特殊显示, 将其作为上级目录(ImageIndex 为 0),其它目录 ImageIndex 为 1,文件的 ImageIndex 为 2。 此函数代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::RefreshViewRemote(void) { TListItem *NewItem; AnsiString Name,Attribute,Date,Size; if(!NMFTP1->ParseList)//没有设定对列表信息进行解析 return; ListViewRemote->Clear(); for (int I = 0; I < NMFTP1->FTPDirectoryList->name->Count; I++) { 第 10 章 FTP工具制作 308 Name=NMFTP1->FTPDirectoryList->name->Strings[I]; Size=NMFTP1->FTPDirectoryList->Size->Strings[I]; Date=NMFTP1->FTPDirectoryList->ModifDate->Strings[I]; Attribute=NMFTP1->FTPDirectoryList->Attribute->Strings[I]; if(Attribute[1]=='d' && Name==".") // { continue; } NewItem=ListViewRemote->Items->Add(); if(Attribute[1]=='d' && Name=="..") { NewItem->ImageIndex=0; NewItem->Caption="..上级目录"; continue; } if(Attribute[1]=='d') { NewItem->ImageIndex=1; } else { AnsiString FileExt=ExtractFileExt(Name); if(FileExt==".exe") NewItem->ImageIndex=2; else NewItem->ImageIndex=2; } NewItem->Caption=Name; NewItem->SubItems->Add(Size); NewItem->SubItems->Add(Date); NewItem->SubItems->Add(Attribute); } } //--------------------------------------------------------------------------- 另外,对视图的显示也可以通过 OnListItem 事件来实现。此事件在每列出一项文件或目 录时都会触发一次,它的入口参数为服务器返回的一个字符串,里面包含文件/文件夹的属性、 名字、事件、大小等信息,具有标准的格式,所以可以通过对字符串中各个字符、子串的判 断来自己完成对列表信息的解析,完成对视图的显示和刷新,虽然本程序中并没有使用这个 方法,但是,这里还是给出其实现代码: //--------------------------------------------------------------------------- 第 10 章 FTP工具制作 309 void __fastcall TForm1::NMFTP1ListItem(AnsiString Listing) { TListItem *NewItem; AnsiString FileName=Listing.SubString(56,Listing.Length()); //判断文件类型,目录,或者其他的文件格式,从而选择不同的图标 if(Listing[1]=='d' && FileName==".") // { return; } NewItem=ListViewRemote->Items->Add(); if(Listing[1]=='d' && FileName=="..") { NewItem->ImageIndex=0; NewItem->Caption="..上级目录"; return; } if(Listing[1]=='d') { NewItem->ImageIndex=1; } else { AnsiString FileExt=ExtractFileExt(FileName); if(FileExt==".exe") NewItem->ImageIndex=2; else NewItem->ImageIndex=2; } NewItem->Caption=FileName;//-55 NewItem->SubItems->Add(Listing.SubString(31,11)); NewItem->SubItems->Add(Listing.SubString(43,12)); NewItem->SubItems->Add(Listing.SubString(1,10)); } //--------------------------------------------------------------------------- 改变 ListViewRemote 视图显示的方法有四种:工具栏中的向上按钮、工具栏中的 EditPath 中输入路径按回车、双击 ListViewRemote 中的图标、鼠标右键的 ChangeFolder 选项。它们都 是先通过 NMFTP1 的 ChangeDir 方法改变工作目录,然后调用 NMFTP1 的 List 方法从而实 现对视图的刷新,鼠标的响应,在后边单独集中列出代码,这里列出前三种方法的实现代码: 第 10 章 FTP工具制作 310 //--------------------------------------------------------------------------- //视图的双击事件响应 void __fastcall TForm1::ListViewRemoteDblClick(TObject *Sender) { AnsiString TmpFile; if(ListViewRemote->SelCount==1) { if(ListViewRemote->Selected->ImageIndex==0) { AnsiString tmpDir=".."; NMFTP1->ChangeDir(tmpDir); ListViewRemote->Items->Clear(); NMFTP1->List(); EditPath->Text=NMFTP1->CurrentDir; } else if(ListViewRemote->Selected->ImageIndex==1) { AnsiString tmpDir=ListViewRemote->Selected->Caption; NMFTP1->ChangeDir(tmpDir); ListViewRemote->Items->Clear(); NMFTP1->List(); EditPath->Text=NMFTP1->CurrentDir; } } } //--------------------------------------------------------------------------- //向上按钮的响应 void __fastcall TForm1::ToolButtonUpClick(TObject *Sender) { NMFTP1->ChangeDir(".."); ListViewRemote->Items->Clear(); NMFTP1->List(); EditPath->Text=NMFTP1->CurrentDir; } //--------------------------------------------------------------------------- //EditPath 中回车键的响应 void __fastcall TForm1::EditPathKeyPress(TObject *Sender, char &Key) { if(Key==13)//回车 { 第 10 章 FTP工具制作 311 NMFTP1->ChangeDir(EditPath->Text); NMFTP1->List(); } } //--------------------------------------------------------------------------- ListViewRemote 视图的运行效果如图 10-6: 图 10-6 ListViewRemote 运行效果 10.4.5 PopupMenuLocal 和 PopupMenuRemote 菜单的响应 FTP 工具主要的功能,如文件的上传、下载、服务器上文件的删除、重命名等操作都放 在右键菜单中。 对于 PopupMenuLocal 菜单,由于在第六章已经比较详细的介绍了对文件的操作以及实 现,所以在此菜单中只给出了与 FTP 操作相关的上传等操作,代码如下: //--------------------------------------------------------------------------- //文件上传 void __fastcall TForm1::PopMenuUpLoadClick(TObject *Sender) { AnsiString FileName; if(ListViewLocal->Selected->ImageIndex==1) //目录 { FileName=ListViewLocal->Selected->Caption; 第 10 章 FTP工具制作 312 MessageDlg(FileName+ "是一个目录,软件尚未提供目录下载功能", mtInformation, TMsgDlgButtons() << mbOK, 0); } else //文件 { FileName=ListViewLocal->Selected->Caption; NMFTP1->Upload(CurrentDir+"\\"+FileName,FileName); //刷新视图 PopMenuRefresh->Click(); } } //--------------------------------------------------------------------------- //“上传为”菜单项 void __fastcall TForm1::PopMenuUpLoadAsClick(TObject *Sender) { AnsiString FileName; if(ListViewLocal->Selected->ImageIndex==1) //目录 { FileName=ListViewLocal->Selected->Caption; MessageDlg(FileName+ "是一个目录,软件尚未提供目录下载功能", mtInformation, TMsgDlgButtons() << mbOK, 0); } else //文件 { AnsiString NewName; if(InputQuery("输入文件名","上载后的文件名:",NewName)) { FileName=ListViewLocal->Selected->Caption; NMFTP1->Upload(CurrentDir+"\\"+FileName,FileName); PopMenuRefresh->Click(); } } } //--------------------------------------------------------------------------- //刷新 ViewListLocal 视图 void __fastcall TForm1::PopMenuLocalRefreshClick(TObject *Sender) { RefreshViewLocal(); } //--------------------------------------------------------------------------- 第 10 章 FTP工具制作 313 PopupMenuRemote 菜单中放置了最终要的文件传输相关的功能,如下载、删除、重命名、 改变工作目录等,其实现代码如下: //--------------------------------------------------------------------------- //下载菜单 void __fastcall TForm1::PopMenuDownClick(TObject *Sender) { AnsiString FileName; if(ListViewRemote->Selected->ImageIndex==1) //目录 { FileName=ListViewLocal->Selected->Caption; MessageDlg(FileName+ "是一个目录,软件尚未提供目录下载功能", mtInformation, TMsgDlgButtons() << mbOK, 0); } else //文件 { FileName=ListViewRemote->Selected->Caption; NMFTP1->Download(FileName,CurrentDir+"\\"+FileName); RefreshViewLocal(); } } //--------------------------------------------------------------------------- //“下载为”菜单 void __fastcall TForm1::PopMenuDownAsClick(TObject *Sender) { AnsiString FileName; if(ListViewRemote->Selected->ImageIndex==1) //目录 { FileName=ListViewLocal->Selected->Caption; MessageDlg(FileName+ "是一个目录,软件尚未提供目录下载功能", mtInformation, TMsgDlgButtons() << mbOK, 0); } else //文件 { AnsiString NewName; if(InputQuery("输入文件名","下载后的文件名:",NewName)) { FileName=ListViewRemote->Selected->Caption; NMFTP1->Download(FileName,CurrentDir+"\\"+FileName); RefreshViewLocal(); 第 10 章 FTP工具制作 314 } } } //--------------------------------------------------------------------------- //“删除”菜单项 void __fastcall TForm1::PopMenuDeleteClick(TObject *Sender) { //需要有选中文件,且不是上级目录 AnsiString FileName; FileName=ListViewRemote->Selected->Caption; if(ListViewRemote->Selected->ImageIndex==1) //目录 { if(MessageDlg(AnsiString("确认删除目录")+FileName+ "?", mtConfirmation, TMsgDlgButtons() << mbYes <RemoveDir(FileName); } } else { if(MessageDlg(AnsiString("确认删除文件")+FileName+ "?", mtConfirmation, TMsgDlgButtons() << mbYes <Delete(FileName); } } NMFTP1->List(); } //--------------------------------------------------------------------------- //“重命名”菜单项 void __fastcall TForm1::PopMenuRenameClick(TObject *Sender) { //在 ViewList 中有列表项被选中,而且不是上级目录那一项时,才能使用 //这个在右键弹出时判断 AnsiString NewName; if(InputQuery("重命名","填写新的文件名:",NewName)) { AnsiString OldName; 第 10 章 FTP工具制作 315 OldName=ListViewRemote->Selected->Caption; NMFTP1->Rename(OldName,NewName); NMFTP1->List(); } } //--------------------------------------------------------------------------- //“新建目录”菜单项 void __fastcall TForm1::PopMenuMakeFolderClick(TObject *Sender) { AnsiString TheDir; if(InputQuery("创建目录","填写要创建的目录名:",TheDir)) { NMFTP1->MakeDirectory(TheDir); NMFTP1->List(); } } //--------------------------------------------------------------------------- //“改变工作目录”菜单项 void __fastcall TForm1::PopMenuChangeFolderClick(TObject *Sender) { AnsiString TheDir; if(InputQuery("切换当前目录到...","填写目录:",TheDir)) { NMFTP1->ChangeDir(TheDir); //刷新视图 NMFTP1->List(); } } //--------------------------------------------------------------------------- //刷新视图菜单项 void __fastcall TForm1::PopMenuRefreshClick(TObject *Sender) { NMFTP1->List(); } //--------------------------------------------------------------------------- 另外,需要注意的是菜单项的很多选项只有在与服务器的连接已经建立之后才能用,而 且有的选项如删除、重命名、下载,都需要在 ListViewRemote 中有被选择项的时候才可用, 所以需要在菜单弹出时,首先判断当前的工作状态,然后为菜单中各个项设置响应的 Enabled 属性。 代码如下: 第 10 章 FTP工具制作 316 //--------------------------------------------------------------------------- void __fastcall TForm1::PopupMenuRemotePopup(TObject *Sender) { //判断哪些选项可用 PopMenuDown->Enabled=false; PopMenuDownAs->Enabled=false; PopMenuDelete->Enabled=false; PopMenuRename->Enabled=false; PopMenuMakeFolder->Enabled=false; PopMenuChangeFolder->Enabled=false; PopMenuRefresh->Enabled=false; if(NMFTP1->Connected) { if(ListViewRemote->SelCount==1 && ListViewRemote->Selected->ImageIndex!=0) { PopMenuDown->Enabled=true; PopMenuDownAs->Enabled=true; PopMenuDelete->Enabled=true; PopMenuRename->Enabled=true; } PopMenuMakeFolder->Enabled=true; PopMenuChangeFolder->Enabled=true; PopMenuRefresh->Enabled=true; } } //--------------------------------------------------------------------------- 对 PopupMenuLocal,由于程序一开始便会触发驱动器组合框的事件,使得 ListViewLocal 视图显示相应目录中的文件,而且本程序中没有为 PopupMenuLocal 设置删除文件等功能, 所以菜单项较少,且只需要对上传和上传为两个菜单项进行设置,代码如下: void __fastcall TForm1::PopupMenuLocalPopup(TObject *Sender) { //判断哪些选项可用 PopMenuUpLoad->Enabled=false; PopMenuUpLoadAs->Enabled=false; // PopMenuLocalRefresh->Enabled=false; if(NMFTP1->Connected) { if(ListViewLocal->SelCount==1 && ListViewLocal->Selected->ImageIndex!=0) 第 10 章 FTP工具制作 317 { PopMenuUpLoad->Enabled=true; PopMenuUpLoadAs->Enabled=true; } } } //--------------------------------------------------------------------------- 10.4.6 对 NMFTP1 各种事件的响应 对于 NMFTP1 的各种事件,都需要在 Memo1 中将相应的信息显示出来。特别的,对于 OnSuccess 事件,如果是 cmdList 类型,需要调用 ListViewRemote 的刷新函数;对于传输文 件开始事件,需要将 ListViewRemote 等组件设为不可用,因为在传输文件时,如果再发送其 它 FTP 命令会造成错误;再文件传输结束时,需要恢复各个组件为可用;对于接收或发送文 件数据的事件,要根据已接收或已发送的数据的大小和文件的实际大小进行比较,然后在进 度栏上绘制进度。 其它一些事件,很多本程序中都没有用到,只是将相应的信息显示在 Memo1 中,读者 根据下面的代码和提示信息,可以知道各种事件的作用。 各种事件的响应代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::NMFTP1Connect(TObject *Sender) { Memo1->Lines->Add("成功连接到"+NMFTP1->Host+"端口"+NMFTP1->Port); Memo1->Lines->Add(AnsiString("用户名:")+NMFTP1->UserID); Memo1->Lines->Add("密码:"+NMFTP1->Password); //调用 List 方法获得文件列表,并在 ListViewRemote 中显示 NMFTP1->List(); EditPath->Text=NMFTP1->CurrentDir; //置连接按钮不可用,置其它几个按钮可用 ToolButtonConnect->Enabled=false; ToolButtonDisconnect->Enabled=true; ToolButtonUp->Enabled=true; EditPath->Enabled=true; //清空 Form2 登陆信息窗体中的内容 Form2->EditHost->Text=""; Form2->EditUser->Text=""; Form2->EditPass->Text=""; Form2->EditPort->Text="21"; 第 10 章 FTP工具制作 318 Form2->CheckBox1->Checked=true; } //--------------------------------------------------------------------------- void __fastcall TForm1::NMFTP1ConnectionFailed(TObject *Sender) { Memo1->Lines->Add("连接失败,Server:"+NMFTP1->Host+"端口"+NMFTP1->Port); } //--------------------------------------------------------------------------- void __fastcall TForm1::NMFTP1Disconnect(TObject *Sender) { Memo1->Lines->Add("连接已断开!"); } //--------------------------------------------------------------------------- void __fastcall TForm1::NMFTP1Error(TComponent *Sender, WORD Errno, AnsiString Errmsg) { Memo1->Lines->Add(AnsiString("Error ")+IntToStr(Errno)+": "+Errmsg); } //--------------------------------------------------------------------------- void __fastcall TForm1::NMFTP1Failure(bool &Handled, TCmdType Trans_Type) { switch (Trans_Type) { case cmdChangeDir: Memo1->Lines->Add("ChangeDir failure"); break; case cmdMakeDir: Memo1->Lines->Add("MakeDir failure"); break; case cmdDelete: Memo1->Lines->Add("Delete failure"); break; case cmdRemoveDir: Memo1->Lines->Add("RemoveDir failure"); break; case cmdList: Memo1->Lines->Add("List failure"); break; case cmdRename: Memo1->Lines->Add("Rename failure"); break; case cmdUpRestore: Memo1->Lines->Add("UploadRestore failure"); break; case cmdDownRestore: Memo1->Lines->Add("DownloadRestore failure"); break; case cmdDownload: Memo1->Lines->Add("Download failure"); break; case cmdUpload: Memo1->Lines->Add("Upload failure"); break; case cmdAppend: Memo1->Lines->Add("UploadAppend failure"); break; case cmdReInit: Memo1->Lines->Add("ReInit failure"); break; case cmdAllocate: Memo1->Lines->Add("Allocate failure"); break; case cmdNList: Memo1->Lines->Add("NList failure"); break; 第 10 章 FTP工具制作 319 case cmdDoCommand: Memo1->Lines->Add("DoCommand failure"); break; default: Memo1->Lines->Add("Unrecognized command failed."); break; } } //--------------------------------------------------------------------------- void __fastcall TForm1::NMFTP1HostResolved(TComponent *Sender) { Memo1->Lines->Add("Host Resolved"); } //--------------------------------------------------------------------------- void __fastcall TForm1::NMFTP1InvalidHost(bool &Handled) { Memo1->Lines->Add("Invalid Host,Please Choose another host"); } //--------------------------------------------------------------------------- void __fastcall TForm1::NMFTP1PacketRecvd(TObject *Sender) { //因为这个事件每接受一个数据包都会触发,所以做成进度条 ProgressBar1->Position= float(NMFTP1->BytesRecvd)*100/float(NMFTP1->BytesTotal); } //--------------------------------------------------------------------------- void __fastcall TForm1::NMFTP1PacketSent(TObject *Sender) { ProgressBar1->Position= float(NMFTP1->BytesSent)*100/float(NMFTP1->BytesTotal); } //--------------------------------------------------------------------------- void __fastcall TForm1::NMFTP1Status(TComponent *Sender, AnsiString Status) { Memo1->Lines->Add(Status); } //--------------------------------------------------------------------------- void __fastcall TForm1::NMFTP1Success(TCmdType Trans_Type) 第 10 章 FTP工具制作 320 { switch(Trans_Type) { case cmdChangeDir: Memo1->Lines->Add("ChangeDir success"); break; case cmdMakeDir: Memo1->Lines->Add("MakeDir success"); break; case cmdDelete: Memo1->Lines->Add("Delete success"); break; case cmdRemoveDir: Memo1->Lines->Add("RemoveDir success"); break; case cmdList: Memo1->Lines->Add("List success"); RefreshViewRemote(); //刷新视图 break; case cmdRename: Memo1->Lines->Add("Rename success"); break; case cmdUpRestore: Memo1->Lines->Add("UploadRestore success"); break; case cmdDownRestore: Memo1->Lines->Add("DownloadRestore success"); break; case cmdDownload: Memo1->Lines->Add("Download success"); break; case cmdUpload: Memo1->Lines->Add("Upload success"); break; case cmdAppend: Memo1->Lines->Add("UploadAppend success"); break; case cmdReInit: Memo1->Lines->Add("ReInit success"); break; case cmdAllocate: Memo1->Lines->Add("Allocate success"); break; case cmdNList: Memo1->Lines->Add("NList success"); break; case cmdDoCommand: Memo1->Lines->Add("DoCommand success"); break; } } //--------------------------------------------------------------------------- void __fastcall TForm1::NMFTP1TransactionStart(TObject *Sender) { Memo1->Lines->Add("Starting data transaction"); //传送数据期间,将 FTP 栏所有组件置为不可用 ToolBar1->Enabled=false; ToolBar2->Enabled=false; ListViewRemote->Enabled=false; } //--------------------------------------------------------------------------- void __fastcall TForm1::NMFTP1TransactionStop(TObject *Sender) { Memo1->Lines->Add("Transaction Complete"); //将组件置为可用 ToolBar1->Enabled=true; 第 10 章 FTP工具制作 321 ToolBar2->Enabled=true; ListViewRemote->Enabled=true; //将进度条清空 ProgressBar1->Position=0; } //--------------------------------------------------------------------------- void __fastcall TForm1::NMFTP1UnSupportedFunction(TCmdType Trans_Type) { switch(Trans_Type) { case cmdChangeDir: Memo1->Lines->Add("ChangeDir not supported"); break; case cmdMakeDir: Memo1->Lines->Add("MakeDir not supported"); break; case cmdDelete: Memo1->Lines->Add("Delete not supported"); break; case cmdRemoveDir: Memo1->Lines->Add("RemoveDir not supported"); break; case cmdList: Memo1->Lines->Add("List not supported"); break; case cmdRename: Memo1->Lines->Add("Rename not supported"); break; case cmdUpRestore: Memo1->Lines->Add("UploadRestore not supported"); break; case cmdDownRestore: Memo1->Lines->Add("DownloadRestore not supported"); break; case cmdDownload: Memo1->Lines->Add("Download not supported"); break; case cmdUpload: Memo1->Lines->Add("Upload not supported"); break; case cmdAppend: Memo1->Lines->Add("UploadAppend not supported"); break; case cmdReInit: Memo1->Lines->Add("ReInit not supported"); break; case cmdAllocate: Memo1->Lines->Add("Allocate not supported"); break; case cmdNList: Memo1->Lines->Add("NList not supported"); break; case cmdDoCommand: Memo1->Lines->Add("DoCommand not supported"); break; } } //--------------------------------------------------------------------------- 另外本程序中没有对 OnAuthenticationFailed 、 OnAuthenticationNeeded 、 OnConnectionRequired 三个事件的响应,对它们的使用,参看下面的代码: //--------------------------------------------------------------------------- //此事件在用户输入的帐户名或者密码错误时触发,下面的代码功能是在此事件触发是 //弹出输入窗,让用户重新输入帐户和密码 void __fastcall TForm1::NMFTP1AuthenticationFailed(bool &Handled) { AnsiString ThePass, TheID; if (MessageDlg("Authentication Failed. Retry?", mtConfirmation, TMsgDlgButtons() << mbYes << mbNo, 0) == mrYes) { 第 10 章 FTP工具制作 322 ThePass = NMFTP1->Password; TheID = NMFTP1->UserID; InputQuery("Reauthenticate", "Enter User ID", TheID); InputQuery("Reauthenticate", "Enter Password", ThePass); NMFTP1->Password = ThePass; NMFTP1->UserID = TheID; Handled = TRUE; } } //------------------------------------------------------------------ //此事件在用户名或者密码为空时触发,下面代码功能是判断用户名和密码哪项为空, //然后弹出对话框让用户重新输入 void __fastcall TForm1::NMFTP1AuthenticationNeeded(bool &Handled) { AnsiString APass, AnID; if (NMFTP1->Password == "") if (InputQuery("Password needed", "Enter password: ", APass)) { NMFTP1->Password = APass; Handled = TRUE; } else Handled = FALSE; if (NMFTP1->UserID == "") if (InputQuery("User ID needed", "Enter User ID: ", AnID)) { NMFTP1->UserID = AnID; Handled = TRUE; } else Handled = FALSE; } //------------------------------------------------------------------------------- //如果调用 NMFTP 的某些方法,而这些方法需要与服务器连接才能使用,而且此时并没有 //与服务器连接,那么就会触发此事件,此时,NMFTP 会自动调用这个事件的处理代码 //如果没有响应的代码处理这个事件,NMFTP 就会抛出一个异常信息 //下面的代码在 ConnectionRequired 事件触发时弹出确认框,让用户选择是否连接服务器 void __fastcall TForm1::NMEcho1ConnectionRequired(bool &Handled) 第 10 章 FTP工具制作 323 { if (MessageDlg("Connection Required", mtConfirmation, TMsgDlgButtons() << mbYes << mbNo, 0) == mrYes) { NMEcho1->Connect(); Handled = TRUE; } } //--------------------------------------------------------------------------------- Memo1 和进度条对 NMFTP 事件的响应效果如图 10-7 所示: 图 10-7 Memo 以及进度栏对 NMFTP 事件的响应 10.5 思考题 z TNMFTP 组件中的断点续传方法,是不是所有 FTP 服务器都支持? z 分析 OnListItem 事件中 Listing 字符串的各个部分代表什么意思? z 请为 FTP 工具程序添加连接超时检查的功能。 z 请为 FTP 工具程序的 ListViewLocal 添加文件的操作功能,包括新建文件夹、删除文件、 重命名文件等 z 如何实现从 FTP 上下载整个目录的文件? z 如何上载整个目录的文件? 《C++ Builder 6 编程实例精解 赵明现》 第 11 章 远程屏幕监视程序 本章重点 本章介绍远程屏幕监视程序的制作,包括屏幕图象的获取以及利用 WinSock 传输图象。 介绍了 WinSock 编程的概念,以及 C++Builder 中与 WinSock 相关的几个类;实现利用 API 函数对屏幕图象的获取和格式的转换;实现利用 WinSocket 相关的组件传输图象;最后介绍 任务栏图标的使用。 学习目的 通过本章的学习,您可以: ■ 了解 WinSock 编程的一般概念 ■ 掌握 WinSocket 相关的几个类 ■ 掌握 ClientSocket 和 ServerSocket 的使用 ■ 掌握利用 API 函数截取屏幕图象的方法 ■ 掌握 BMP 图象向 JPG 格式转换的方法 ■ 掌握任务栏图标的使用 第 11 章 远程屏幕监视程序 325 本章典型效果图 第 11 章 远程屏幕监视程序 326 11.1WinSock 编程概述 TCP/IP 协议相信大家都不陌生,最初的 TCP/IP 协议通信的标准是一组在 UNIX 系统上 实现的被称为 Berkeley Sockets 的约定,后来其它的操作系统也都实现了 TCP/IP 通信,为互 联网的发展奠定了基础。但是,由于不同的操作系统对 TCP/IP 的实现有所不同,使得事情 变的有些混乱,于是,一些著名的厂商联合起来指定了 WinSock 规范。 WinSock 是一组 API 函数,用于 Internet 上传输数据和交换信息。这组函数一般包含在 WinSock.dll 或 Wsock32.dll 中,低级的 WinSockAPI 提供了建立 TCP/IP 连接所需要的所有支 持,也支持其它相关的 Xerox Network System (XNS)、DECnet、Novell 的 IPX/SPX 等协议。 虽然现在有很多现成的像 Web 浏览器和 FTP 程序一样的工具可以方便的实现 Internet 上的数 据和文件的传输,但是通过 WinSock 编程可以获得更大的灵活性,而且由于它是一个统一的 通信标准,所以进行 WinSock 编程并不需要考虑网络的连接细节。但使用 WinSock 意味着要 与一堆的 API 函数打交道,而 WinSock 的编程本身也是很麻烦的。 在 C++Builder 中提供两个组件 TServerSocket 和 TClientSocket,它们封装了 WinSock 的 大部分 API 函数,使得利用 WinSock 编程变得简单容易的多了。 11.1.1 WinSock 概述 要通过 WinSock 实现网络连接、传递数据,至少需要一对 Socket,一个客户端 Socket, 一个服务器端 Socket。一旦客户端和服务器端 Socket 相互连通,就可以进行数据通信。Socket 之间的连接过程简单叙述如下: 客户端连接:是指由客户端的 Socket 像服务器端的 Socket 发出连接请求。当然首先必须 先向客户端 Socket 描述服务器端的2018香港马会开奖现场和端口等信息,然后客户端 Socket 根据所描述的服务 器信息定位到服务器端 Socket,找到服务器 Socket 之后向服务器发送请求连接的信息。此时, 如果服务器端 Socket 处于监听状态,就会将客户端的请求信息放入一个客户请求队列,在它 认为合适的时候向发送请求的客户端 Socket 发出“允许连接”(Accept)信号。这样,客户 端 Socket 和服务器端 Socket 的连接就建立起来了。 监听连接:这是指服务器端的 Socket。服务器端的 Socket 并不对客户端的 Socket 进行定 位,而是一直处于等待连接的状态并监听客户端的连接请求,当接收到客户端的连接请求时, 就会在合适的时候建立新的 Socket 句柄并向客户端发出允许连接的信号从而建立连接。在与 客户端 Socket 连接的同时,服务器端 Socket 仍然处于监听状态,等待其它客户端 Socket 的 连接请求。 服务器端连接:在服务器端 Socket 接收到客户端的请求后,会把服务器端的 Socket 描述 发送给客户端,客户端确认返回的描述信息之后,两个 Socket 之间的连接就建立了。 在 C++Builder 中,TClientSocket 和 TServerSocket 组件用于客户端和服务器端的连接, 但是它们本身并不是 Socket 对象,它们只负责操纵客户端和服务器端 Socket 的通信和连接。 TCustomWinSocket 类及其派生类均可操纵 Socket 对象,如 TClientSocket、TServerSocket 和 TServerClientSocket 等。 第 11 章 远程屏幕监视程序 327 11.1.2 建立服务器端 Socket 在应用程序的窗体中添加 TServerSocket 组件,这个应用程序就成了一个 TCP/IP 服务器。 要使服务器 Socket 开始工作,需要先为它指定服务端口,即 Port 属性,或者指定服务器 Socket 的类型(FTP、HTTP 等),这些指定类型的服务都有固定的端口。如果在指定了服务类型之 后又指定了 Port 属性,那么 Port 属性将被忽略。 指定了服务器 Socket 的描述信息之后,就可以调用 TServerSocket 的 Open 方法或者将其 Active 属性置为 true,从而使服务器 Socket 处于监听状态。进入监听状态以后,可以通过 TServerSocket 组件的 Socket 属性访问服务器的 Socket 对象,从而得到 SocketHandle 等 Socket 属性。此时,如果有客户端 Socket 发出连接请求,服务器 Socket 在接收到请求之后会处理请 求并与客户端建立连接,此时会触发 TServerSocket 的 OnConnect 事件。连接建立以后,就可 以通过组件的 Socket 属性完成与客户端之间的数据传输。 需要服务器端Socket断开连接时,只需要调用TServerSocket的Close方法或者将其Active 属性置为 false 即可。但是需要注意的是,这样做会断开与所有客户端 Socket 的连接。而如 果是在客户端调用 Close 方法或置客户端 Socket的Active为false,则不会影响服务器端 Socket 与其它 Socket 之间的连接。 11.1.3 建立客户端 Socket 在应用程序的窗体上添加 TClientSocket 组件,这个应用程序就成了一个 TCP/IP 客户端, 要建立客户端与服务器端的连接,需要先向 TClientSocket 描述服务器端的属性,也就是为 TClientSocket 指定服务器2018香港马会开奖现场和端口,指定服务器2018香港马会开奖现场有两种方式,Host 属性可以指定域名, Address 属性可以指定 IP。而端口也可以通过设定 TClientSocket 的服务类型(Service 属性) 来确定,与 TServerSocket 一样,指定了服务类型之后将忽略对 Port 属性的设定。 然后,调用 TClientSocket 的 Open 方法或者将其 Active 属性置为 true,客户端的 Socket 就会向指定的服务器端 Socket 发送连接请求。此时,如果服务器端 Socket 处于监听状态,就 会自动接受请求并建立连接。建立连接之后会触发 OnConnect 事件。连接建立以后就可以通 过访问 TClientSocket 的 Socket 属性来实现与服务器之间的数据传输。 要断开与服务器端的连接,可以调用 TClientSocket 的 Close 方法或者将其 Active 属性置 为 false,此时,会触发 TClientSocket 的 OnDisconnect 事件,而在服务器端则会触发 OnClientDisconnect 事件。 11.2 操纵 Socket 对象实现数据传输 TServerSocket 和 TClientSocket 组件用于管理服务器端和客户端的 Socket 连接,但是具 体的数据传输等操作还是通过它们的 Socket 属性来完成的。TServerSocket 的 Socket 属性是 TServerWinSocket 类的一个实例,而 TClientSocket 的 Socket 属性则是 TClientWinSocket 类的 第 11 章 远程屏幕监视程序 328 一个实例,而 TServerWinSocket 和 TClientWinSocket 两个类则是从 TCustomWinSocket 类继 承而来。 11.2.1 TCustomWinSocket 类 TCustomWinSocket 类用来描述 Windows Socket 连接中一个节点的属性、方法和事件。 TCustomWinSocket 有三个派生类。其中,TClientWinSocket 用于操纵客户端的 Socket 对象; TServerWinSocket 用于操纵处于监听状态的服务器端的 Socket 对象;TServerClientWinSocket 用于操纵已经与客户端建立连接的服务器端的 Socket 对象。 TCustomWinSocket 类的属性和方法主要有: 表 11-1 TCustomWinSocket 类的属性和方法 属性/方法 说明 Connected 标示 Socket 连接的状态。为 true 时,标示已经建立 Socket 连接并可用; 否则,Socket 处于关闭状态并且可以改变。要打开客户端的 Socket, 使用 Open 方法;要打开服务器端的 Socket,使用 Listen 方法。关闭 Socket 连接,使用 Close 方法 LocalAddress LocalHost LocalPort 它们分别为本地主机的 IP 2018香港马会开奖现场、域名和端口。一台计算机可以有多个 IP 2018香港马会开奖现场,每个 IP 2018香港马会开奖现场可以同时用于多个 Socket 连接,但是每个 Socket 连接的端口号必须相异,以区别与同一 IP 2018香港马会开奖现场的其它连接 RemoteAddress RemoteHost RemotePort 它们分别为与本地计算机进行 Socket 连接的另一计算机的 IP、域名和 端口。其中对于端口,如果客户端向服务器端请求某种特殊的服务时, 返回的端口号可能不同于客户请求的端口号 SocketHandle 返回 Socket 对象的 Windows 句柄。在直接调用 WinSock 的 API 时, 需要用到这个句柄,如果 Connected 属性为 false,则该属性的值为 INVAILD_SOCKET Close void __fastcall Close(void); 关闭 Socket 连接。对于服务器端,调用 Close 方法会断开与所有客户端的 Socket 连接,并退出监听状态;而客户端 调用此方法只断开它自己与服务器端的连接,不影响其它客户端的连 接 Listen void __fastcall Listen(const AnsiString Name, const AnsiString Address, const AnsiString Service, Word Port, int QueueSize, bool Block = true);此 方法用于被服务器端调用,使服务器端 Socket 处于监听状态 Lock void __fastcall Lock(void);此方法用于阻塞其它正在执行的线程,直到 调用 Unlock 为止 ReceiveBuf int __fastcall ReceiveBuf(void *Buf, int Count);此方法用于读取 Count 个 字节到 Buf 参数中,并返回实际读取的字节数。一般在处理 Socket 对 象的 OnSocketEvent 事件、TServerSocket 组件的 OnClientRead 事件、 第 11 章 远程屏幕监视程序 329 TClientSocket 组件的 OnRead 事件时使用 此方法只使用于非阻塞方式的 Socket ReceiveLength int __fastcall ReceiveLength(void);此方法用于估计从 Socket 连接中读取 数据的字节数 ReceiveText AnsiString __fastcall ReceiveText();从 Socket 连接中读取一个字符串并 返回该字符串。此方法一般在响应 Socket 对象的 OnSocketEvent 事件 或者响应 TServerSocket 组件的 OnClientRead 事件时调用 SendBuf int __fastcall SendBuf(void *Buf, int Count);使用 SendBuf 方法可以向另 一端的 Socket 发送数据。它将缓冲区 Buf 中 Count 个字节发送到 Socket 连接,并返回实际发送的字节数 SendStream bool __fastcall SendStream(Classes::TStream* AStream);此方法用于向另 一端的 Socket 发送一个流式对象,如果发送成功,返回 true SendStreamThenDrop bool __fastcall SendStreamThenDrop(Classes::TStream* AStream);此方法 于 SendStream 功能相似,但是它在流式对象发送完毕以后断开 Socket 连接 SendText int __fastcall SendText(const AnsiString S);向另一端的 Socket 发送一个 字符串 Unlock void __fastcall Unlock(void);解除对其它线程的阻塞 11.2.2 TServerWinSocket 类 TServerWinSocket 类用于描述处于 TCP/IP 监听状态的服务器端 Socket,一个服务器端 Socket 可以同时与多个客户端 Socket 连接,分别有各自的线程来处理这些连接。 TServerWinSocket 的主要属性和方法有: 表 11-2 TServerWinSocket 类的属性与方法 属性/方法 说明 ServerType enum TServerType { stNonBlocking, stThreadBlocking }; __property TServerType ServerType = {read = FServerType,write = SetServerType , nodefault};该属性用于设置与客户的连接方式,取为 stThreadBlocking 表示与每一个客户的连接都自动分配一个线程 (TServerClientThread 对象);stNonBlocking 表示用非阻塞方式连接每一 个客户 ActiveConnections 当前活动的连接数,也即 Connections 数组的元素个数 ActiveThreads 此属性返回当前正在使用的 TServerClientThread 对象的个数。 在服务器端,是通过 TServerClientThread 来管理与客户之间连接的每个线 程的 Connections __property TCustomWinSocket* Connections[int Index] = 第 11 章 远程屏幕监视程序 330 {read = GetConnections };此数组的每一个元素都代表一个当前活动的连 接(TServerClientWinSocket 对象) GetClientThread TServerClientThread* __fastcall GetClientThread(TServerClientWinSocket* ClientSocket);此方法返回由 ClientSocket 参数指定的连接所对应的线程对 象 TServerWinSocket 类的事件很多,其中常用的有 OnClientConnect、OnClientDisconnect、 OnClientRead 和 OnClientWrite。 z OnClientConnect 事件 服务器端 Socket 从监听状态到完成连接的过程如下:当服务器端 Socket 进入监听状态, 就会触发 OnSocketEvent 事件,并且 SocketEvent 的参数为 seListen;监听状态的服务器 Socket 监听到客户端的连接请求,触发 OnGetSocket 事件,而且,此时又会触发以 seAccept 为参数的 OnSocketEvent 事件;如果 ServerType 设置为 stThreadBlocking,而且缓冲区没 有线程对象可以使用,那么将触发 OnGetThread 事件;在 OnGetThread 事件的句柄中, 如果没有创建一个 TServerThread 线程对象,服务器端的 Socket 将自动创建线程对象, 创建线程对象以后,线程开始执行从而触发 OnThreadStart 事件;最后,触发 OnClientConnect 事件,通知服务器端 Socket 连接已经建立。 需要注意,如果 ServerType 属性为 stThreadBlocking,那么要保证 OnClientConnect 事件 的代码是线程安全的。 z OnClientDisconnect 事件 在服务器端 Socket 与某个客户端的连接被断开时,触发此事件。同时,相应的管理此连 接的 TServerClientWinSocket 线程对象被删除。如果 ServerType 属性被设置为 stThreadBlocking,在触发 OnClientDisconnect 事件之后还会触发 OnThreadEnd 事件。 z OnClientRead 事件 当客户端有数据发送过来时,会触发此事件,从而通知服务器端 Socket 对象去读取信息。 z OnClientWrite 事件 此事件通知服务器端 Socket 对象去写信息。 11.2.3 TClientWinSocket 类 TClientWinSocket 类是直接从 TCustomWinSocket 类继承而来,只增添了一个 ClientType 属性。ClientType 的声明如下: enum TClientType { ctNonBlocking, ctBlocking }; __property TClientType ClientType = {read=FClientType, write=SetClientType, nodefault}; ClientType 属性用于设置 Socket 的通信方式是阻塞通信还是非阻塞通信。如果使用非阻塞方 式,客户端 Socket 将进行异步读写,即在读写操作的时候,不会影响其它线程的执行。如果 使用阻塞方式,则读或写的操作必须在两个 Socket 之间同步进行。在阻塞方式下,可以使用 TWinSocketStream 类进行数据传输。 第 11 章 远程屏幕监视程序 331 11.2.4 TServerClientWinSocket 类 TServerClientWinSocket 用于描述与客户端 Socket 连接的服务器端 Socket 对象。只有当 服务器端监听到一个客户端的连接请求之后,才会创建 TServerClientWinSocket 对象来管理 与该客户之间的 Socket 连接。当与客户端之间的连接断开之后,该 TServerClientWinSocket 对象会自动删除。 TServerClientWinSocket 类也是从 TCustomWinSocket 类继承而来,并且只是增添了一个 ServerWinSocket 属性。该属性声明如下: __property TServerWinSocket* ServerWinSocket = {read=FServerWinSocket}; 此属性用于返回处于监听状态的服务器端 Socket 对象。 11.3 界面设计 程序分两部分,即服务器端程序和客户端程序。程序中在服务器端使用默认端口为 900。 11.3.1 服务器端 服务器端程序只负责监听客户连接,在与客户端完成连接之后,处理客户端发送过来的 命令,根据命令要求截取屏幕图象并发送回客户端,所以,服务器端程序基本上只需要后台 运行,界面设计无关紧要。 图 11-1 服务器端界面设计 如图 11-1,在新建窗体中添加一个 TServerSocket 组件,一个状态栏,和一个按钮。按 钮用户手工开启和关闭服务器 Socket 的监听状态;状态栏用于显示服务器 Socket 的状态信息 等。设置 ServerSocket1 的 Port 属性为 900,即默认的 Socket 服务端口。 11.3.2 客户端 z 主窗口界面 第 11 章 远程屏幕监视程序 332 客户端界面如图 11-2 所示。添加 TClientSocket 组件,即 Socket 通信的客户端。再在窗 体中添加一个 TImage 组件,并设置其 Align 属性为 alClient,设置其 Stretch 为 true,它用来 显示从服务器端传回来的图片。添加 TSavePictureDialog 对话框组件,用于保存 Image1 中的 图片。添加主菜单和一个弹出菜单 PopupMenu1,并设置 Image1 的弹出菜单为 PopupMenu1。 在主菜单中的“连接”菜单项添加“设置连接参数”、“建立连接”、“断开连接”、“退出程序” 四个子菜单;在“图象”菜单项中添加“全屏/关闭全屏”、“自动缩放”、“保存图象”、“下一 副”四个子菜单。弹出菜单的菜单项与主菜单中“图象”菜单的子菜单一样。 “图象”菜单项中,“自动缩放”是设置 Image1 中显示图片是是否让图片按照 Image1 的大小进行缩放显示,即 Image1 的 Stretch 属性,默认为 true,所以需要将“自动缩放”的 Checked 设为 true;“全屏/关闭全屏”用于切换是否全屏显示从服务器 Socket 传回的图片;“保 存图象”用于保存 Image1 中的图象;“下一幅”用于向服务器发送命令,使服务器 Socket 截 取屏幕图象并发送回客户端 Socket。 “连接”菜单项中,“建立连接”和“断开连接”用于建立和断开与服务器 Socket 之间 的连接,它们需要在设置连接参数之后才能使用,所以设置它们的 Enabled 为 false;“设置连 接参数”菜单项需要弹出一个对话框,设置服务器端 Socket 的2018香港马会开奖现场,以及传回图象的色深和 品质等参数。 图 11-2 客户端主界面设计 z 全屏窗口界面 图象切换到全屏显示时,需要弹出一个新的窗口,窗口设计很简单,只需要一个 TImage 组件用来显示图片,以及一个弹出菜单,弹出菜单的菜单项与主窗口中的弹出菜单相同。将 Image1 的 PopupMenu 属性设为弹出菜单 PopupMenu1。 因为要全屏显示图象,所以设置 Image1 的 Align 为 alClient,设置窗体的 BorderStyle 为 第 11 章 远程屏幕监视程序 333 bsNone,WindowState 为 wsMaximized。 界面设计图如图 11-3: 图 11-3 全屏窗体界面设计 z “连接参数设置”窗体界面 图 11-4 “连接参数设置”窗体界面 如图 11-4 设置窗体,其中图象色深的 TrackBar1 最小值为 1,最大值为 8,并设置其默 认选择值(Position 属性)为 3。图象品质 TrackBar2 的 Min 为 1,Max 为 10,Position 为 5。 确认按钮的 Kind 属性为 bkOK,取消按钮的 Kind 为 bkCancel。 11.4 服务器端功能实现 11.4.1 API 函数介绍 对屏幕图象的截取要用到几个 API 函数,先对它们稍做介绍。 z GetWindowRect 第 11 章 远程屏幕监视程序 334 BOOL GetWindowRect( HWND hWnd, // 窗口句柄 LPRECT lpRect // 用来存放窗口位置的矩形结构指针 ); 此函数用于取得指定窗口的区域范围,以整个屏幕的左上角为坐标原点。如果函数调用成功, 返回非零值,调用失败,返回零。 z GetDesktopWindow HWND GetDesktopWindow(VOID) 此函数用于取得桌面的句柄。 z GetForegroundWindow HWND GetForegroundWindow(VOID) 此函数用于取得当前窗口的句柄。 z GetDC HDC GetDC( HWND hWnd // 窗口句柄 ); 此函数用于获得指定窗口的显示设备描述表,利用此描述表,可以调用 GDI 函数在窗口区域 中进行图象读写操作。函数调用成功,返回指定窗口的设备描述表,调用失败,返回 NULL。 z BitBlt BOOL BitBlt( HDC hdcDest, // 描述表句柄 int nXDest, // 指定区域左上角 x 坐标 int nYDest, // 指定区域左上角 y 坐标 int nWidth, // 指定区域的宽度 int nHeight, // 指定区域宽度 HDC hdcSrc, // 图象源的设备描述表句柄 int nXSrc, // 源区左上角 x 坐标 int nYSrc, // 源区左上角 y 坐标 DWORD dwRop // 光栅模式 ); 此函数用于从指定设备读取指定区域内的图象,并保存在目的设备 hdcDest 中。其中图 象光栅模式可以取 BLACKNESS 、 DSTINVERT 、 MERGECOPY 、 MERGEPAINT 、 NOTSRCCOPY、NOTSRCERASE、PATCOPY、PATINVERT、PATPAINT、SRCAND 、 SRCCOPY、SRCERASE、SRCINVERT、SRCPAINT、WHITENESS, 本章程序中取光栅模式为 SRCCOPY,即将源图拷贝至目的位置。 第 11 章 远程屏幕监视程序 335 11.4.2 屏幕图象的截取 本程序中捕获屏幕图象的函数如下。其中参数 options 为 1 表示需要捕获的是整个屏幕的 图象,是 2 表示捕获当前活动的窗口;参数 level 和 cq 分别表示图象的色深和转换为 jpeg 图 象的图象品质;imagestream 是捕获的图象转换为内存流文件之后保存的内存2018香港马会开奖现场。 //--------------------------------------------------------------------------- // 捕获屏幕图象并保存到内存流 imagestream 中 void CaptureImage(int options, int level, int cq, TMemoryStream * imgstream) { LONG width,height; RECT capRect; HDC DesktopDC; //捕获屏幕区域选择 switch (options) { case CM_ENTIRESCREEN: // 捕获整个屏幕 //取得桌面的矩形区域范围 GetWindowRect(GetDesktopWindow(),&capRect); break; case CM_ACTIVEWINDOW: // 捕获当前窗口 HWND ForegWin; // 取得当前窗口句柄 ForegWin = GetForegroundWindow(); //如果当前句柄为空,则捕获整个屏幕 if (!ForegWin) ForegWin = GetDesktopWindow(); // 取得当前窗口的矩形区域范围 GetWindowRect(ForegWin,&capRect); break; } DesktopDC = GetDC(GetDesktopWindow()); // 创建内存设备描述表 //计算需要截取图象的举行区域的高度和宽度 width = capRect.right - capRect.left; height = capRect.bottom - capRect.top; Graphics::TBitmap *bBitmap; // 定义位图变量 try { //分配内存,设置图片大小 bBitmap = new Graphics::TBitmap(); bBitmap->Width=width; 第 11 章 远程屏幕监视程序 336 bBitmap->Height=height; if ((level>0)&&(level<8)) {// 设定色深 bBitmap->PixelFormat = TPixelFormat(level); } // 拷贝屏幕的指定区域到位图 BitBlt(bBitmap->Canvas->Handle,0,0,width,height,DesktopDC, capRect.left,capRect.top,SRCCOPY); if (cq>=0) { TJPEGImage *jpeg; try { jpeg = new TJPEGImage; // 创建 JPEG 图象 jpeg->CompressionQuality = cq; // 设定图象品质 jpeg->Assign(bBitmap); // 将位图转化为 JPEG 格式 jpeg->SaveToStream(imgstream); // 保存 JPEG 图象信息 } __finally { delete jpeg; // 释放资源 } } else { bBitmap->SaveToStream(imgstream); // 保存位图信息 } } __finally { delete bBitmap; // 释放资源 } } //--------------------------------------------- 11.4.3 客户端命令的提取与图象的发送 客户端如果需要服务器完成指定的任务,或者要给服务传送一些参数,方便的办法就是 发送指定格式的字符串,然后由服务器端对字符串进行分解,得到客户端发出的命令和参数。 本程序中,用回车符’\n’对命令以及参数进行分割,分割后的第一个字符串表示客户端发出的 命令类型,如要服务器截取图象并传回,则第一个字符串为“GetImage”;如果是要服务器改 变端口,则第一个字符串为“ChangePort”。根据不同的命令类型,再对后面的字符串进行分 第 11 章 远程屏幕监视程序 337 解。判断出不同的命令之后,再将后面的字符串分别命令的参数。 发送图片时,首先截取屏幕图片,然后将截取的图片保存到内存流文件中,再利用 ServerSocket 的 SendStream 方法发送到 Socket。需要注意的是,内存流文件的内存空间不需 要手动释放,在成功发送到 Socket 之后,会自动释放。 客户端的命令字符串到达服务器 Socket 后,触发服务器的 OnRead 事件,所以对命令字 符串的解析和执行都放在 ServerSocket1 的 OnClientRead 事件中,代码如下: //--------------------------------------------- // 捕获并发送自己的屏幕图象 void __fastcall TForm1::ServerSocket1ClientRead(TObject *Sender, TCustomWinSocket *Socket) { AnsiString sRecvString = Socket->ReceiveText(); // 保存接收到的字符串 AnsiString sRemoteAddress = Socket->RemoteAddress; // 保存对方 IP AnsiString cmd; int option,CL,CQ; // 先从字符串中分解出第一个参数 cmd int pos = sRecvString.Pos("\n"); // cmd,命令类型 cmd = sRecvString.SubString(1,pos-1); sRecvString = sRecvString.SubString(pos+1,sRecvString.Length()-pos); if(cmd=="GetImage") //读取图象 { pos = sRecvString.Pos("\n"); // 截取图象的选择参数,1 表示全屏图象,2 表示当前活动窗口 option = StrToIntDef(sRecvString.SubString(1,pos-1),0); sRecvString = sRecvString.SubString(pos+1,sRecvString.Length()-pos); pos = sRecvString.Pos("\n"); // 图象色深色深 CL = StrToIntDef(sRecvString.SubString(1,pos-1),0); sRecvString = sRecvString.SubString(pos+1,sRecvString.Length()-pos); pos = sRecvString.Pos("\n"); // 品质 CQ = StrToIntDef(sRecvString.SubString(1,pos-1),0); TMemoryStream *ImageStream; // 定义数据流 try { ImageStream = new TMemoryStream; // 分配内存 第 11 章 远程屏幕监视程序 338 // 捕获当前屏幕并保存到 ImageStream 中 if(option==CM_ACTIVEWINDOW) CaptureImage(CM_ACTIVEWINDOW, CL, CQ, ImageStream); else CaptureImage(CM_ENTIRESCREEN, CL, CQ, ImageStream); ImageStream->Position=0; //将指针位置指向流文件开头 // 发送 ImageStream 到接收端口 if (!ServerSocket1->Socket->Connections[0]->SendStream(ImageStream) ) MessageBox(0,"发送数据流失败","冰河",MB_ICONERROR); } __finally{//资源会自动释放 //delete ImageStream; // 释放资源 } } else if(cmd=="ChangePort") //改变服务器 Socket 端口 { int tmp; //先关闭 Socket 服务,然后更改 Port 属性,更改之后重新启动 Socket 服务 pos = sRecvString.Pos("\n"); // 读取端口 tmp = StrToIntDef(sRecvString.SubString(1,pos-1),0); if(tmp>0) { Button1->Click();//关闭 Socket 服务 ServerSocket1->Port=tmp; Button1->Click();//启动 Socket 服务 StatusBar1->SimpleText="端口变为"+ServerSocket1->Port; } } else //不能识别的命令 return; } //--------------------------------------------------------------------------- 11.4.4 服务器端 Socket 其它事件的响应 对 Socket 其它的事件,本程序中不需要做特殊的处理,对于几种常用的事件,在状态栏 中加入文本提示,代码如下: //--------------------------------------------------------------------------- 第 11 章 远程屏幕监视程序 339 void __fastcall TForm1::ServerSocket1Accept(TObject *Sender, TCustomWinSocket *Socket) { StatusBar1->SimpleText="接受客户端"+Socket->RemoteAddress+"的连接请求"; } //--------------------------------------------------------------------------- void __fastcall TForm1::ServerSocket1ClientConnect(TObject *Sender, TCustomWinSocket *Socket) { StatusBar1->SimpleText="来自"+Socket->RemoteAddress+"的客户端已接入"; } //--------------------------------------------------------------------------- void __fastcall TForm1::ServerSocket1ClientDisconnect(TObject *Sender, TCustomWinSocket *Socket) { StatusBar1->SimpleText="与"+Socket->RemoteAddress+"的连接已断开"; } //--------------------------------------------------------------------------- void __fastcall TForm1::ServerSocket1ClientError(TObject *Sender, TCustomWinSocket *Socket, TErrorEvent ErrorEvent, int &ErrorCode) { StatusBar1->SimpleText="发生错误了:("; } //--------------------------------------------------------------------------- 11.4.5 Socket 服务的开启和关闭 按钮 Button1 控制服务器 Socket 的开启和关闭,另外,在程序结束时,如果还有连接没 有断开,需要提示用户是否断开连接。代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::Button1Click(TObject *Sender) { if(!ServerSocket1->Active ) { 第 11 章 远程屏幕监视程序 340 ServerSocket1->Open(); Button1->Caption="停止服务"; } else { ServerSocket1->Close(); Button1->Caption="开启服务"; } } //--------------------------------------------------------------------------- void __fastcall TForm1::FormCloseQuery(TObject *Sender, bool &CanClose) { int result; if(ServerSocket1->Socket->ActiveConnections > 0) { result=MessageBox(Handle, "还有客户端与服务器保持连接,关闭窗口将失去与所有客户端的连接,要继续吗?" , "确定",MB_OKCANCEL); if(result==IDOK) { CanClose=false; return; } } ServerSocket1->Close(); CanClose=true; } //--------------------------------------------------------------------------- 11.5 客户端功能的实现 客户端 Socket 按照先后顺序,需要完成的工作有:首先打开参数设置窗口输入服务器地 址和其它参数;根据输入的服务器2018香港马会开奖现场连接服务器 Socket;连接之后发出获取图象的命令; 最后由 ClinetSocket 的 OnRead 事件从 Socket 中读取从服务器传回的图片,并显示在 Image1 中。另外,也可以发送更改服务器的服务端口的命令。 对于图象的显示,主要是要可以切换到全屏模式。由于主窗口的主菜单不能设为不可见, 所以只能新建窗口,在新窗口中实现全屏。对于显示在 Image1 中的图象,给出保存图象的功 能。 第 11 章 远程屏幕监视程序 341 通过双击 Image1,也可以获取下一幅图象。 11.5.1 连接参数的设置 与服务器的连接参数,有服务器2018香港马会开奖现场、图象色深和图象品质,它们在窗体 FormConfig 中输入。此窗体在选择主菜单“连接”->“设置连接参数”时弹出,此菜单的响应代码如下: //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuConnectConformClick(TObject *Sender) { if(FormConfig->ShowModal()==mrOk) { //默认使用 900 端口 ServerPort=900; //默认截取全屏图象 Options=1; //Serverport、CL、CQ 是在头文件中声明的全局变量 ClientSocket1->Address=FormConfig->ComboBox1->Text; // 主机 IP ClientSocket1->Port=ServerPort; //默认 900 CL=FormConfig->TrackBar1->Position; // 色深 CQ=FormConfig->TrackBar2->Position*10; // 品质 MenuConnectConnect->Enabled=true; } } //--------------------------------------------------------------------------- FormConfig 窗体中,按下确认键之后,需要检查输入的 ip 2018香港马会开奖现场是否已经在组合框中, 如果是新的 ip 2018香港马会开奖现场,则把它加入组合框以供下次选择。确认按钮的响应代码如下: //--------------------------------------------------------------------------- void __fastcall TFormConfig::BitBtn1Click(TObject *Sender) { // 如果是新的 IP,则保存到下拉列表中 TStringList *IPList = new TStringList; IPList->AddStrings(ComboBox1->Items); int Index; if (!IPList->Find(ComboBox1->Text,Index)) { IPList->Append(ComboBox1->Text); ComboBox1->Items->Clear(); ComboBox1->Items->AddStrings(IPList); } 第 11 章 远程屏幕监视程序 342 delete IPList; } //--------------------------------------------------------------------------- 11.5.2 “连接”菜单的响应 “连接参数设置”子菜单的代码上面已经列出,“建立连接”、“断开连接”与“退出程 序”的响应代码如下: //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuConnectConnectClick(TObject *Sender) { if(!ClientSocket1->Active) //连接状态 ClientSocket1->Open(); } //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuConnectDisconnectClick(TObject *Sender) { if(ClientSocket1->Active) //连接状态 ClientSocket1->Close(); } //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuConnectExitClick(TObject *Sender) { Close(); } //--------------------------------------------------------------------------- “建立连接”、“断开连接”两个菜单项在 Socket 连接连通和断开状态具有不同的 Enabled 属性,对他们 Enabled 属性的更改放在 ClientSocket1 的相应事件的响应代码中。 另外,与服务器端 Socket 一样,在关闭程序时判断一下是否断开了 Socket 连接,代码如 下: //--------------------------------------------------------------------------- void __fastcall TFormMain::FormCloseQuery(TObject *Sender, bool &CanClose) { //关闭客户端窗体 int result; 第 11 章 远程屏幕监视程序 343 if(ClientSocket1->Active) { result=MessageBox(Handle,"与服务器连接还没有断开,确定要退出吗?", "确定",MB_OKCANCEL); if(result==IDOK) { ClientSocket1->Close(); CanClose=true; } else { CanClose=false; } } } //--------------------------------------------------------------------------- 11.5.3 命令的发送和返回图象的读取 前面已经说过,客户端向服务器发送的命令为一个字符串,字符串由’\n’分割为几个子串, 第一个子字符串是命令的类型,后面的子串是命令参数。本程序中并没有给出改变服务器端 口的菜单项,但在代码中以注释的方式列出了发送命令的示例。 向服务器发送获取图象的命令的操作有五个地方:主菜单中“下一幅”子菜单、主窗体 和全屏窗体的 Image 组件的右键弹出菜单的“下一幅”子菜单、以及它们两个 Image 的鼠标 双击事件。实现代码在主菜单中,其它地方都是通过调用主菜种“下一幅”子菜单的 Click 事件代码来实现的。其代码如下: //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuImageNextClick(TObject *Sender) { if(!ClientSocket1->Active) return; AnsiString cmd="GetImage"; //AnsiString cmd="ChangePort"; //改变服务器端口的命令 //AnsiString Msg = cmd + "\n" + NewPort + "\n"; AnsiString Msg = cmd + "\n" + IntToStr(Options) + "\n" + IntToStr(CL) + "\n" + IntToStr(CQ) + "\n"; ClientSocket1->Socket->SendText(Msg); 第 11 章 远程屏幕监视程序 344 } //--------------------------------------------------------------------------- 从服务器返回的图象数据触发 ClientSocket 的 OnRead 事件,所以,从 Socket 中读取图 象数据的代码放在 OnRead 事件的响应代码中。其中需要注意的是,由于非阻塞通信中只提 供了 SendStream 这个发送流文件的方法,并没有相应的接收流文件的方法,所以对于流文件 的接收,只能用 ReceiveBuf 方法从 Socket 循环读取数据,然后组合成一个流文件,而且,每 循环一次都要让程序延迟一段时间,因为毕竟读取数据的速度远大于从服务器传回数据的速 度,如果不设置事件延迟,读文件的操作会在文件还没传送完毕就终止。 ClientSocket1 的 OnRead 事件响应代码如下: //--------------------------------------------------------------------------- void __fastcall TFormMain::ClientSocket1Read(TObject *Sender, TCustomWinSocket *Socket) { TMemoryStream *Stream; try { int n; char Buffer[1000]; // 定义一个数据流并分配内存 Stream = new TMemoryStream; // 开始接收图象到数据流中 do{ n=Socket->ReceiveBuf(Buffer,sizeof(Buffer)); if(n<=0) break; else Stream->Write(Buffer,n); Sleep(200); //延迟 200ms }while(1); if (Stream->Size)//不为空 { Stream->Position=0; TJPEGImage *jpeg; // 定义 JPEG 图象 try { jpeg = new TJPEGImage; // 分配内存 // 从数据流中载入图象 jpeg->LoadFromStream(Stream); // 显示图象 Image1->Picture->Bitmap->Assign(jpeg); MessageBeep(MB_OK); // 发出提示声音 第 11 章 远程屏幕监视程序 345 } __finally { delete jpeg; // 释放资源 } } } __finally { delete Stream; // 释放资源 } } //--------------------------------------------------------------------------- 11.5.4 客户端 Socket 其它事件的响应 对于 ClientSocket1 的事件,大都在状态栏中显示相应的提示信息。对于连接建立和断开 事件,还需要对主菜单中“建立连接”和“断开连接”两个子菜单的 Enabled 属性进行更改。 代码如下: //--------------------------------------------------------------------------- void __fastcall TFormMain::ClientSocket1Connect(TObject *Sender, TCustomWinSocket *Socket) { MenuConnectConnect->Enabled=false; MenuConnectDisconnect->Enabled=true; StatusBar1->SimpleText="与服务器连接成功"; } //--------------------------------------------------------------------------- void __fastcall TFormMain::ClientSocket1Error(TObject *Sender, TCustomWinSocket *Socket, TErrorEvent ErrorEvent, int &ErrorCode) { StatusBar1->SimpleText="连接到服务器时发生错误"; } //--------------------------------------------------------------------------- void __fastcall TFormMain::ClientSocket1Disconnect(TObject *Sender, TCustomWinSocket *Socket) { MenuConnectDisconnect->Enabled=false; 第 11 章 远程屏幕监视程序 346 MenuConnectConnect->Enabled=true; StatusBar1->SimpleText="连接已断开"; } //--------------------------------------------------------------------------- void __fastcall TFormMain::ClientSocket1Connecting(TObject *Sender, TCustomWinSocket *Socket) { StatusBar1->SimpleText="正在连接服务器..."; } //--------------------------------------------------------------------------- 11.5.5 其它菜单的相应 对于其它菜单,图象的保存,全屏显示的切换等,代码如下: //--------------------------------------------------------------------------- //主菜单->全屏/关闭全屏 菜单的响应 void __fastcall TFormMain::MenuImageFullScreenClick(TObject *Sender) { if(!FormFullScreen->Visible) { FormFullScreen->Image1->Picture=Image1->Picture; FormFullScreen->Visible=true; } else FormFullScreen->Visible=false; } //--------------------------------------------------------------------------- //主菜单->自动缩放 菜单的响应 void __fastcall TFormMain::MenuImageStretchClick(TObject *Sender) { //设置 Image 显示图片时是否自动缩放 MenuImageStretch->Checked=!MenuImageStretch->Checked; Image1->Stretch=MenuImageStretch->Checked; //更改右键弹出菜单项的 Check PopupStretch->Checked=MenuImageStretch->Enabled; //更改全屏窗口的 Image 的 Stretch FormFullScreen->Image1->Stretch=Image1->Stretch; 第 11 章 远程屏幕监视程序 347 } //--------------------------------------------------------------------------- //主菜单->保存图象到文件 void __fastcall TFormMain::MenuImageSaveClick(TObject *Sender) { // 将图象保存为文件 if (SavePictureDialog1->Execute()) Image1->Picture->Bitmap->SaveToFile(SavePictureDialog1->FileName); } //--------------------------------------------------------------------------- //主窗口弹出菜单 全屏/关闭全屏 菜单 void __fastcall TFormMain::PopupFullScreenClick(TObject *Sender) { MenuImageFullScreen->Click(); } //--------------------------------------------------------------------------- //主窗口弹出菜单 自动缩放 菜单 void __fastcall TFormMain::PopupStretchClick(TObject *Sender) { MenuImageStretch->Click(); } //--------------------------------------------------------------------------- //主窗口弹出菜单 保存图象 菜单 void __fastcall TFormMain::PopupSaveClick(TObject *Sender) { MenuImageSave->Click(); } //--------------------------------------------------------------------------- //主窗口弹出菜单 下一幅 菜单 void __fastcall TFormMain::PopupNextClick(TObject *Sender) { MenuImageNext->Click(); } //---------------------------------------------------------------------------- //主窗口 Image1 的鼠标双击事件 void __fastcall TFormMain::Image1DblClick(TObject *Sender) { MenuImageNext->Click(); } //--------------------------------------------------------------------------- 第 11 章 远程屏幕监视程序 348 全屏显示窗口 FormFullScreen 的弹出菜单和 Image1 的鼠标事件响应: //--------------------------------------------------------------------------- //FormFullScreen 窗口弹出菜单 全屏/关闭全屏 菜单 void __fastcall TFormFullScreen::PopupFullScreenClick(TObject *Sender) { FormMain->MenuImageFullScreen->Click(); } //--------------------------------------------------------------------------- //FormFullScreen 窗口弹出菜单 自动缩放 菜单 void __fastcall TFormFullScreen::PopupStretchClick(TObject *Sender) { FormMain->MenuImageStretch->Click(); } //--------------------------------------------------------------------------- //FormFullScreen 窗口弹出菜单 保存图象 菜单 void __fastcall TFormFullScreen::PopupSaveClick(TObject *Sender) { FormMain->MenuImageSave->Click(); } //--------------------------------------------------------------------------- //FormFullScreen 窗口弹出菜单 下一幅 菜单 void __fastcall TFormFullScreen::PopupNextClick(TObject *Sender) { FormMain->MenuImageNext->Click(); //将 FormMain 中 Image1 内的图显示在全屏窗口的 Image1 中 Image1->Picture=FormMain->Image1->Picture; } //--------------------------------------------------------------------------- //FormFullScreen 窗口 Image1 鼠标双击事件 void __fastcall TFormFullScreen::Image1DblClick(TObject *Sender) { //双击获取下一幅图片 PopupNext->Click(); } //--------------------------------------------------------------------------- 11.6 任务栏图标的使用 对于服务器端的程序,我们不希望它运行的时候总是有个窗口在桌面上,最好的办法就 第 11 章 远程屏幕监视程序 349 是象金山词霸一样,在任务栏有个图标。本节欲实现的任务栏图标效果如图 11-5 和图 11-6: 图 11-5 任务栏图标提示文本效果 图 11-6 任务栏图标右键菜单效果 要实现这个功能也不复杂,只需要用一个 API 函数 Shell_NotifyIcon,其声明如下: WINSHELLAPI BOOL WINAPI Shell_NotifyIcon( DWORD dwMessage, // 消息标志 PNOTIFYICONDATA pnid // NOTIFYICONDATA 结构指针 ); 其中,dwMessage 是发送消息的标志,可以取得值有: NIM_ADD // 往任务栏添加图标 NIM_DELETE //从任务栏删除图标 NIM_MODIFY //通知任务栏修改图标,即重绘图标 z 菜单添加 首先,在任务栏图标上点击右键,需要弹出一个菜单,所以我们先往窗体中添加弹出菜 单,其设计图如图 11-7: 图 11-7 任务栏图标右键菜单 其中,第一个菜单项的 Caption 需要与窗体中 Button 相同,也即在 Socket 开启的时候显 示“关闭服务”,在 Socket 关闭的时候显示“开启服务”。所以,在 PopupMenu1 的 OnPopup 事件中添加如下代码: //--------------------------------------------------------------------------- void __fastcall TForm1::PopupMenu1Popup(TObject *Sender) 第 11 章 远程屏幕监视程序 350 { PopOpenClose->Caption=Button1->Caption; } //--------------------------------------------------------------------------- 其它菜单项的响应代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::PopExitClick(TObject *Sender) { Close(); } //--------------------------------------------------------------------------- void __fastcall TForm1::PopShowClick(TObject *Sender) { if (Form1->Visible) { SetForegroundWindow(Handle); } else Show(); } //--------------------------------------------------------------------------- void __fastcall TForm1::PopOpenCloseClick(TObject *Sender) { Button1->Click(); } //--------------------------------------------------------------------------- z 消息截取 在头文件中,添加如下内容: #define MYWM_NOTIFY (WM_APP+100) //自定义消息 #define IDC_MYICON 1006 //图标标志号 public: // User declarations BEGIN_MESSAGE_MAP //截取自定义消息 MESSAGE_HANDLER(MYWM_NOTIFY,TMessage,MyNotify) END_MESSAGE_MAP(TForm); void __fastcall MyNotify(TMessage& Msg); bool __fastcall TrayMessage(DWORD dwMessage); 第 11 章 远程屏幕监视程序 351 z 添加任务栏图标 要向任务栏添加图标,只需要定义一个 NOTIFYICONDATA 结构,为其指定提示文本、 窗口句柄、图标句柄等参数,然后调用 API 函数 Shell_NotifyIcon 即可。我们定义函数 TrayMessage,根据参数的不同为 NOTIFYICONDATA 结构设置不同的参数,然后调用 Shell_NotifyIcon 函数实现图标的添加、修改和删除。 本程序中为 Form1 指定一个图标(Icon 属性),将此图标作为任务栏中显示的图标,并 将窗体的标题(Caption 属性)作为鼠标停留在任务栏图标上时显示的提示文本。TrayMessage 函数代码如下: //--------------------------------------------------------------------------- bool __fastcall TForm1::TrayMessage(DWORD dwMessage) { NOTIFYICONDATA tnd; PSTR pszTip; //将窗体标题作为任务栏图标的提示内容 pszTip = Form1->Caption.c_str(); tnd.cbSize = sizeof(NOTIFYICONDATA); //结构的大小 tnd.hWnd = Handle; //接受回调消息的窗口句柄 tnd.uID = IDC_MYICON; //图标标志号 //指定以下三个参数哪个包含有效数据 tnd.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; tnd.uCallbackMessage = MYWM_NOTIFY;//自定义的回调消息,在头文件中声明 if (dwMessage == NIM_MODIFY) { tnd.hIcon =(HICON)Form1->Icon->Handle; //取得图标句柄 if (pszTip) lstrcpyn(tnd.szTip, pszTip, sizeof(tnd.szTip)); else tnd.szTip[0] = '\0'; } else { tnd.hIcon = NULL; tnd.szTip[0] = '\0'; } return (Shell_NotifyIcon(dwMessage, &tnd)); } //-------------------------------------------------------------------------------- 第 11 章 远程屏幕监视程序 352 在程序开始运行时,即在任务栏显示图标,在程序关闭时,删除任务栏图标,代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::FormCreate(TObject *Sender) { //在任务栏添加图标,并绘制图标 TrayMessage(NIM_ADD); TrayMessage(NIM_MODIFY); } //------------------------------------------------------------------------- void __fastcall TForm1::FormCloseQuery(TObject *Sender, bool &CanClose) { …………//其它代码略去 TrayMessage(NIM_DELETE);//删除任务栏图标 } //------------------------------------------------------------------------- z 任务栏图标右键 通过拦截自定义的消息,获得任务栏程序图标的鼠标事件,在事件处理中区别是鼠标左 键还是右键,如果是左键,则将窗体置到桌面前端,如果窗体隐藏,则显示之;如果是右键, 则弹出菜单。实现此过程的代码如下: //------------------------------------------------------------------------- void __fastcall TForm1::MyNotify(TMessage& Msg) { POINT MousePos; switch(Msg.LParam) { case WM_RBUTTONUP: if (GetCursorPos(&MousePos)) //取得鼠标所在位置 { PopupMenu1->PopupComponent = Form1; //在鼠标所在位置弹出菜单 PopupMenu1->Popup(MousePos.x, MousePos.y); } else Show(); break; case WM_LBUTTONUP: if (Form1->Visible) 第 11 章 远程屏幕监视程序 353 { //将窗体显示到最上方 SetForegroundWindow(Handle); } else Show(); break; default: break; } TForm::Dispatch(&Msg); } //------------------------------------------------------------------------- 11.7 思考题 z 程序中给出了让服务器改变服务端口的命令格式以及服务器对此类命令的处理,请读者 在程序中添加菜单项来完成此项功能 z 参照第九章的内容,设计命令格式,让服务器端发送回来其当前窗口、环境变量、磁盘、 内存等系统信息 z 程序中客户端对流文件的读取采用延迟循环读取的方式,这种方式并不好,因为如果网 络速度很快,这种方法会浪费时间,而如果网络太慢,则会因为无数据可读而退出读操 作,当然可以根据网络速度设置不同的延迟时间,但是,是否有其它更好的解决方法呢? 请实现之 z 在 NT 系统中,程序的完全隐藏是不可能的,但是要让程序不容易被发现还是有很多方 法的,你知道哪些? 《C++ Builder 6 编程实例精解 赵明现》 第 12 章 俄罗斯方块 本章重点 本章通过俄罗斯方块游戏的制作示例一个完整的 Windows 游戏程序的开发过程。对于一 个实际问题,在计算机实现时,通常将它划分为几个功能模块来分别实现,而游戏编程的功 能模块结构尤为明显。本章还介绍了制作 hlp 帮助文件的方法。 学习目的 通过本章的学习,您可以: ■ 掌握对实际问题进行模块划分和分别实现的方法 ■ 了解游戏编程的思路 ■ 掌握俄罗斯方块的编写技巧 ■ 掌握帮助文件的制作 ■ 掌握程序中启动帮助文件的方法 第 12 章 俄罗斯方块 355 第 12 章 俄罗斯方块 356 本章典型效果图 第 12 章 俄罗斯方块 357 俄罗斯方块是一个很经典的游戏,一般学编程都会做小游戏来锻炼自己的编程能力,而 俄罗斯方块,相信是很多人都做过的其中一个。选择一个游戏作为编程能力的综合考验,这 主要是因为: z 对于游戏程序来说,一般具有比较复杂的逻辑结构,对于本章要做的俄罗斯方块游戏, 算是一个规模比较大程序。通过对本程序的规划和实现,锻炼程序员对规模比较大的程 序的驾驭能力。 z 游戏程序作为比较典型的具有实际应用价值的应用程序,设计到很多方面的技术,是对 各种编程技术技巧的考验。 z 对于只为锻炼自己编程能力的程序员来说,游戏无疑具有更大的吸引力,更能激起对编 程技术的钻研兴趣。 本章分别从游戏的界面设计、数据结构、各个游戏类的定义和实现以及帮助文件的制作 等方面来讲述俄罗斯方块游戏的具体制作过程。 12.1 界面设计 图 12-1 俄罗斯方块界面设计 俄罗斯方块游戏程序的界面设计图如图 12-1 所示,关键的组件是四个 Panel,两个大的 Panel 为双人游戏时的两个游戏玩家的游戏区域,它们的 Name 分别为 Panel1 和 Panel2,两 个小的 Panel 为绘制预览方块的区域,其 Name 属性分别为 Panel3 和 Panel4。 窗体中加入一系列的 StaticText 组件,用于显示得分、消去的函数、游戏级别以及游戏 第 12 章 俄罗斯方块 358 的控制按键等。之所以使用 StaticText 而不用 Label 是因为对于 StaticText 可以设置更多的属 性,如边框、底色等,可以美化界面。需要注意的是,所有 StaticText 的 AutoSize 属性都应 该设为 false,以便其 Caption 属性改变时,不影响界面的格局。 添加两个定时器组件,Timer1 和 Timer2,它们分别用于两个游戏玩家的游戏控制。方块 的下落就是要靠定时器事件来触发的,而对定时器的 Interval 属性的设置可以更改游戏的速 度。 添加主菜单,菜单项“游戏”用于游戏的开始、暂停和退出操作;菜单项“Multiplayers” 用于选择单人游戏模式还是双人对战模式;菜单项“设置”用于游戏的配置操作;“帮助”用 于打开程序的帮助文件。 12.2 游戏逻辑结构和数据组织 12.2.1 游戏的结构分析 俄罗斯方块游戏中,游戏界面区中不断有各种形状的方块从上向下落,在遇到下面已经 固定的方块或者界面区域的底部而不能再下落时,则被固定在所在位置,并且游戏将预览的 方块在游戏区域从上继续下落,而且产生一个新的预览方块;当方块堆积的超过游戏界面区 域时(或者游戏者积分超过一定值时),游戏结束。在固定方块时,需要判断是否有全都是方 块的行,如果有,则消去该行,并根据预定的规则增加游戏者获得的分数,并修改游戏的级 别(即游戏的速度)。 游戏的控制,也即当前正在下落的方块的左右移动和变形操作,由按键控制,根据相应 的按键调用相应的函数过程。当方块左右移动遇到游戏界面中已经固定的方块时,它就被阻 挡而不能移动,或者当方块已经到了游戏区域的两侧边界时,也不能移动。对于变形操作, 我们用下落方块的变形后的方块跟游戏区域中已经固定的方块比较,如果变形后没有覆盖已 经固定的方块,则下落方块可以变形,否则,它就受到了一定固定的方块的阻挡而不能变形。 在双人休息时,需要实现“赠送礼物”的功能,即如果一个游戏者一次消去了多行,则 在另一个游戏者的游戏界面的底层插入几行的方块,当然,行中要随机的有缺陷。赠送和接 收礼物的操作都在每次消行的操作之后。 游戏界面的底部留有一个窄横条,用来绘制当前下落的方块的位置。游戏界面左右各一 个竖条,它在双人游戏时使用,用来显示对家游戏界面中累积的方块的高度。 由于俄罗斯方块不涉及复杂的绘图操作,只需要绘制方形的区域,我们可以使用 TShape 组件来表示一个小方块,在 TShape 中提供足够的属性供我们改变方块的颜色、边线等属性。 12.2.2 Square 类的定义 由上面分析,我们知道,游戏中的最小单位是小方块,一般俄罗斯方块的界面都 20×10 的方块的规模,在本章的程序中,我们使用 22×10 的规模。在小方块的基础上,可以定义 第 12 章 俄罗斯方块 359 下落方块的结构,它可以有一个小方块矩阵来实现;预览方块的定义与下落方块的定义相同; 游戏区域的定义则可以定义一个 22×10 的小方块的矩阵来实现。在头文件中定义 BlockW 和 BlockH 分别表示游戏区域小方块矩阵的大小,如下: #define BlockH 22 #define BlockW 10 我们约定,小方块我们叫做“方格”,在程序中用 Square 来表示;而下落和预览的方格 矩阵我们称之为“方块”,程序中用 Blocks 表示。 我们在游戏中定义一个 Square 类来表示方格的属性,定义 Blocks 类来表示方块的属性。 现在考虑一下,对于方格类 Square,应该定义哪些属性。首先,由上面的分析,我们看书对 于每一个方格,它可能有两种状态,要么方格是空心的,要么是实心的。但是对于实心方格, 还应该分为两种情况,其一是游戏区域中已经被固定的方格,其二是正在下落的方块在游戏 区域中占据的方格。所以在 Square 类中,我们定义 IsBlock 属性来表示方格是否是实心的, 再定义 IsActive 属性来表示方格是否是正在下落的方块中的实心方格在游戏区域中占据的位 置。最后,为了游戏美观,我们为方格再增加一个颜色属性。 Square 类的定义如下: //--------------------------------------------------------------------------- class Square { public: Square(){IsBlock=false; IsActive=false; /*Color=SquareColor[0];*/} bool IsBlock; //方格中是否有已经固化(不会再下落)的方块 bool IsActive; //方格中是否有正在下落的方块 int Color; //方格的颜色 }; //--------------------------------------------------------------------------- 其中的 Color 属性为 1~8 的一个整数,作为颜色数组 SquareColor 的下标。SquareColor 的定 义如下: const int SquareColor[8]={0x004E3B1,0x004988F1,0x0071CCB1,0x00D58AD2,0x00EADD44, 0x0070C6EB,0x00FDB7C1,0x00367BEF}; 其实,可以用一个整数来表示方格的三种状态,我们这里定义两个量,是为了在编写程序过 程中看着易懂,方便。 12.2.3 Blocks 类的定义 各种形状的方块都可以放置在一个 4×4 的方格中,所以我们对于方块,使用一个 4×4 的方格数组中实心方块的位置(数组下标)来表示。比如方格: 第 12 章 俄罗斯方块 360 图 12-2 方块的数据表示 它有四个方格,且四个方格在 4×4 数组中的位置为(1,1)、(1,2)、(2,1)、(2、2),所 以对于此方块,我们就可以使用一个数组[1,1,1,2,2,1,2,2]来表示。 首先,我们在 Blocks 类中需要定义一个 4×4 的方格,Square s[4][4]。 其次,经典的俄罗斯有 7 中不同的方块,每种方块有四种变形,方块的每种变形都需要 一个 8 个元素的一位数组来表示,所以,所有的方块形状及其变形可以用一个 7×4×8=224 个元素的数组来表示。 在 Blocks 类中定义该数组命名为 sAll,并定义一个整数 Shape 用来表 示当前方块形状数组(8 个元素)在 sAll 中的起始位置。sAll 数组的内容见 Blocks 的构造函 数。 对于下落的一个方块,对它里面的实心方格的范围,我们定义 DownPos,UpPos,LeftPos,RightPos,分别表示实心方格在 4×4 的方格组成的方块数组中最下 方、最上方的行位置和最左、最右方在主游戏区的列位置。 在游戏中,我们对于一个方块中的所有方格都使用同一种颜色,而且,七种方块各对应 自己的颜色。所以在 Blocks 类中定义颜色属性 Color,它依然是颜色数组 SquareColor 中的下 标。Color 的值由方块形状 Shape 决定。 Blocks 类的定义如下: //--------------------------------------------- class Blocks { public: int DownPos,UpPos,LeftPos,RightPos; int Color; Square s[4][4]; Blocks(); void SetBlock(int choise,int Clr);//choise=0-27 //返回下一个变形方块的 Shape 值 int GetNewShape(); private: int sAll[224]; //sAll[7*4*8];//7 种形状,4 个变形,8 个整数表示一个 4*4 矩阵 int Shape;//sAll 数组中下标位置÷8 }; //--------------------------------------------- 在 Blocks 类中,构造函数完成对 sAll 数组的赋值操作,SetBlock 方法的入口参数 choise 即方块的形状数组在 sAll 数组中的起始位置,根据它对 4×4 方格数组 Squares 进行赋值;另 一个参数 Clr 为方块的颜色,这里只是留下的一个扩展接口,实际赋值中,颜色由 Shape(即 choise)决定。 第 12 章 俄罗斯方块 361 Blocks 中各函数的代码在后面的小节中讲述,其中构造函数仅仅完成 sAll 的赋值操作, 所以列出其代码如下: //-----------------Blocks 类------------------ Blocks::Blocks() { int temp[224]={ 1,1,1,2,2,1,2,2, 1,1,1,2,2,1,2,2, //** 1,1,1,2,2,1,2,2, //** 1,1,1,2,2,1,2,2, //1 1,0,1,1,1,2,2,1, // 0,1,1,1,1,2,2,1, //*** 0,1,1,0,1,1,1,2, // * 0,1,1,0,1,1,2,1, //2 0,1,1,1,2,1,3,1, // * 1,0,1,1,1,2,1,3, // * 0,1,1,1,2,1,3,1, // * 1,0,1,1,1,2,1,3, //3 * 0,2,1,1,1,2,2,1, // 1,0,1,1,2,1,2,2, // * 0,2,1,1,1,2,2,1, // ** 1,0,1,1,2,1,2,2, //4* 0,1,1,1,1,2,2,2, // 1,1,1,2,2,0,2,1, // * 0,1,1,1,1,2,2,2, // ** 1,1,1,2,2,0,2,1, //5 * 1,0,1,1,2,1,3,1, // 1,0,1,1,1,2,2,0, // ** 0,0,1,0,2,0,2,1, // * 1,2,2,0,2,1,2,2, //6 * 1,1,1,2,2,1,3,1, // 1,1,2,1,2,2,2,3, // ** 0,2,1,2,2,1,2,2, // * 1,1,1,2,1,3,2,3}; //7 * for(int j=0;j<224;j++) sAll[j]=temp[j]; } //------------------------------------------- 第 12 章 俄罗斯方块 362 12.2.4 MainFrame 类的定义 定义 MainFrame 类,表示游戏的主界面,即游戏区域的各种属性和相关的方法。我们这 里使用 TShape 组件来作为方格的可视化输出(当然也可以用画图的方法处理,只需要把程 序中处理输出的部分修改即可),并且,所有的 Shape 都输出在一个 Panel 上面,窗体中添加 了 Panel1 和 Panel2 就是用于盛放游戏区域的 TShape 组件的。预览方块的处理也在 MainFrame 中,它画在另外的 Panel 上,即 Panel3 和 Panel4。 MainFrame 类的定义如下: //--------------------------------------------- class MainFrame { public: MainFrame(); //设置游戏区域对应的游戏区输出 Panel 和预览区输出 Panel void SetPanel(TPanel *Panel,TPanel *Panel2); //初始化主方框 void Init(void); //根据当前的主方框情况,整体重绘 //如果游戏界面可能被破坏时用 void ReDraw(); //用来处理方块下降的函数 //如果可以下移并且移动成功,返回 true, //否则,如果不能下移,返回 false bool BlockDown(); //用来处理左移的函数,如果可以左移并且移动成功,返回 true //否则,如果不能左移,返回 false bool BlockLeft(); //用来处理右移的函数,如果能够右移并且右移成功,返回 true //否则,如果不能右移,返回 false bool BlockRight(); //方块变形处理函数,如果可以变形,并且变形成功,返回 true //否则,如果不能变形,返回 false bool BlockChangeShape(); //当方块不能下移时,用来固化(IsAvtive->IsBlock)方块,消行, //以及生成新方块的函数 //如果方块固化后 MainFrame 没有填满,即游戏没有结束,返回消掉的行数 //如果游戏结束,返回-1 int BlockEnd(); //画预览方块 第 12 章 俄罗斯方块 363 void DrawPreView(int IfBg); //画底部方块位置标志 void DrawDown(void); //画游戏区域左右侧的对家高度标志 void DrawLR(int top); //允许外部访问的变量 int MainTop;//纪录 MainFrame 中实心方块的最高位置的 X 下标 int Gift; //另一个游戏者给的增量,每次新方块出来之前检查 gift,确定是否需要增加 private: TPanel *MumPanel,*PrePanel; TShape *MainShape[BlockH][BlockW];//主体方框的位图数组 TShape *DownShape[BlockW]; //底部标志方块位置 TShape *LRShape[2][BlockH]; //左右边对家高度标志 TShape *PreShape[4][4]; //预览 Square MainSquare[BlockH][BlockW]; Blocks MainBlocks;//声明当前方块 Blocks Temp; //作为中间变量的备用参数,画预览方块时也用到 int MainBlocksShape;//当前方块的形状参数 int NextBlocksShape;//预览方块的形状参数 //int NextBlocksColor; int BlockPosX,BlockPosY;//给出 4*4block 的最下一行在 MainFrame 中的 //位置以及最左边一列的位置 }; //------------------------------------------ 其中,MumPanel 和 PrePanel 为游戏区域的 TShape 组件和预览区域的 TShape 组件的容 器;MainShape 和 PreShape 分别是游戏区的 TShape 数组和预览区的 TShape 数组;DownShape 是游戏区底部用来绘制当前下落方块的左右位置的 TShape 数组;LRShape 是左右两侧用来 绘制对家累积的方块高度的 TShape 数组;MainSquare 为游戏区的方格状态数组;MainBlocks 和 NextBlocks 为当前下落的和预览的方块;BlockPosX 和 BlockPosY 表示当前下落的方块在 主游戏区的位置;MainBlockShape 和 NextBlockShape 表示当前下落的方块和下一个方块(现 在的预览方块)的形状;Temp 是一个临时变量,在 MainFrame 的函数实现中要使用。 MainFrame 类中最重要的方法,就是方块的左右移动、向下移动、方块的固定、消行和 变形操作。各个方法的具体实现,在后面小节中讲述。 12.2.5 TetrisGame 类的定义 定一个游戏界面类 MainFrame 之后,我们在定义 TetrisGame 类来实现对游戏的控制。包 括游戏的计分、级别,游戏初始化、开始、暂停和结束等操作。 第 12 章 俄罗斯方块 364 TetrisGame 类定义如下: //------------------------------------------ class TetrisGame { public: int Status;//1:游戏进行中 2:游戏暂停中 3:游戏结束或者未开始 int Score;//游戏得分 int Rate; //方块下落速度,ms 单位 bool HighRate;//是否加速状态 int Level;//游戏级别 01-10 int Line1,Line2,Line3,Line4; //Line removed TPanel *MumPanel,*PrePanel; TTimer *Timer; TStaticText *TxtScore,*TxtLine1,*TxtLine2, *TxtLine3,*TxtLine4,*TxtLevel; int Left,Right,Up,Down,SpeedUp; //控制键 MainFrame FMain; TetrisGame() {} void SetPandT(TPanel *P,TPanel *P2,TTimer *T,TStaticText *Score, TStaticText *Line1,TStaticText *Line2, TStaticText *Line3,TStaticText *Line4,TStaticText *Level); void Init(); //游戏开始 void Play(); //游戏暂停 void Pause(); //游戏结束 void Over(); private: }; //--------------------------------------------------------------------------- TetrisGame 类中定义了用于输出得分、级别、消去行数等的 StaticText 组件;定义一个游 戏界面(MainFrame 类)FMain,以及 FMain 对应的 Panel 组件;定义一个定时器组件,它用 来控制该 TetrisGame 的运行;最后,还有该游戏(TetrisGame)的控制按键,如果要更改游 戏的控制按键,只需要更改 TetrisGame 的 Left、Right、Up、Down 属性即可。 游戏的控制方法有游戏的初始化方法 Init,游戏开始 Play,游戏暂停 Pause 和游戏结束。 第 12 章 俄罗斯方块 365 12.3 各类的具体实现 本程序中定义了四个类,Square、Blocks、MainFrame 和 TetrisGame,其中 Square 和 Blocks 描述方格、方块的性质,MainFrame 描述游戏界面以及游戏中的各种事件(方块移动,消行 等),TetrisGame 类用于对 MainFrame 进行控制,并且记录 MainFrame 的游戏得分等情况并 显示在窗体中。Square 类没有定义任何的方法,下面分别分析其它三个类的具体实现。 12.3.1 Blocks 类的实现 Blocks 类的构造函数在 12.2.3 已经给出,它的另外两个方法为 SetBlock 和 GetNewShape。 SetBlock 用于对设置一个方块,即从 sAll 数组中读取实心方格的位置坐标,从而设置 Blocks 的 s 数组;GetNewShape 用于获得方块得下一个变形形状,方块得变形是按照逆时针的方向 旋状,但程序中并不管旋转操作,只是在 sAll 数组中读取下一个方块得数据即可,只需要在 下一个方块数据已经越到其它形状的方块时转到当前方块形状的第一个变形即可。 代码如下: //------------------------------------------- void Blocks::SetBlock(int choise,int Clr)//choise=0-27 { int pos[8]; int i; Shape=choise; //Color=Clr; 形状一样,方块颜色一样,所以不用这个 Color=SquareColor[choise/4]; //根据 choise 决定方块的颜色 for(i=0;i<4;i++) { for(int j=0;j<4;j++) { s[i][j].IsActive=false; s[i][j].Color=clBlack; //底色 } }//清空变形前的方块数据 for(i=0;i<8;i++) pos[i]=sAll[Shape*8+i]; DownPos=0; UpPos=3; LeftPos=3; RightPos=0; for(i=0;i<4;i++) { s[pos[2*i]][pos[2*i+1]].IsActive = true; 第 12 章 俄罗斯方块 366 s[pos[2*i]][pos[2*i+1]].Color=Clr; if(pos[2*i] > DownPos) DownPos=pos[2*i]; if(pos[2*i] < UpPos) UpPos=pos[2*i]; if(pos[2*i+1] > RightPos) RightPos=pos[2*i+1]; if(pos[2*i+1] < LeftPos) LeftPos=pos[2*i+1]; } } //------------------------------------------------- int Blocks::GetNewShape() //返回下一个变形方块的 Shape 值 { int temp=Shape; if(temp/4 == (temp+1)/4) //在同一个方块类型中 temp+=1; else //Shape+1跳进另外一个方块类型 { temp-=3; } return temp; } //-------------------------------------------------- 12.3.2 MainFrame 类的实现 MainFrame 类主要完成游戏区域的绘制,游戏中方块的移动、变形以及相应的消行等操 作,另外还要绘制游戏区域下部方块位置标志,以及左右两侧的对家方块累积高度标志和预 览区方块等。它们的实现如下: //----------MainFrame 类----------------- //构造函数,为游戏中需要使用的各个 TShape 组件声明实例,分配内存 MainFrame::MainFrame() { int i,j; //下面初始化 MainShape 数组 for(i=0;iPanel1; //下面初始化 MainShape 数组 第 12 章 俄罗斯方块 368 for(i=0;iParent=MumPanel; MainShape[i][j]->Pen->Style=psClear; MainShape[i][j]->Pen->Width=0; //TShape 的大小,还有相对与 Panel 边界的距离都是尝试后得到的,主游戏区 Panel1 和 Panel2 //的大小为 565×265,预览区的 Panel3 和 Panel4 大小为 90×90 MainShape[i][j]->Width=25; MainShape[i][j]->Height=25; MainShape[i][j]->Brush->Color=clBlack; MainShape[i][j]->Left=6+j*25; MainShape[i][j]->Top=6+i*25; } } for(i=0;i<4;i++) { for(j=0;j<4;j++) { PreShape[i][j]->Parent=PrePanel; PreShape[i][j]->Pen->Style=psClear; PreShape[i][j]->Pen->Width=0; PreShape[i][j]->Width=20; PreShape[i][j]->Height=20; PreShape[i][j]->Brush->Color=clBlack; PreShape[i][j]->Left=5+j*20; PreShape[i][j]->Top=5+i*20; } } for(i=0;iParent=MumPanel; DownShape[i]->Width=25; DownShape[i]->Height=5; DownShape[i]->Brush->Color=clBlack; DownShape[i]->Left=6+i*25; DownShape[i]->Top=MumPanel->ClientHeight-8; DownShape[i]->Pen->Style=psClear; DownShape[i]->Pen->Width=0; } 第 12 章 俄罗斯方块 369 //for(i=0;i<2;i++) //两边对家高度标志 //{ for(j=0;jParent=MumPanel; LRShape[0][j]->Width=5; LRShape[0][j]->Height=25; LRShape[0][j]->Brush->Color=clBlack; LRShape[0][j]->Top=6+j*25; LRShape[0][j]->Left=3; LRShape[0][j]->Pen->Style=psClear; LRShape[0][j]->Pen->Width=0; LRShape[1][j]->Parent=MumPanel; LRShape[1][j]->Width=5; LRShape[1][j]->Height=25; LRShape[1][j]->Brush->Color=clBlack; LRShape[1][j]->Top=6+j*25; LRShape[1][j]->Left=MumPanel->ClientWidth-8; LRShape[1][j]->Pen->Style=psClear; LRShape[1][j]->Pen->Width=0; } //} for(i=0;iBrush->Color=TColor(MainSquare[i][j].Color); else MainShape[i][j]->Brush->Color=clBlack; } } }//end of ReDraw //--------------------------- //用来处理方块下降的函数 //如果可以下移并且移动成功,返回 true, //否则,如果不能下移,返回 false bool MainFrame::BlockDown() { int i,j; //判断是否可以下移 //如果越出下界,返回失败信息 if(BlockPosX+MainBlocks.DownPos-2 /*当前方块实心方格的最下一行位置的下一行*/ > BlockH-1)//即如果方块再下降一个就会有实心方格跑到游戏界面之下了(越界) return false; for(i=MainBlocks.UpPos; i<=MainBlocks.DownPos; i++) {//对下落方块的每个实心方格都判断,看它下移一行的位置是否是已固化的方格 //如果是,侧不能下移 if(BlockPosX+i-2>=0) { for(j=MainBlocks.LeftPos; j<=MainBlocks.RightPos; j++) { if(MainBlocks.s[i][j].IsActive) //方块这个位置上实心 if(MainSquare[BlockPosX+i-2][BlockPosY+j].IsBlock) return false; } 第 12 章 俄罗斯方块 371 } } //抹掉现在方块的图形 for(i=MainBlocks.UpPos; i<=MainBlocks.DownPos; i++) { if(BlockPosX+i-3 >= 0) { for(j=MainBlocks.LeftPos; j<=MainBlocks.RightPos; j++) { if(MainBlocks.s[i][j].IsActive) //方块这个位置上实心 { MainShape[BlockPosX+i-3][BlockPosY+j]->Brush->Color=clBlack; MainSquare[BlockPosX+i-3][BlockPosY+j].IsActive=false; MainSquare[BlockPosX+i-3][BlockPosY+j].Color=clBlack; } } } } //画方块在新位置的图形 for(i=MainBlocks.UpPos; i<=MainBlocks.DownPos; i++) { if(BlockPosX+i-2 >=0 ) { for(j=MainBlocks.LeftPos; j<=MainBlocks.RightPos; j++) { if(MainBlocks.s[i][j].IsActive) //方块这个位置上实心 { MainShape[BlockPosX+i-2][BlockPosY+j]->Brush->Color= TColor(MainBlocks.Color); MainSquare[BlockPosX+i-2][BlockPosY+j].IsActive=true; MainSquare[BlockPosX+i-2][BlockPosY+j].Color=MainBlocks.Color; } } } } //修改方块的位置 BlockPosX+=1; return true; } //--------------------------- 第 12 章 俄罗斯方块 372 //用来处理左移的函数,如果可以左移并且移动成功,返回 true //否则,如果不能左移,返回 false bool MainFrame::BlockLeft() { int i,j; //根据方块最左侧一列左移之后是否越界判断方块左移后是否越左界 if(BlockPosY+MainBlocks.LeftPos<=0) return false; //如果方块还没有下来(方块是从游戏区之外的上方开始下落的),设置不能左右移动 if(BlockPosX + MainBlocks.DownPos - 3 < 0) return false; //判断没越界情况下是否可以左移 for(i=MainBlocks.UpPos;i<=MainBlocks.DownPos;i++) {//判断方块中每个实心方格左移一列之后的位置是否是实心的已固化方格 //如果是,则不能左移 if(BlockPosX + i -3 >=0 ) { for(j=MainBlocks.LeftPos;j<=MainBlocks.RightPos;j++) { if(MainBlocks.s[i][j].IsActive) if(MainSquare[BlockPosX+i-3][BlockPosY+j-1].IsBlock) return false; } } } //可以移动,抹掉原来的方块图象 for(i=MainBlocks.UpPos;i<=MainBlocks.DownPos;i++) { if(BlockPosX+i-3>=0) { for(j=MainBlocks.LeftPos;j<=MainBlocks.RightPos;j++) { if(MainBlocks.s[i][j].IsActive) { MainShape[BlockPosX+i-3][BlockPosY+j]->Brush->Color=clBlack; MainSquare[BlockPosX+i-3][BlockPosY+j].IsActive=false; MainSquare[BlockPosX+i-3][BlockPosY+j].Color=clBlack; } } } 第 12 章 俄罗斯方块 373 } //绘制新位置的方块图象 for(i=MainBlocks.UpPos;i<=MainBlocks.DownPos;i++) { if(BlockPosX+i-3>=0) { for(j=MainBlocks.LeftPos;j<=MainBlocks.RightPos;j++) { if(MainBlocks.s[i][j].IsActive) { MainShape[BlockPosX+i-3][BlockPosY+j-1]->Brush->Color= TColor(MainBlocks.Color); MainSquare[BlockPosX+i-3][BlockPosY+j-1].IsActive=true; MainSquare[BlockPosX+i-3][BlockPosY+j-1].Color=MainBlocks.Color; } } } } //方块位置左移 BlockPosY-=1; DrawDown(); //重画底部位置标志 return true; } //--------------------------- //用来处理右移的函数,如果能够右移并且右移成功,返回 true //否则,如果不能右移,返回 false bool MainFrame::BlockRight() { int i,j; //根据方块最左侧一列右移植否是否越界判断方块右移后是否越右界 if(BlockPosY+MainBlocks.RightPos >= (BlockW-1)) return false; //如果方块还没有下来,设置不能左右移动 if(BlockPosX + MainBlocks.DownPos - 3 < 0) return false; //判断没越界情况下是否可以右移 for(i=MainBlocks.UpPos;i<=MainBlocks.DownPos;i++) {//根据方块每个实心方格右移后的位置是否是已固定的实心方块判断是否可以右移 if(BlockPosX + i -3 >=0 ) { 第 12 章 俄罗斯方块 374 for(j=MainBlocks.LeftPos;j<=MainBlocks.RightPos;j++) { if(MainBlocks.s[i][j].IsActive) if(MainSquare[BlockPosX+i-3][BlockPosY+j+1].IsBlock) return false; } } } //可以移动,抹掉原来的方块图象 for(i=MainBlocks.UpPos;i<=MainBlocks.DownPos;i++) { if(BlockPosX+i-3>=0) { for(j=MainBlocks.LeftPos;j<=MainBlocks.RightPos;j++) { if(MainBlocks.s[i][j].IsActive) { MainShape[BlockPosX+i-3][BlockPosY+j]->Brush->Color=clBlack; MainSquare[BlockPosX+i-3][BlockPosY+j].IsActive=false; MainSquare[BlockPosX+i-3][BlockPosY+j].Color=clBlack; } } } } //绘制新位置的方块图象 for(i=MainBlocks.UpPos;i<=MainBlocks.DownPos;i++) { if(BlockPosX+i-3>=0) { for(j=MainBlocks.LeftPos;j<=MainBlocks.RightPos;j++) { if(MainBlocks.s[i][j].IsActive) { MainShape[BlockPosX+i-3][BlockPosY+j+1]->Brush->Color= TColor(MainBlocks.Color); MainSquare[BlockPosX+i-3][BlockPosY+j+1].IsActive=true; MainSquare[BlockPosX+i-3][BlockPosY+j+1].Color=MainBlocks.Color; } } } 第 12 章 俄罗斯方块 375 } //方块位置左移 BlockPosY+=1; DrawDown(); //重画底部位置标志 return true; } //--------------------------- //方块变形处理函数,如果可以变形,并且变形成功,返回 true //否则,如果不能变形,返回 false bool MainFrame::BlockChangeShape() { int i,j; //先把 MainBlocks 的下一个变形的方块数据拷贝 //到中间变量 Temp 中。Temp 在类 MainFrame 中声明 Temp.SetBlock(MainBlocks.GetNewShape(),MainBlocks.Color); //判断方块是否能够变形 //首先判断变形后是否越界 if(BlockPosX+Temp.DownPos-3 > (BlockH-1) || BlockPosY+Temp.LeftPos < 0 || BlockPosY+Temp.RightPos > (BlockW-1)) return false; //不越界情况下是否可以变形 else {//根据变形后的方块的实心方格所对应的位置是否是已经固定的方格判断是否可变形 for(i=Temp.UpPos;i<=Temp.DownPos;i++) { if(BlockPosX + i -3 >=0 ) { for(j=Temp.LeftPos;j<=Temp.RightPos;j++) { if(Temp.s[i][j].IsActive) if(MainSquare[BlockPosX+i-3][BlockPosY+j].IsBlock) return false; } } } } //如果可以变形 //抹掉原来的方块图形 for(i=MainBlocks.UpPos;i<=MainBlocks.DownPos;i++) 第 12 章 俄罗斯方块 376 { if(BlockPosX+i-3>=0) { for(j=MainBlocks.LeftPos;j<=MainBlocks.RightPos;j++) { if(MainBlocks.s[i][j].IsActive) { MainShape[BlockPosX+i-3][BlockPosY+j]->Brush->Color=clBlack; MainSquare[BlockPosX+i-3][BlockPosY+j].IsActive=false; MainSquare[BlockPosX+i-3][BlockPosY+j].Color=clBlack; } } } } //变形方块,把变形后的方块数据写进 MainBlocks MainBlocks.SetBlock(MainBlocks.GetNewShape(),MainBlocks.Color); //绘制变形后的方块的图形 for(i=MainBlocks.UpPos;i<=MainBlocks.DownPos;i++) { if(BlockPosX+i-3>=0) { for(j=MainBlocks.LeftPos;j<=MainBlocks.RightPos;j++) { if(MainBlocks.s[i][j].IsActive) { MainShape[BlockPosX+i-3][BlockPosY+j]->Brush->Color= TColor(MainBlocks.Color); MainSquare[BlockPosX+i-3][BlockPosY+j].IsActive=true; MainSquare[BlockPosX+i-3][BlockPosY+j].Color=MainBlocks.Color; } } } } DrawDown(); //重画底部位置标志 return true; //修改方块后,MainBlocks 4*4 矩阵在 MianFrame 中的位置不变 //所以方块的位置参数不用改动 } //--------------------------- //当方块不能下移时,用来固化(IsAvtive->IsBlock)方块,消行, 第 12 章 俄罗斯方块 377 //以及生成新方块的函数 //如果方块固化后 MainFrame 没有填满,即游戏没有结束,返回消掉的行数 //如果游戏结束,返回-1 int MainFrame::BlockEnd() { randomize(); int RowKill=0; //固化,将下落方块固定 for(int i=MainBlocks.UpPos;i<=MainBlocks.DownPos;i++) { //if(BlockPosX+i-3>=0) 由循环条件,这个关系一定满足 //{ for(int j=MainBlocks.LeftPos;j<=MainBlocks.RightPos;j++) { if(MainBlocks.s[i][j].IsActive) { if(BlockPosX+i-3>=0) {//只改变 IsActive 和 IsBlock 属性,颜色属性不改 MainSquare[BlockPosX+i-3][BlockPosY+j].IsActive=false; MainSquare[BlockPosX+i-3][BlockPosY+j].IsBlock=true; } //累积方格的最高层位置需要判断是否要改变 if(BlockPosX+i-3 < MainTop)MainTop=BlockPosX+i-3; } } //} } //可能需要消的行位置在 BlockPosX+MainBlocks.UpPos-3 //到 BlockPosX+MainBlocks.DownPos-3 之间 for(int i=BlockPosX+MainBlocks.DownPos-3; i>=BlockPosX+MainBlocks.UpPos-3; i--) { bool Kill=true; if(i<0)break; for(int j=0;jMainTop;k--) { for(int j=0;jBrush->Color=clBlack; // else // MainShape[k][j]->Brush->Color=clGreen; // MainSquare[k][j].IsBlock=!MainSquare[k][j].IsBlock; //} MainSquare[k][j].IsBlock=MainSquare[k-1][j].IsBlock; MainSquare[k][j].Color=MainSquare[k-1][j].Color; MainShape[k][j]->Brush->Color=TColor(MainSquare[k][j].Color); } } for(int j=0;jBrush->Color=clBlack; MainSquare[MainTop][j].IsBlock=false; MainSquare[MainTop][j].Color=clBlack; } } MainTop++;//最高位置减一 i++; BlockPosX++; } } //消行之后判断方块是否溢出(上溢),以此判断游戏是否结束 if(MainTop<0) return -1; 第 12 章 俄罗斯方块 379 //检查一下是否有对家送过来的“礼物”Gift if(MainTop-Gift < 0) {//下部添加 Gift 行之后要溢出,即游戏玩家失败 return -1; } //可以上移,则在最底层添加 Gift 行的方格,每行都随即有一个空心方格 if(Gift) //gift != 0 { int k,l; randomize(); for(k=MainTop-Gift;kBrush->Color=clBlack; } else //颜色由 NextBlockShape 决定 PreShape[i][j]->Brush->Color=TColor(SquareColor[NextBlocksShape/4]); } } } //--------------------------- void MainFrame::DrawDown(void) //画底部方块位置标志 {//根据下落方块中实心方块的左右边界的位置绘制 int i; 第 12 章 俄罗斯方块 381 for(i=0;iBrush->Color=clBlack; } for(i=MainBlocks.LeftPos;i<=MainBlocks.RightPos;i++) { DownShape[BlockPosY+i]->Brush->Color=clRed; } } //--------------------------- void MainFrame::DrawLR(int top) //画左右对家高度标志 {//top 参数为对家的最高行的位置,即其 MainTop 属性 int i; if(top<0)top=0; for(i=0;iBrush->Color=clBlack; LRShape[1][i]->Brush->Color=clBlack; } for(i=top;iBrush->Color=clRed; LRShape[1][i]->Brush->Color=clRed; } } //--------------------------------------------------------------------------- 12.3.3 TetrisGame 类的实现 TetrisGame 是游戏控制类,主要用于游戏计分、速度控制、游戏状态控制等,具体实现 如下: //-------------TetrisGame 类--------------------- //将窗体上的输出组件设置到 TetrisGame 中 void TetrisGame::SetPandT(TPanel *P,TPanel *P2,TTimer *T,TStaticText *Score, TStaticText *Line1,TStaticText *Line2, TStaticText *Line3,TStaticText *Line4,TStaticText *Level) { 第 12 章 俄罗斯方块 382 MumPanel=P; PrePanel=P2; FMain.SetPanel(MumPanel,PrePanel); Timer=T; //定时器 Timer->Enabled=false; TxtScore=Score; TxtLine1=Line1; TxtLine2=Line2; TxtLine3=Line3; TxtLine4=Line4; TxtLevel=Level; } //--------------------------------------------- //初始化 void TetrisGame::Init() { //初始化变量 Status=3; //游戏还未开始 FMain.Init();//初始化 FMain 里面的参数 Score=0; Rate=600;//600ms HighRate=false; Level=1; Line1=0; Line2=0; Line3=0; Line4=0; Timer->Interval=Rate; Timer->Enabled=false; //画游戏界面 TxtScore->Caption=0;TxtLine1->Caption=0;TxtLine2->Caption=0; TxtLine3->Caption=0;TxtLine4->Caption=0;TxtLevel->Caption=0; FMain.DrawPreView(1); //清除预览区 } //--------------------------------------------- //游戏开始 void TetrisGame::Play() { if(Status==2)//如果是暂停状态 { Timer->Enabled=true; Status=1; } else if(Status==3)//如果是未开始状态 { FMain.DrawPreView(0);//绘制预览方块 Timer->Enabled=true; FMain.DrawDown(); //重画底部位置标志 第 12 章 俄罗斯方块 383 Status=1; } } //--------------------------------------------- //游戏暂停 void TetrisGame::Pause() { Timer->Enabled=false;//定时器停止 Status=2; //状态参数置为暂停 } //--------------------------------------------- //游戏结束 void TetrisGame::Over() { Status=3; Timer->Enabled=false; //Init(); //提示消息确认之后再初始化得分,级别的显示 } //--------------------------------------------- 12.4 键盘、定时器和菜单的控制 在程序中,我们定义两个游戏,即两个 TetrisGame 的实例,然后根据用户选择的是单玩 家模式还是对战模式来调整窗口的显示,完成对游戏的控制。 在头文件中定义如下: public: // User declarations TetrisGame Game1,Game2; int Mode; //游戏模式,1 表示 single,2 表示对战 键盘、菜单和定时器等对游戏的控制,也就是根据 Mode 对 Game1 和 Game2 的控制。 12.4.1 键盘的控制 游戏中,方块的左右移动、变形、快下和加速操作都要通过键盘来控制,相应操作的控 制键在 TetrisGame 中定义。我们需要截取键盘事件,对于左右移动和变形,都可以通过响应 相应控制键的 KeyDown 事件来实现,而对于加速控制,则应该在 KeyDown 事件之后加速, 在 KeyUp 事件时恢复速度,所以我们需要拦截键盘按下和键盘弹起两个事件,代码如下: private: // User declarations 第 12 章 俄罗斯方块 384 void __fastcall MyKeyDown(TMessage &Message); void __fastcall MyKeyUp(TMessage &Message); BEGIN_MESSAGE_MAP MESSAGE_HANDLER(WM_KEYDOWN,TMessage,MyKeyDown);//拦截 keydown 事 件 MESSAGE_HANDLER(WM_KEYUP,TMessage,MyKeyUp); //拦截 KeyUp 事件 END_MESSAGE_MAP(TForm); 其中 MyKeyDown 和 MyKeyUp 函数的代码如下: //------------------------------------- void __fastcall TForm1::MyKeyDown(TMessage &Message) { if(Game2.Status==1) {//对 Game2 的控制 if(Message.WParamLo==Game2.Left) { Game2.FMain.BlockLeft(); //左移 } else if(Message.WParamLo==Game2.Right) { Game2.FMain.BlockRight();//右移 } else if(Message.WParamLo==Game2.Up) { Game2.FMain.BlockChangeShape(); //变形 } else if(Message.WParamLo==Game2.Down) {//直下 Game2.Timer->Enabled=false; while(Game2.FMain.BlockDown()); Form1->Timer2Timer(this); //因为方块已经落到底部,所以需要立即执行定时器函数 Game2.Timer->Enabled=true; } else if(!Game2.HighRate && Message.WParamLo==Game2.SpeedUp) {//加速 //按这不放的时候不处理,直接跳出 Game2.HightRate==true Game2.Timer->Interval=80; //设置快下速度为 100ms Game2.HighRate=true; } } if(Game1.Status==1) {//Game1 的控制 第 12 章 俄罗斯方块 385 if(Message.WParamLo==Game1.Left) { Game1.FMain.BlockLeft(); } else if(Message.WParamLo==Game1.Right) { Game1.FMain.BlockRight(); } else if(Message.WParamLo==Game1.Up) { Game1.FMain.BlockChangeShape(); } else if(Message.WParamLo==Game1.Down) { //直下 if(Game1.Status==1)//游戏进行中 Game1.Timer->Enabled=false; while(Game1.FMain.BlockDown()); Form1->Timer1Timer(this);//因为方块已经落到底部所以需要立即执行定时器函数 Game1.Timer->Enabled=true; } else if(!Game1.HighRate && Message.WParamLo==Game1.SpeedUp) {//加速 //按这不放的时候不处理,直接跳出 Game1.Timer->Interval=80; //设置快下速度为 100ms Game1.HighRate=true; } } //将消息传递给系统处理 TForm::Dispatch(&Message); } //--------------------------------------------------------------------------- void __fastcall TForm1::MyKeyUp(TMessage &Message) { if(Game2.Status==1) { //加速 if(Message.WParamLo==Game2.SpeedUp && Game2.HighRate)//游戏加速中 { //GameWin.StopTimer(); Game2.Timer->Interval=Game2.Rate; 第 12 章 俄罗斯方块 386 Game2.HighRate=false; } } if(Game1.Status==1) { //加速 if(Message.WParamLo==Game1.SpeedUp && Game1.HighRate)//游戏加速中 { //GameWin.StopTimer(); Game1.Timer->Interval=Game1.Rate; Game1.HighRate=false; } } TForm::Dispatch(&Message); } //--------------------------------- 12.4.2 定时器的控制 每次定时器事件都要让游戏中的方块向下移动一行,如果移动成功,则返回,如果移动 失败,则判断是不是游戏结束了,如果游戏没有结束,则根据方块固定时消去的行数对游戏 分数、Removed Line 的显示、游戏级别以及送给对家游戏者的 Gift 等进行设置,如果游戏结 束,则根据游戏模式和游戏结束状态弹出提示信息,确认提示信息之后再对游戏进行初始化。 Timer1 对 Game1 进行控制,Timer2 对 Game2 进行控制,而且,它们的控制过程类似, 代码也相似,如下: //---------------------------------------------------------------- void __fastcall TForm1::Timer1Timer(TObject *Sender) { int Rows; if(! Game1.FMain.BlockDown()) { //下面操作费时较长,所以,先关闭定时器 Timer1->Enabled=false; Rows=Game1.FMain.BlockEnd();//所消行数 Game2.FMain.DrawLR(Game1.FMain.MainTop); //在对家中画自己高度 if(Rows==-1)//游戏结束 { if(Mode==1) { 第 12 章 俄罗斯方块 387 Game1.Over(); Application->MessageBoxA("You are failed!","Game Over",0); Game1.Init(); return; } else if(Mode==2) { Game1.Over(); Game2.Over(); Application->MessageBoxA("Player 2 win!","Game Over",0); Game1.Init(); Game2.Init(); return; } } else { if(Rows)//Rows>0 { switch(Rows) { case 1: { Game1.Score=Game1.Score+Rows*100; Game1.Line1++; Game1.TxtLine1->Caption=Game1.Line1; } break; case 2: { Game1.Score=Game1.Score+Rows*150; Game1.Line2++; Game1.TxtLine2->Caption=Game1.Line2; if(Mode==2)Game2.FMain.Gift+=1; //给 Game2 传送一行 } break; case 3: { Game1.Score=Game1.Score+Rows*200; Game1.Line3++; Game1.TxtLine3->Caption=Game1.Line3; if(Mode==2)Game2.FMain.Gift+=2; //给 Game2 传送二行 } break; case 4: { Game1.Score=Game1.Score+Rows*300; Game1.Line4++; Game1.TxtLine4->Caption=Game1.Line4; if(Mode==2)Game2.FMain.Gift+=3; //给 Game2 传送三行 } break; 第 12 章 俄罗斯方块 388 } //新的得分 Game1.TxtScore->Caption=Game1.Score; //判断游戏级别是否改变,若改变,则重新输出级别, //并根据升级的信息更新 Game.Rate if(Game1.Level != Game1.Score/2000 +1) { Game1.Level=Game1.Score/2000 + 1;//新的游戏级别 Game1.TxtLevel->Caption=Game1.Level; } if(Game1.Level >= 11) { Game1.Over(); //游戏成功过关而结束游戏 if(Mode==2) {//如果是对战,则结束对家游戏,显示 Game1 获胜 Game2.Over(); Application->MessageBoxA("Player 1 win!","Game Over",0); }//如果是单人,则不只显示获胜提示 else Application->MessageBoxA("Victory!","Game Over",0); //提示获胜后,将游戏初始化 Game1.Init(); if(Mode==2)Game2.Init(); return; } else { //改变方块下落速度 Game1.Rate=760-Game1.Level * 70;//ms 600ms--150ms Game1.Timer->Interval=Game1.Rate; //重新输出游戏级别 } } } Timer1->Enabled=true;//打开定时器 } } //------------------------------------- void __fastcall TForm1::Timer2Timer(TObject *Sender) { int Rows; 第 12 章 俄罗斯方块 389 if(! Game2.FMain.BlockDown()) { //下面操作费时较长,所以,先关闭定时器 Timer2->Enabled=false; Rows=Game2.FMain.BlockEnd();//所消行数 Game1.FMain.DrawLR(Game2.FMain.MainTop); //在对家中画自己高度 if(Rows==-1)//游戏结束 { Game1.Over(); Game2.Over(); Application->MessageBoxA("Player 1 win!","Game Over",0); Game1.Init(); Game2.Init(); return; } else { if(Rows)//Rows>0 { switch(Rows) { case 1: { Game2.Score=Game2.Score+Rows*100; Game2.Line1++; Game2.TxtLine1->Caption=Game2.Line1; } break; case 2: { Game2.Score=Game2.Score+Rows*150; Game2.Line2++; Game2.TxtLine2->Caption=Game2.Line2; Game1.FMain.Gift++; //给 Game1 传送一行 } break; case 3: { Game2.Score=Game2.Score+Rows*200; Game2.Line3++; Game2.TxtLine3->Caption=Game2.Line3; Game1.FMain.Gift+=2; //给 Game1 传送二行 } break; case 4: { Game2.Score=Game2.Score+Rows*300; Game2.Line4++; Game2.TxtLine4->Caption=Game2.Line4; Game1.FMain.Gift+=3; //给 Game1 传送三行 } break; 第 12 章 俄罗斯方块 390 } //新的得分 Game2.TxtScore->Caption=Game2.Score; //判断游戏级别是否改变,若改变,则重新输出级别, //并根据升级的信息更新 Game.Rate if(Game2.Level != Game2.Score/2000 +1) { Game2.Level=Game2.Score/2000 + 1;//新的游戏级别 Game2.TxtLevel->Caption=Game2.Level; } if(Game2.Level >= 11) { Game2.Over(); //游戏成功过关而结束游戏 Game1.Over(); Application->MessageBoxA("Player 2 win!","Game Over",0); Game2.Init(); Game1.Init(); return; } else { //改变方块下落速度 Game2.Rate=760-Game2.Level * 70;//ms 600ms--150ms Game1.Timer->Interval=Game1.Rate; //重新输出游戏级别 } } } Timer2->Enabled=true; //打开定时器 } } //--------------------------------------------------------------------------- 12.4.3 菜单的控制 主菜单中“游戏”菜单项,有“新建”、“暂停”和“离开”三个子菜单,它们用于游戏 的新建,暂停和程序的退出,代码如下: //--------------------------------------------------------------------------- //新建游戏 第 12 章 俄罗斯方块 391 void __fastcall TForm1::NewClick(TObject *Sender) { if(Mode==1) //Single 模式 { Game1.Over(); //结束 Game1.Init(); //初始新游戏 Game1.Play(); //新游戏开始 } else if(Mode==2) //VS 模式 { Game1.Over(); Game1.Init(); Game2.Over(); Game2.Init(); Game1.Play(); Game2.Play(); } Form1->Pause->Enabled=true; } //--------------------------------------------------------------------------- //暂停游戏 void __fastcall TForm1::PauseClick(TObject *Sender) { if(Game1.Status==1) { Game1.Pause(); Form1->Pause->Caption="继续&C"; } else if(Game1.Status==2) //暂停中 { Game1.Play(); Form1->Pause->Caption="暂停&P"; } if(Mode==2) //VS { if(Game2.Status==1) Game2.Pause(); else if(Game2.Status==2) Game2.Play(); } 第 12 章 俄罗斯方块 392 } //--------------------------------------------------------------------------- //程序退出 void __fastcall TForm1::QuitClick(TObject *Sender) { Form1->Close(); } //--------------------------------------------------------------------------- “Multiplayers”菜单有“Single Player”和“Human VS Human”两个子菜单,用于选择 游戏模式为单人游戏或者双人对战,代码如下: //--------------------------------------------------------------------------- void __fastcall TForm1::SingleClick(TObject *Sender) { if(Mode==2) { Game1.Left=VK_LEFT; Game1.Right=VK_RIGHT; Game1.Up=VK_UP; Game1.Down=VK_DOWN; Game1.SpeedUp=17; //Ctrl 键 Game2.Left='A'; Game2.Right='D'; Game2.Up='W'; Game2.Down='S'; Game2.SpeedUp='J'; //Form1宽度减半 TxtUp1->Caption="↑"; TxtLeft1->Caption="←"; TxtRight1->Caption="→"; TxtDown1->Caption="↓"; TxtSpeed1->Caption="Ctrl"; Form1->Width=490; //980 Game2.Over(); Game2.Init(); Mode=1; } Game1.Over(); Game1.Init(); Form1->Pause->Enabled=false; } //--------------------------------------------------------------------------- void __fastcall TForm1::VSClick(TObject *Sender) { if(Mode==1) { Game2.Left=VK_LEFT; Game2.Right=VK_RIGHT; Game2.Up=VK_UP; Game2.Down=VK_DOWN; Game2.SpeedUp=17; //Ctrl 键 第 12 章 俄罗斯方块 393 Game1.Left='A'; Game1.Right='D'; Game1.Up='W'; Game1.Down='S'; Game1.SpeedUp='J'; //Form1宽度重设 Form1->Width=980; Mode=2; TxtUp1->Caption="W"; TxtLeft1->Caption="A"; TxtRight1->Caption="D"; TxtDown1->Caption="S"; TxtSpeed1->Caption="J"; } Game1.Over(); Game2.Over(); Game1.Init(); Game2.Init(); Form1->Pause->Enabled=false; } //--------------------------------------------------------------------------- 12.4.4 其它 在程序开始时,需要进行一些初始化工作,将窗体上的输出组件对应赋给 Game1、Game2 的相应属性,设置 Game1 和 Game2 的默认控制键,以及对游戏进行初始化等,代码如下: //--------------------------------------------------------------------------- __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { //WindowProc=MyKeyDown; Mode=1; //默认 single 模式 //设置默认的控制键 Game1.Left=VK_LEFT; Game1.Right=VK_RIGHT; Game1.Up=VK_UP; Game1.Down=VK_DOWN; Game1.SpeedUp=17; //Ctrl 键 Game2.Left='A'; Game2.Right='D'; Game2.Up='W'; Game2.Down='S'; Game2.SpeedUp='J'; Form1->Width=490; Game1.SetPandT(Form1->Panel1,Form1->Panel3,Form1->Timer1,Form1->TxtScore1, Form1->TxtLine11,Form1->TxtLine12,Form1->TxtLine13,Form1->TxtLine14, Form1->TxtLevel1); Game1.Init(); Game2.SetPandT(Form1->Panel2,Form1->Panel4,Form1->Timer2,Form1->TxtScore2, Form1->TxtLine21,Form1->TxtLine22,Form1->TxtLine23,Form1->TxtLine24, Form1->TxtLevel2); 第 12 章 俄罗斯方块 394 Game2.Init(); } //--------------------------------------------------------------------------- 12.5 帮助文件的制作 帮助文件是一个应用程序不可缺少的部分,它为程序使用者提供使用说明。帮助文件的 类型有 hlp 文件和 chm 文件两种,我们这里使用 hlp 文件。hlp 文件是由 RTF 文件经过编译 得到的,帮助文件中的各个主题及其内容都保存在 RTF 文件中,而工程管理信息保存在 hpj 文件中,经过编译器编译后,将生成 hlp 类型的帮助文件和 cnt 类型的帮助目录文件。 制作帮助的过程如下: (1)帮助系统的整体设计。在进行帮助内容的整体设计和显示时,一般利用树形目录实现对 总体内容的统一把握。 (2)编写 RTF 文件。根据帮助系统的结构编写各个部分的文档,按照一定的格式编写为 RTF 文件。 (3)利用帮助文件制作工具创建并编辑 HPJ 工程文件,设计目录树、自定义窗口等。 (4)编译 HPJ 工程文件,生成 HLP 帮助文件。 (5)在应用程序中调用制作的帮助文件。 HPJ 文件的创建和编辑需要使用帮助文件制作工具,C++Builder6 自带的 MicroSoft Help Workshop 在 C++Builder 安装目录中的\Help\Tools 中。我们就使用它来制作帮助文件。 下面以本章的俄罗斯方块游戏的帮助文件为例,讲述帮助文件的制作过程。 12.5.1 RTF 文件 RTF 文件可以使用 Microsoft Word 来编辑,在创建帮助文件时需要用到一些 RTF 的文本 格式,下面分别对各种格式在帮助文件中的使用做介绍。 z 分页 帮助文件中不同的相互独立的主题页都对应 RTF 文件中一个页面,也就是,编辑好一个 主题页的内容之后需要在其后插入分页符,与其它主题页的内容分开。分页符的插入可以在 菜单“插入”->“分割符”弹出的窗口中选择分页符,也可以使用快捷键 Ctrl+Enter。 z 脚注 RTF 中的脚注文本在帮助文件中具有特殊的意义,可以使用的标记有“#”、“K”和“$”。 “#”标记的内容表示当前主题页的标志符,也即帮助上下文的 ID。一个主题页中有且 只有一个标志符,帮助文件在各个主题页中跳转时就是靠标志符来确定主题页的。其命名规 则与文件的命名规则类似。 “K”标记的脚注内容是当前主题页的关键字。一个主题页可以没有关键字,也可以有 多个关键字。在帮助文件的索引模式下,是依靠关键字来查找相应的主题页面。 “$”标记的脚注内容为当前主题页的标题,它是对当前主题页内容的简单描述。 第 12 章 俄罗斯方块 395 z 文字格式 对文字格式的一些特殊设置,编译为帮助文件时会做特殊的处理。几种常用的格式有双 下划线格式、单下划线格式、删除线格式和隐藏格式。 双下划线格式:双下划线格式的文字在帮助文件中将以单下划线的形式显示,而且显示 颜色会显示为绿色。点击此文字可以象超链接一样跳转到其它主题也。在编辑 RTF 文本时, 在双下划线的后面要紧跟着(中间不能有空格)写上要跳转到的页面的标志符,这些标志符 要用隐藏格式。 单下划线格式:单下划线格式的文字在帮助文件中将以虚下划线的形式显示,显示的颜 色也为绿色。点击此文字会弹出一个附属的子窗口,窗口中显示相应主题页中的内容。再次 点击鼠标,弹出窗口消失。需要跳转到的页面的标志符也是放在单下划线文字的后面。 删除线格式:与双下划线格式的功能相同。 隐藏格式:设置隐藏格式的文本在生成的帮助文件中是显示不出来的。一般将需要跳转 到的页面的标志符设置为隐藏格式,放在双下划线、单下划线和删除线格式文本的后边。 z 跳转页面的方式 图 12-3 帮助文件附属窗口效果 在帮助文件中经常要从一个页面跳转到另一个页面,对文字使用双下划线或删除线可以 在同一个帮助窗口中跳转到其它的主题页面,如果需要弹出新的帮助窗口来显示主题页面, 只需要在主题页标志符后添加一个“>”然后紧跟窗口的名称即可。窗口是在 HPJ 文件中定 义的,具体创建方法在后面说明。 如果对文字使用单下划线格式,那么弹出的窗口是一个附属子窗口。效果如图 12-3: 如果要跳转到另外一个帮助文件的某个主题页面,可以基于双下划线格式的方法,在跳 第 12 章 俄罗斯方块 396 转目的主题页面的标志符后添加一个“@”字符,接着在后面添加该主题页面所在的帮助文 件的文件名。 12.5.2 创建俄罗斯方块游戏帮助文件的 RTF 文档 根据上面对 RTF 文档格式的说明,我们创建俄罗斯方块游戏帮助文件的 RTF 文档,共 有 5 个页面,每个页面及其脚注如下: z MainPage 页: #K$俄罗斯方块游戏帮助(V2.10) 游戏制作工具 Tools 游戏控制 Control 得分规则 Score 游戏作者About 本页脚注内容为: # MainPage K 俄罗斯方块 $ 俄罗斯方块游戏帮助 z Tools 页 #K$游戏制作工具 俄罗斯方块(Tetris)使用 C++Builder 制作完成。…… 本页脚注内容为: # Tools K 制作工具 $ 游戏编写工具 z Control 页 #K$游戏控制 单人游戏时,用四个光标键控制。↑为变形、↓为直下、←为左移、→为右移、Ctrl 为 加速。双人游戏时,右侧玩家的控制键同上,左侧玩家的控制键为:w 为变形、s 为直下、a 为左移、d 为右移、j 为加速。 本页脚注内容为: # Control K 控制 $ 游戏控制 z Score 页 第 12 章 俄罗斯方块 397 #K$得分规则 一次消一行,得 100 分;一次消两行,得 300 分;一次消三行,得 600 分;一次消 4 行, 得 1200 分。每 2000 分升一级,升 10 级(即 20000 分)游戏结束。 定时器事件间隔得算法为:760-游戏级别×70 ms 如果是双人游戏,自己一次消 2 行,则对家界面底部增加 1 行方格,自己一次消 3 行, 对家增 2 行,自己一次消 4 行,对家增 3 行。 本页脚注内容为: # Score K 得分 $ 得分规则 z About 页 #K$游戏作者 游戏作者:赵明现,昵称 Alpher、小现 QQ:2311572 email:alpher@ccermail.net 欢迎大家与我联系,给出批评和建议! 2004-2-9 本页脚注内容为: # About K 作者 $ 游戏作者 12.5.3 HPJ 的创建 运行 Help Workshop 程序,选择菜单“File”->“New”,选择“Help Project”创建一个 工程,为工程设定文件名为 Tetris.hpj,并保存。 在界面右侧的按钮中选择 Files,弹出 Topic Files 对话窗,在对话窗中选择 Add 按钮,将 编写的 Tetris.rtf 文件添加进去。如图 12-4 所示。 此时已经可以编译生成 hlp 文件了,但是你会发现生成的 hlp 帮助文件只有索引模式, 没有目录树结构,要实现目录树结构的显示,需要为选择“File”->“New”,选定“Help Contents”,为工程添加目录标签功能。 通过界面右侧的按钮为目录树添加节点,如 12-5 图。其中,标签前面为书形图形的,其 类型为 Heading,问号图形的,其类型为 Topic。为各个 Topic 设置相对应的主题页的标志符, 也就是其 Topic ID 属性。如,游戏作者的 Topic ID 属性为 About。 第 12 章 俄罗斯方块 398 图 12-4 为 hpj 工程添加 Rtf 文档 图 12-5 帮助目录树设计 第 12 章 俄罗斯方块 399 在俄罗斯方块帮助文件中,没有用到弹出新的帮助窗口,如果用到这种方法,需要在 RTF 中双下划线和删除线文字后方的页面标志符后,指定显示主题页面的窗口,而在这里就需要 在工程总自定义窗口。定义窗口的方法如下: 在 Help Workshop 打开 hpj 文件的时候,选择右边的“Windows…”按钮,将弹出“Add 阿 New Window Type”对话窗,在“Create a window named”项中输入窗口名称,如“MyWin”。 单击 OK 按钮后弹出“Window Properties”对话窗,在它里面对窗口的颜色、标题、按钮等 属性进行设置。 将新建的窗口名称,添加到 RTF 需要打开新窗口的地方,即可。 12.5.4 编译生成 hlp 文件 将制作的 Tetris.hpj 和 Tetris.cnt 保存,选择菜单中的编译命令,编译后即生成 Tetris.hlp 文件,即帮助文件。其运行效果如图 12-6: 图 12-6 帮助文件运行效果 第 12 章 俄罗斯方块 400 12.5.5 在游戏中启动帮助 在游戏中启动帮助文件可以使用 Application 全局对象中的函数,也可以通过 Windows API 函数来实现,相关函数的说明如下: z Application 提供的方法 HelpContext 方法: bool __fastcall HelpContext(Classes::THelpContext Context); 其中,Context 是一个长整型数,它与帮助文件中主题页的标志符构成一一映射。标志符和长 整型数的映射可以点击 Help Workshop 中 HPJ 窗口的 Map…按钮来设置。 HelpCommand 方法: bool __fastcall HelpCommand(int Command, int Data); 此方法用于向 WinHelp 发送一个命令 Command。使用示例如下: void __fastcall TForm1::BitBtn1Click(TObject* Sender) { Application->HelpFile = "myhelp.hlp"; Application->HelpCommand(HELP_CONTENTS, 0); } HelpJump 方法: bool __fastcall HelpJump(const AnsiString JumpID); 其中 JumpID 为默认打开的帮助文件中的主题页标志符。 z Windows API 函数 WinHelp 是一个 API 函数,它用来打开帮助文件。其声明如下: BOOL WinHelp( HWND hWndMain, // 调用帮助的窗口的句柄 LPCTSTR lpszHelp, // 指向帮助文件的指针 UINT uCommand, // type of help DWORD dwData // 附加数据,与 uCommand 相关 ); 与帮助有关的 API 函数还有 GetMenuContextHelpId 、 GetWindowContextHelpId 、 SetMenuContextHelpId 和 SetWindowContextHelpId。 12.5.6 Tetris 游戏中帮助的启动 要想在游戏中启动帮助,需要先指定帮助文件。要指定帮助文件,可以在代码中使用如 下方法指定: Application->HelpFile = "myhelp.hlp"; 也可以点击菜单“Project”->“Options…”,在弹出对话窗的 Application 页中,指定“Help file”。 第 12 章 俄罗斯方块 401 俄罗斯方块游戏中的帮助菜单有两个子菜单,分别用于打开帮助文件和弹出关于游戏作 者的信息,由于帮助文件中有关于作者的信息,所以让“关于作者”菜单打开帮助文件中的 “About”主题页。 它们的代码如下: //--------------------------------------------------------------------------- //打开帮助文件 void __fastcall TForm1::HelpClick(TObject *Sender) { Application->HelpCommand(HELP_FINDER, 0); } //--------------------------------------------------------------------------- //打开帮助文件,进入“关于作者”主题页 void __fastcall TForm1::AboutClick(TObject *Sender) { Application->HelpJump("About"); } //--------------------------------------------------------------------------- 12.6 思考题 z 本章的 Tetris 游戏中有哪些功能模块?分别完成哪些功能? z 为 Tetris 游戏增添自定义控制键功能 z 为游戏添加背景图片 z 结合第 11 章的内容为游戏增添网络对战功能 z 你能为 Tetris 游戏实现人机对战功能吗?尝试之 《C++ Builder 6 编程实例精解 赵明现》 第 13 章 制作 DirectX 动画 本章重点 本章讲述利用 DirectX 技术实现动画的方法。DirectX 技术是一个很优秀的 Windows 游戏 开发接口,DirectX API 基于 COM 建立,可以处理 2D、3D 图象、声音、各种输入设备、网 络功能等。本章介绍 DirectDraw、DirectSound、DirectInput 等常用技术的使用。 学习目的 通过本章的学习,您可以: ■ 掌握 DirectX 的一般概念 ■ 掌握 DirectX 编程的基本步骤 ■ 掌握 DirectDraw、DirectSound、DirectInput 的使用 ■ 掌握制作动画的方法 第 13 章 制作 DirectX 动画 403 本章典型效果 第 13 章 制作 DirectX 动画 404 13.1 DirectX 简介 13.1.1 DirectX 的特点 在 DirectX 之前,大多数的电脑游戏都是 MS-DOS 模式的,开发这些游戏必须直接操作 各种硬件。DirectX 的推出,主要目的就是引导电脑游戏向 Windows 操作系统发展,它提供 应用程序与硬件间坚实可靠的接口操作,减轻安装设置及体现硬件优越性能的复杂程度,使 得软件开发者可以在不必考虑硬件细节的情况下,也能充分的发挥硬件的性能。使用 DirectX 的最主要的目标就是使得 Windows 成为一个理想的多媒体平台,简化多媒体应用程序的设 计,但是在简化编程的同时,又不能降低程序运行速度,所以在 DirectX 中,除了很好的利 用 Windows 的设备独立性分层技术之外,又提供了程序员直接操作硬件的功能。 DirectX 有下面几个特点: z DirectX 是一种 Windows 环境下标准的高性能游戏、多媒体开发工具包,使用 DirectX 开 发的程序能够与操作系统默契地配合成为“真正”的桌面应用程序。 z 使用 DirectX 可以利用硬件厂商提供的驱动程序接口,充分最佳的设备性能。 z 通过直接底层硬件操作,实现最快速、短延时、设备无关的底层接口。 z DirectX 采用了 COM(组件对象模型)标准,所以对于不同对象的版本可以有不一样的 接口,这使得用 DirectX 开发的程序在未来一切可能的计算机硬件状态下的使用得到完全 的兼容和支持。 13.1.2 DirectX 的结构和组成 DirectX 中为了实现设备无关性,它使用了两个驱动程序,一个是硬件抽象层 HAL,一 个是硬件模拟层 HEL。当 DirectX 创建对象时,同时会创建一个描述表,其中记录系统硬件 支持的功能列表,当应用程序操作 DirectX 对象实现某个功能时,首先就会查询该描述表, 如果当前系统硬件支持所请求的功能,则向 HAL 发送请求,实现之,如果当前系统硬件不 支持,则向 HEL 发送请求,通过模拟的方法实现所请求的功能。例如,如果声卡支持硬件混 音,则混音的工作将由声卡完成,如果不支持,则可以通过软件进行混音。 DirectX 分为两个层次,即 DirectX Foundation(系统层)和 DirectX Media(应用层)。 其中,DirectX Foundation 是 Windows 平台上多媒体应用程序运行的基础,包括 DirectDraw、 DirectInput、DirectSound、DirectSound 3D、DirectMusic 和 Direct3D Immediate Mode;DirectX Media 包括 Direct3D Retain Mode、DirectAnimation、DirectShow 和 DirectPlay,同时它也支持 CRML(Vitrual Reality Markup/Modeling Language)。 其中常用的部分有: z DirectDraw:处理图形、动画和视频输出。DirectX 是图形和动画的内存管理器,它可以 第 13 章 制作 DirectX 动画 405 直接使用硬件加速功能,例如页面切换和图形覆盖。 z DirectSound:提供声音播放的接口,实现了硬件及软件的混音,并可适时对声音进行捕 捉等效果处理。 z DirectInput:提供游戏输入设备的控制接口。它基于 Windows 的硬件输入 API 及驱动程 序,支持鼠标、键盘、游戏杆及力反馈设备等。 z DirectPlay:提供网络相关的功能,可以轻松的创建串行连接、Modem、IPX 和 Internet 的联机对战游戏程序。 本章主要介绍 DirectDraw、DirectSound 和 DirectInput。 13.2 DirectX 使用基础 13.2.1 DirectDraw 的使用 DirectDraw 是 DirectX 的核心部分,DirectX 的其它组件,如 Direct3D,都是建立在它的 基础之上的。DirectDraw 可以访问从前程序员不能访问的硬件,如显示卡上的内存,实现硬 件显示块移动,硬件屏遮以及切换平面等。 Microsoft DirectDraw API 支持快速访问计算机视频适配器的加速硬件功能。它支持在所 有视频适配器上显示图形的标准方法,并且使用加速驱动程序时可以更快更直接地访问。 DirectDraw 为程序(如游戏和二维图形程序包)以及 Windows 系统组件(如数字视频编解 码器)提供了一种独立于设备之外的方法来访问特定显示设备的功能,而不要求用户提供设 备功能的其它信息。 使用 DirectDraw 时,首先应该创建一个 DirectDraw 对象,DirectDraw 对象提供了创建和 操作其子对象(如画面对象 DirectDrawSurface、调色板 DirectDrawPalette)的方法。在一个 程序中可以创建多个 DirectDraw 对象,不同的 DirectDraw 对象之间相互独立,拥有独立的接 口和子对象。 DirectDraw 对象有: 表 13-1 DirectDraw 对象 对象 说明 DirectDraw 使用 DirectDraw 可以查询显示能力、改变系统显示模式、控制内存等, 并且可以管理其子对象。 DirectDrawSurface 提供生成平滑动画,操作覆盖图和访问 GDI 所需的功能 DirectDrawPalette 提供了调色板显示模式的颜色控制 DirectDrawclipper 此对象允许用户创建运行于用户桌面窗口的 DirectDraw 应用程序 z 枚举设备 显示设备,即指已经安装的显示驱动程序和显卡,硬件抽象层 HAL 与设备相关,而 DirectDraw 对象的创建是与 HAL 相关的。显示设备可以有多个,所以存在多个 HAL 的可能, 如安装多块显卡,就会造成多个 HAL 的产生。 第 13 章 制作 DirectX 动画 406 枚举设备的 API 函数为 DirectDrawEnumerate,其函数说明如下: HRESULT WINAPI DirectDrawEnumerateA( LPDDENUMCALLBACKW lpCallback, //回调函数指针 LPVOID lpContext //传递给回调函数的设备描述表指针 ); 其中回调函数的声明如下: BOOL WINAPI DDEnumCallback( GUID FAR * lpGUID, //指向设备的 GUID LPSTR lpDriveDescription, //设备描述信息字符串指针 LPSTR lpDriveName, //指向设备名称字符串 LPVOID lpContext//指向 ); 每查找到一个设备,便会调用一次回调函数,并将设备的信息传递给回调函数。当回调 函数返回值为 DDENUMRET_OK 时,回调函数继续枚举设备,如果返回值为 DDENUMRET_CANCEL,回调函数停止枚举。 对于 GUID 标志符,系统的主显示设备的 GUID 为 NULL,也就是它没有 GUID 标志符。 z 创建 DirectDraw 对象 获得系统设备以后,就可以根据显示设备创建 DirectDraw 对象。创建 DirectDraw 对象的 API 函数为 DirectDrawCreat,其声明如下: HRESULT WINAPI DirectDrawCreat( GUID FAR *lpGUID, //设备的 GUID 的指针 LPDIRECTDRAW FAR *lpDD, //指向存放 IDirectDraw 接口的指针 IUnknown FAR *pUnkOuter //系统保留,取 NULL ); 其中参数 lpGUID 可取如下值:NULL ,表示使用系统显示主设备; DDCREATE_EMULATIONONLY,表示不使用所用的硬件模拟,而只使用 HEL 模拟; DDCREATE_HARDWAREONLY,表示仅使用硬件的特性,不使用 HEL 模拟,如果硬件不 支持所请求的操作,返回 DDERR_UNSUPPORTED。 函数调用成功,将获得一个大 IDirectDraw 接口的 DirectDraw 对象。 z 设置协作级别 在获得 DirectDraw 对象之后,下一步工作就是设置协作级别(Cooperative Levels),决定 应用程序如何运行,程序是否全屏显示,是否是窗口显示,是否使用独占模式,能否使用 Ctrl+Alt+Del 重起计算机等方面的性质。设置协作级别的函数为 SetCooperativeLevel,其声明 如下: HRESULT SetCooperativeLevel( HWND hWnd, //窗口句柄 DWORD dwFlags //控制标志 ); 参数 dwFlags 的取值有: 第 13 章 制作 DirectX 动画 407 表 13-2 协作级别取值 取值 说明 DDSCL_ALLOWMODEX 允许使用 Mode X 显示模式。它必须与 DDSCL_EXCLUSIVE 和 DDSCL_FULLSCREEN 一起使 用 DDSCL_ALLOWREBOOT 允许在独占模式和全屏模式下使用 Ctrl+Alt+Del 重起电脑 DDSCL_EXCLUSIVE 独占模式,它必须与 DDSCL_FULLSCREEN 模式一起使 用 DDSCL_FULLSCREEN 全屏模式,必须与 DDSCL_EXCLUSIVE 一起使用 DDSCL_NORMAL 设置程序为普通的 Windows 程序,它不能与 DDSCL_ALLOWMODEX 、 DDSCL_EXCLUSIVE 和 DDSCL_FULLSCREEN 同时使用 DDSCL_NOWINDOWCHANGES 表示程序窗口在激活时不能最小化和自动恢复 通常使用 DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN 将协作级别设置为全屏独占模 式,在这种模式下,程序可以使用硬件的一切,包括设置使用定义或动态调色板,改变显示 分辨率以及进行页交换等。也可以使用普通 Windows 模式,即 DDSCL_NORMAL。 z 设置显示模式 显示模式包括显示分辨率,色彩深度和刷新频率。要设置显示模式,首先要获得显示器 支持的显示模式,这就要用到显示模式枚举函数 EnumDisplayModes,它是 DirectDraw 对象 的一个方法,可以枚举硬件所支持的所有显示模式,其声明如下: HRESULT EnumDisplayModes( DWORD dwFlags, //显示模式 LPDDSURFACEDESC lpDDSurfaceDesc, //DDSURFACEDESC 结构指针 LPVOID lpContext, //传回给回调函数的描述表指针 LPDDENUMMODESCALLBACK lpEnumModesCallback //回调函数指针 ); 回调函数的声明如下: HRESULT WINAPI EnumModesCallback( LPDDSURFACEDESC lpDDSurfaceDesc, LPVOID lpContext //设备描述表指针 ); 此函数返回 DDENUMRET_OK 时,显示模式枚举将继续进行,返回 DDENUMRET_CANCEL 时,停止枚举。 设置显示模式要使用 DirectDraw 对象的 SetDisplayMode 方法,其定义如下: HRESULT SetDisplayMode( DWORD dwWidth, //显示分辨率的宽度 DWORD dwHeight,//分辨率的高度 DWORD dwBPP,//存储一个象素的位数 第 13 章 制作 DirectX 动画 408 DWORD dwRefreshRate, //新的刷新频率 DWORD dwFlags //附加标志 ); 程序结束,显示模式会自动恢复,不需要使用代码完成。要恢复显示模式,也可以使用 函数实现,此函数为 HRESULT RestoreDisplayMode(); z 位转换 位转换也就是内存数据的复制,在 DirectDraw 重,复制操作的源对象和目的对象都是 DirectDrawSurface 对象的一部分或整体。DirectDrawSurface 提供两个位转换操作的方法 Blt 和 BltFast。 Blt 的声明如下: HRESULT Blt( LPRECT lpDestRect, //目标矩形结构的2018香港马会开奖现场 LPDIRECTDRAWSURFACE3 lpDDSrcSurface, //源画面的2018香港马会开奖现场 LPRECT lpSrcRect, //源矩形结构2018香港马会开奖现场 DWORD dwFlags, //控制标志 LPDDBLTFX lpDDBltFx //其它效果的 DDBLTFX 结构2018香港马会开奖现场 ); 其中 dwFlags 的取值如下: 表 13-3 Blt 函数控制标志 取值 说明 DDBLT_ASYNC 将位转换操作加入到位转换操作队列中,异步执行。默认值 DBLT_WAIT 函数调用一直等到位转换操作建立起来或出现一个 DDERR_WASSTILLDRAWING 之外的错误才返回 DDBLT_COLORFILL 使用参数 DDBltFx 的 dwFillColor 成员来填充目标矩形 DDDBLT_DDFX 使用参数 DDBltFx 的 dwDDFX 成员所指定的效果来执行位转换操作 DBLT_KEYDEST 使用与目标画面向缓的颜色值 DDDBLT_KEYSR 使用与源画面相关的颜色值 BltFast 函数的声明如下: HRESULT BltFast( DWORD dwX, //目标矩形的 X 坐标 DWORD dwY, //目标矩形的 Y 坐标 LPDIRECTDRAWSURFACE3 lpDDSrcSurface, //位转换操作源画面的指针 LPRECT lpSrcRect, //操作源矩形指针 DWORD dwTrans //控制标志 ); dwTrans 的取值如下: 第 13 章 制作 DirectX 动画 409 表 13-4 BltFast 函数控制标志 取值 说明 DDBLTFAST_DESTCOLORDKEY 使用目标颜色值 DDBLTFAST_SRCCOLORKEY 使用源颜色值 DDBLTFAST_NOCOLORKEY 执行不透明的位转换操作。默认值 DDBLTFAST_WAIT 在位转换操作建立或出现错误之前不要返回 13.2.2 DirectSound 的使用 Microsoft DirectSound API 为程序和音频适配器的混音、声音播放和声音捕获功能之间 提供了链接。DirectSound 为多媒体软件程序提供低延迟混合、硬件加速以及直接访问声音 设备等功能。 z 声音设备的枚举 声音设备的枚举函数声明如下: HRESULT WINAPI DirectSoundEnumerate( LPDSENUMCALLBACK lpDSEnumCallback, LPVOID lpContext ); 回调函数声明如下: BOOL DSEnumCallback(LPGUID lpGuid,LPCSTR lpcstrDescription,LPCSTR lpcstrModule, LPVOID lpContext); 第一个枚举的设备总是主声音驱动程序,枚举函数不会传递它的 GUID 指针给回调函数。 在大多数情况下,没必要枚举声音设备,因为 DirectSound 对象通常是初始化系统中最好的 声音设备。 z 创建 DirectSound 对象 DirectSound 对象的生成使用函数 DirectSoundCreate,其声明如下: HRESULT WINAPI DirectSoundCreate( LPGUID lpGuid, //设备的 GUID,主设备的 GUID 为 NULL LPDIRECTSOUND *ppDS,//指针,函数被调用之后初始化 IUnknown FAR *pUnkOuter //不使用,设为 NULL ); z 设置协作级别 DirectSound 对象建立之后,需要调用其 SetCooperativeLevel 方法,将 DirectSound 对象 绑定在一个窗口上,并声明应用程序和其它应用程序之间怎样共享声音设备。 SetCooperativeLevel 函数声明如下: HRESULT SetCooperativeLevel( HWND hWnd, //应用程序的窗口句柄 DWORD dwLevel //协作级别 第 13 章 制作 DirectX 动画 410 ); 表 13-5 DirectSound 协作级别 级别 说明 DSSCL_NORMAL 与其它应用程序的最好的协作方式。这种方式下,不能改变主缓 冲区声音格式,限制只能使用 DirectSound 默认的输出格式 DSSCL_PRIORITY 允许改变主缓冲区的声音格式,这样可能会影响到其它应用程序 DSSCL_EXCLUSIVE 允许独占声音设备,将所有的后台应用程序静音 DSSCL_WRITEPRIMARY 允许直接存取。不能播放从缓冲区的声音对象,其它应用程序将 丢失声音对象 一般在程序中设置 DSSCL_NORMAL 或 DSSCL_PRIORITY 即可满足需求。 z 设置主缓冲区对象的格式 DirectSound 只支持 wav 格式的声音文件,wav 声音文件的格式信息包含在一个 WAVEFORMATEX 结构中,该结构如下: typedef struct{ WORD wFormatTag; //格式类型 WORD nChannels; //声道数,1:单声道,2:双声道 DWORD nSamplesPerSec; //采样率 DWORD nAvgBytesPerSec; //平均传输率 WORD nBlockAlign;//每个采样点的字节数 WORD wBitsPerSample; //每个采样点位数,8 位或 16 位 WORD cbSize; //附加信息字节数 }WAVEFORMATEX; 默认时主缓冲区格式为:采样频率 22050Hz,双声道,采样点 8 位。如果要改变格式, 可以通过 DirectSound 对象的 SetFormat 方法来实现,该方法只需要一个指向 WAVEFORMATEX 结构的指针作为参数。 13.2.3 DirectInput 的使用 DirectInput API 为游戏提供高级输入功能并能处理游戏杆以及包括鼠标、键盘和强力反 馈游戏控制器在内的其它相关设备的输入。 z 枚举输入设备 对于鼠标和键盘来说,一般不需要进行枚举,但是如果需要使用游戏杆等其它输入设备 时,就需要枚举以获得设备的 GUID。枚举函数声明如下: HRESULT EnumDevices( DWORD dwDevType, //要枚举的设备的类型 LPDIENUMCALLBACK lpCallback, //回调函数指针 LPVOID pvRef,//返回给回调函数使用的数据指针 第 13 章 制作 DirectX 动画 411 DWORD dwFlags//控制标志 ); z 初始化 DirectInput 初始化 DirectInput 的函数为 DirectInputCreate,其声明如下: HRESULT WINAPI DirectInputCreate( HINSTANCE hinst, //程序实例句柄 DWORD dwVersion, //DirectInput 版本号,此值通常取 DIRECTINPUT_VERSION LPDIRECTINPUT *lplpDirectInput, //DirectInput 对象的2018香港马会开奖现场 LPUNKNOWN punkOuter //保留参数 ); z 创建 DirectInput 设备接口 初始化并选定输入设备之后,接下来需要获取设备接口。使用 DirectInput 对象的 CreateDevice 方法,此方法需要设备的 GUID 作为参数。对于常用的键盘鼠标,可以使用系 统预定义的变量 GUID_SysKeyboard 或 GUID_SysMouse。函数声明如下: HRESULT CreateDevice( REFGUID rguid, //设备的 GUID LPDIRECTINPUTDEVICE *lplpDirectInputDevice, //设备接口指针 LPUNKNOWN pUnkOuter //系统保留 ); z 设置数据格式 因为不同的输入设备的输入数据是不一样的,所以要从设备读取输入数据,必须先设置 读取数据的格式。设置数据格式可以使用 SetDataFormat 函数,其声明如下: HRESULT SetDataFormat( LPCDIDATAFORMAT lpdf //CDIDATAFORMAT 结构体的2018香港马会开奖现场 ); DirectX 中预定义了几种数据格式,如下表: 表 13-6 输入设备数据格式 DIDAFORMAT 变量 数据结构 c_dfDIMouse DIMOUSESTATE c_dfDIKeyboard char[256] c_dfDIJoystick DIJOYSTATE,适用于多数的游戏控制器 c_dfDIJoystick2 DIJOYSTATE2,适用于标准游戏控制器 z 获得设备输入数据 DirectInput 不使用 Windows 消息,它采用的是一种类似于 Windows 消息的输入信息系统, 即使用缓冲区数据。每当输入事件发生以后,就会创建一个数据包,这些数据包被放在一个 私有的缓冲区,通过 GetDiviceData 方法从缓冲区读取数据包。GetDeviceData 函数声明如下: HRESULT GetDeviceData( DWORD cbObjectData, //DIDEVICEOBJECTDATA 结构体大小 第 13 章 制作 DirectX 动画 412 LPDIDEVICEOBJECTDATA rgdod, //接收数据的结构数组 LPDWORD pdwInOut, //指向含有最大获取项数的控制标志指针 DWORD dwFlags//控制标志 ); 一次从缓冲区中取出的数据项数由 pdwInOut 决定,一般情况下,从缓冲区中取出一项 数据后,该数据将从缓冲区中删除。 13.3 窗体及资源 在这里,因为要使用全屏方式显示 DirectX 图象,所以窗口不用做任何修改。 对于程序中要使用的位图资源,必须是 256 色的,而且将所有的位图制作到同一个位图 文件中,这样做是为了让所有的位图使用同一个调色板。 本章使用的位图图象如图 13-1 所示: 图 13-1 程序使用的位图资源 第 13 章 制作 DirectX 动画 413 如图 13-1 为程序中使用的位图。我们把用到的图片都放在同一个位图图片中,这样做的 目的是为了让各个图象的调色板一致,但是必然需要在程序中将每个图形的分离读取才能使 用。 图形分两部分,上方是 800×600 的背景图象,下方是一个旋转圆环动画的 60 帧图片, 在程序中每个一定事件间隔播放一帧图片,实现一个晃动圆环的动画效果。每个圆环都是 64 ×64 象素的大小,播放顺序为从左上角到右下角,然后重复再从左上角的帧图象开始。 位图作为资源文件加入程序,编写 rc 文件内容如下: ALL BITMAP ALL.BMP 即将 all.bmp 位图作为资源文件,其标志符为 ALL。 13.4 程序的实现 13.4.1 程序结构 程序运行开始时,在窗体上输入提示字符串“F3 键显示 DirectX 动画,Esc 键退出程序”, 如果按下 F3 键,则初始化 DirectX 并开始绘图。绘图时,设定一定的时间间隔,每次时间计 数达到时间间隔的要求,则将资源文件中的位图图片的上部 800×600 的部分作为背景画出, 然后绘制圆环的每一帧的图片。程序结束时,要释放建立的 DirectDraw 等对象。 程序开始时输出提示字符串的代码如下: //--------------------------------------------------------------------------- void __fastcall TFormMain::FormPaint(TObject *Sender) { AnsiString S = "F3 键显示 DirectX 动画,Esc 键退出程序"; HDC DC = GetDC(Handle); if (!FActive) { SetBkMode(DC, TRANSPARENT); TextOut(DC, 25, 100, S.c_str(), S.Length()); } ReleaseDC(Handle, DC); } //--------------------------------------------------------------------------- 第 13 章 制作 DirectX 动画 414 图 13-2 程序开始的界面效果 程序开始界面的运行效果如图 13-2 所示。 对键盘事件的相应代码如下: //--------------------------------------------------------------------------- void __fastcall TFormMain::FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift) { switch (Key) { case VK_F3: FActive = True; Start(); PostMessage(Handle, WM_INFOSTART, 0, 0); break; case VK_ESCAPE: case VK_F12: FRunApp = False; FActive = False; Close(); break; } } //--------------------------------------------------------------------------- 其中 FActive 表示窗体是否处于激活状态,FRunApp 用于控制是否绘制动画;Start 函数 用来对 DirectDraw 进行初始化,并绘制底图等,PostMessage 函数发送系统消息 WM_INFOSTART,此消息由用户自己定义。它们的代码在下节列出。在程序开始时,需要 第 13 章 制作 DirectX 动画 415 对 FActive 和 FRunApp 设定初始值,如下: //--------------------------------------------------------------------------- __fastcall TFormMain::TFormMain(TComponent* Owner) : TForm(Owner) { FRunApp = True; FActive = False; } //--------------------------------------------------------------------------- 动画中每帧图片的绘制放在 WM_INFOSTART 消息的响应函数中,在程序结束时要释放 DirectDraw 对象,释放的方式是调用其 Release 方法,并将指针设为空,代码如下: //--------------------------------------------------------------------------- void __fastcall TFormMain::FormDestroy(TObject *Sender) { if( lpDD != NULL ) { if( lpDDSPrimary != NULL ) { lpDDSPrimary->Release(); lpDDSPrimary = NULL; } if( lpDDSOne != NULL ) { lpDDSOne->Release(); lpDDSOne = NULL; } if( lpDDPal != NULL ) { lpDDPal->Release(); lpDDPal = NULL; } lpDD->Release(); lpDD = NULL; } } //--------------------------------------------------------------------------- 释放的各个对象都在头文件中声明,代码见下节内容。 第 13 章 制作 DirectX 动画 416 13.4.2 头文件 头文件中,需要声明一些变量,而且还要拦截自定义消息,增添部分的代码如下: #define WM_INFOSTART WM_USER //自定义消息 #include private: //DirectDraw 对象 LPDIRECTDRAW lpDD; //获得的 DirectDraw 接口指针 LPDIRECTDRAWSURFACE lpDDSPrimary; LPDIRECTDRAWSURFACE lpDDSBack; LPDIRECTDRAWSURFACE lpDDSOne; LPDIRECTDRAWPALETTE lpDDPal; BOOL FActive; BOOL FRunApp; void UpdateFrame(void); //绘制下一帧图片 void Start(); //绘图开始的准备工作 void InitFail(); //初始化失败 HRESULT RestoreAll(void);//恢复丢失表面 MESSAGE void MyMove(TMessage &Message); //自定义消息的响应函数 public: BEGIN_MESSAGE_MAP //自定义消息拦截 MESSAGE_HANDLER(WM_INFOSTART, TMessage, MyMove); END_MESSAGE_MAP(TForm); 在 main.cpp 的开头,加入如下内容: #include "ddutil.h" //拷图等操作的函数声明头文件,函数实现在 ddutil.cpp 中 #define NAME "RingAnimate" #define TITLE "RingAnimate" //定义资源中位图文件的名字 char szBitmap[] = "ALL"; 13.4.3 初始化 Start 函数主要是完成创建 DirectDraw 对象,设置协作级别,设置显示模式等操作,具体 实现代码如下: //--------------------------------------------------------------------------- void TFormMain::Start() { 第 13 章 制作 DirectX 动画 417 HRESULT ddrval; DDSURFACEDESC ddsd; DDSCAPS ddscaps; ddrval = DirectDrawCreate( NULL, &lpDD, NULL ); if( ddrval != DD_OK ) { InitFail(); return; } // 独占模式 ddrval = lpDD->SetCooperativeLevel(Handle, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN ); if( ddrval != DD_OK ) { InitFail(); return; } // 设定显示模式为 800×600,色彩为 8 位 ddrval = lpDD->SetDisplayMode( 640, 480, 8); if( ddrval != DD_OK ) { InitFail(); return; } //创建具有一个后台缓冲区(back buffer)的主界面(primary surface) ddsd.dwSize = sizeof( ddsd ); ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT; ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX; ddsd.dwBackBufferCount = 1; ddrval = lpDD->CreateSurface( &ddsd, &lpDDSPrimary, NULL ); if( ddrval != DD_OK ) { InitFail(); return; 第 13 章 制作 DirectX 动画 418 } ddscaps.dwCaps = DDSCAPS_BACKBUFFER; ddrval = lpDDSPrimary->GetAttachedSurface(&ddscaps, &lpDDSBack); if( ddrval != DD_OK ) { InitFail(); return; } //创建和设置调色板 lpDDPal = DDLoadPalette(lpDD, szBitmap); if (lpDDPal) lpDDSPrimary->SetPalette(lpDDPal); // 创建后台界面,并为其装在背景图片 lpDDSOne = DDLoadBitmap(lpDD, szBitmap, 0, 0); if( lpDDSOne == NULL ) { InitFail(); return; } //设置透明色为黑色 DDSetColorKey(lpDDSOne, RGB(0,0,0)); } //--------------------------------------------------------------------------- 其中,如果有初始化操作失败,将弹出失败信息,调用函数 InitFail,其代码如下: //--------------------------------------------------------------------------- void TFormMain::InitFail() { MessageBox(Handle, "DirectDraw Init FAILED", TITLE, MB_OK ); Close(); } //--------------------------------------------------------------------------- 第 13 章 制作 DirectX 动画 419 13.4.4 帧图片的绘制 要实现动画,首先要有动画的各个帧的图片,然后在程序中依次绘制每帧图片,让每帧 图片之间有一定的时间间隔即可。 对于计时,我们选择使用 API 函数 GetTickCount,其声明如下: DWORD GetTickCount(VOID); 此函数用于获得自 Windows 系统启动以来的毫秒计数,由于它是 DWORD 类型,所以当系 统连续运行 49.7 天之后,毫秒计数会返回到 0。 在函数中声明静态变量用来记录上帧画面的绘制时间,程序中设定一个循环过程,每此 循环都判断一下现在的毫秒计数与上帧画面的毫秒计数之间的差值,是否大于要求的帧时间 间隔,如果大于,则绘制下一帧图片。 对于循环过程,需要注意的是需要再循环过程中加入 ProcessMessages 函数,不然会导致 程序无法响应其它消息。 消息的拦截见 13.4.2 的内容,响应函数代码如下: //--------------------------------------------------------------------------- MESSAGE void TFormMain::MyMove(TMessage &Message) { do { UpdateFrame(); Application->ProcessMessages(); } while(FRunApp == True); } //--------------------------------------------------------------------------- 其中 UpdateFrame 函数完成三个不同晃动速度的圆环的帧图画的绘制,其代码如下: //--------------------------------------------------------------------------- void TFormMain::UpdateFrame( void ) { //记录上一帧图画的绘制时刻 static DWORD lastTickCount[3] = {0,0,0}; //记录三个动画的当前帧数 static int currentFrame[3] = {0,0,0}; DWORD thisTickCount; //现在的系统秒计数 RECT rcRect; //矩形区域,用于指定复制图象的区域 //三个动画的显示速度 DWORD delay[3] = {50, 78, 13}; int i; //三个动画的显示位置 第 13 章 制作 DirectX 动画 420 int xpos[3] = {288, 190, 416}; int ypos[3] = {128, 300, 256}; HRESULT ddrval; thisTickCount = GetTickCount(); //获得当前时刻 for(i=0; i<3; i++) { if((thisTickCount - lastTickCount[i]) > delay[i]) { // 判断下一帧画面的帧计数 lastTickCount[i] = thisTickCount; currentFrame[i]++; if(currentFrame[i] > 59) currentFrame[i] = 0; } } // Blit the stuff for the next frame rcRect.left = 0; rcRect.top = 0; rcRect.right = 640; rcRect.bottom = 480; while(1) { ddrval = lpDDSBack->BltFast( 0, 0, lpDDSOne, &rcRect, DDBLTFAST_NOCOLORKEY ); if( ddrval == DD_OK ) break; if( ddrval == DDERR_SURFACELOST ) { ddrval = RestoreAll(); if( ddrval != DD_OK ) { return; } } if( ddrval != DDERR_WASSTILLDRAWING ) { return; 第 13 章 制作 DirectX 动画 421 } } if(ddrval != DD_OK) return; for(i=0; i<3; i++) { rcRect.left = currentFrame[i]%10*64; rcRect.top = currentFrame[i]/10*64 + 480; rcRect.right = currentFrame[i]%10*64 + 64; rcRect.bottom = currentFrame[i]/10*64 + 64 + 480; while( 1 ) { ddrval = lpDDSBack->BltFast( xpos[i], ypos[i], lpDDSOne, &rcRect, DDBLTFAST_SRCCOLORKEY ); if( ddrval == DD_OK ) break; if( ddrval == DDERR_SURFACELOST ) { ddrval = RestoreAll(); if( ddrval != DD_OK ) return; } if( ddrval != DDERR_WASSTILLDRAWING ) return; } } // Flip the surfaces while( 1 ) { ddrval = lpDDSPrimary->Flip( NULL, 0 ); if( ddrval == DD_OK ) break; if( ddrval == DDERR_SURFACELOST ) { ddrval = RestoreAll(); 第 13 章 制作 DirectX 动画 422 if( ddrval != DD_OK ) break; } if( ddrval != DDERR_WASSTILLDRAWING ) break; } } //--------------------------------------------------------------------------- 13.4.5 界面恢复 在用户按下 Alt+Tab、Windows 键,或其它方法(如启动其它程序的热键)切换到其它 程序时,DirectX 绘制的界面将丢失,这就需要对界面图象进行恢复操作。在界面图象丢失后 可以用 HRESULT IDirectDrawSurface::Restore()方法来恢复,同时必需重新绘制界面上的图 形。Restore 方法没有入口参数,但是若要成功恢复已丢失的图面,必须让屏幕显示方式重新 恢复到其初始的状态。为了判断图面是否已经丢失,也可以使用 HRESULT IDirectDrawSurface::IsLost()方法来进行检测,若返回值为 DDERR_SURFACELOST 则说明图 面丢失了。 本程序中恢复图象的操作代码如下: //--------------------------------------------------------------------------- HRESULT TFormMain::RestoreAll( void ) { HRESULT ddrval; ddrval = lpDDSPrimary->Restore(); if( ddrval == DD_OK ) { ddrval = lpDDSOne->Restore(); if( ddrval == DD_OK ) { DDReLoadBitmap(lpDDSOne, szBitmap); } } return ddrval; } //--------------------------------------------------------------------------- 第 13 章 制作 DirectX 动画 423 13.4.6 程序运行效果 图 13-3 DirectX 动画运行效果 13.5 图形操作函数的实现 在 13.4 节中的图象操作中用到了很多函数,如 DDLoadPalette、DDLoadBitmap、 DDSetColorKey,它们都是在 ddutil.h 中声明,在 ddutil.cpp 中实现。ddutil 中的七个函数,都 是 Microsoft 提供的,代码如下: // create a DirectDrawSurface from a bitmap resource IDirectDrawSurface * DDLoadBitmap(IDirectDraw *pdd, LPCSTR szBitmap, int dx, int dy) { HBITMAP hbm; BITMAP bm; DDSURFACEDESC ddsd; IDirectDrawSurface *pdds; // try to load the bitmap as a resource, if that fails, try it as a file hbm = (HBITMAP)LoadImage(GetModuleHandle(NULL), szBitmap, IMAGE_BITMAP, dx, dy, LR_CREATEDIBSECTION); 第 13 章 制作 DirectX 动画 424 if (hbm == NULL) hbm = (HBITMAP)LoadImage(NULL, szBitmap, IMAGE_BITMAP, dx, dy, LR_LOADFROMFILE|LR_CREATEDIBSECTION); if (hbm == NULL) return NULL; // get size of the bitmap GetObject(hbm, sizeof(bm), &bm); // get size of bitmap // create a DirectDrawSurface for this bitmap ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT |DDSD_WIDTH; ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; ddsd.dwWidth = bm.bmWidth; ddsd.dwHeight = bm.bmHeight; if (pdd->CreateSurface(&ddsd, &pdds, NULL) != DD_OK) return NULL; DDCopyBitmap(pdds, hbm, 0, 0, 0, 0); DeleteObject(hbm); return pdds; } /* DDReLoadBitmap * load a bitmap from a file or resource into a directdraw surface. * normaly used to re-load a surface after a restore.*/ HRESULT DDReLoadBitmap(IDirectDrawSurface *pdds, LPCSTR szBitmap) { HBITMAP hbm; HRESULT hr; // try to load the bitmap as a resource, if that fails, try it as a file hbm = (HBITMAP)LoadImage(GetModuleHandle(NULL), szBitmap, IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION); if (hbm == NULL) hbm = (HBITMAP)LoadImage(NULL, szBitmap, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE|LR_CREATEDIBSECTION); if (hbm == NULL) { OutputDebugString("handle is null\n"); return E_FAIL; 第 13 章 制作 DirectX 动画 425 } hr = DDCopyBitmap(pdds, hbm, 0, 0, 0, 0); if (hr != DD_OK) { OutputDebugString("ddcopybitmap failed\n"); } DeleteObject(hbm); return hr; } // DDCopyBitmap draw a bitmap into a DirectDrawSurface HRESULT DDCopyBitmap(IDirectDrawSurface *pdds, HBITMAP hbm, int x, int y, int dx, int dy) { HDC hdcImage; HDC hdc; BITMAP bm; DDSURFACEDESC ddsd; HRESULT hr; if (hbm == NULL || pdds == NULL) return E_FAIL; // make sure this surface is restored. pdds->Restore(); // select bitmap into a memoryDC so we can use it. hdcImage = CreateCompatibleDC(NULL); if (!hdcImage) OutputDebugString("createcompatible dc failed\n"); SelectObject(hdcImage, hbm); // get size of the bitmap GetObject(hbm, sizeof(bm), &bm); // get size of bitmap dx = dx == 0 ? bm.bmWidth : dx; // use the passed size, unless zero dy = dy == 0 ? bm.bmHeight : dy; // get size of surface. ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_HEIGHT | DDSD_WIDTH; pdds->GetSurfaceDesc(&ddsd); if ((hr = pdds->GetDC(&hdc)) == DD_OK) { StretchBlt(hdc, 0, 0, ddsd.dwWidth, ddsd.dwHeight, hdcImage, x, y, dx, dy, SRCCOPY); 第 13 章 制作 DirectX 动画 426 pdds->ReleaseDC(hdc); } DeleteDC(hdcImage); return hr; } // DDLoadPalette Create a DirectDraw palette object from a bitmap resoure IDirectDrawPalette * DDLoadPalette(IDirectDraw *pdd, LPCSTR szBitmap) { IDirectDrawPalette* ddpal; int i; int n; int fh; HRSRC h; LPBITMAPINFOHEADER lpbi; PALETTEENTRY ape[256]; RGBQUAD * prgb; // build a 332 palette as the default. for (i=0; i<256; i++) { ape[i].peRed = (BYTE)(((i >> 5) & 0x07) * 255 / 7); ape[i].peGreen = (BYTE)(((i >> 2) & 0x07) * 255 / 7); ape[i].peBlue = (BYTE)(((i >> 0) & 0x03) * 255 / 3); ape[i].peFlags = (BYTE)0; } // get a pointer to the bitmap resource. if (szBitmap && (h = FindResource(NULL, szBitmap, RT_BITMAP))) { lpbi = (LPBITMAPINFOHEADER)LockResource(LoadResource(NULL, h)); if (!lpbi) OutputDebugString("lock resource failed\n"); prgb = (RGBQUAD*)((BYTE*)lpbi + lpbi->biSize); if (lpbi == NULL || lpbi->biSize < sizeof(BITMAPINFOHEADER)) n = 0; else if (lpbi->biBitCount > 8) n = 0; else if (lpbi->biClrUsed == 0) n = 1 << lpbi->biBitCount; else n = lpbi->biClrUsed; // a DIB color table has its colors stored BGR not RGB so flip them around. 第 13 章 制作 DirectX 动画 427 for(i=0; i 8) n = 0; else if (bi.biClrUsed == 0) n = 1 << bi.biBitCount; else n = bi.biClrUsed; // a DIB color table has its colors stored BGR not RGB so flip them around. for(i=0; iCreatePalette(DDPCAPS_8BIT, ape, &ddpal, NULL); return ddpal; } /*DDColorMatch * convert a RGB color to a pysical color. * we do this by leting GDI SetPixel() do the color matching * then we lock the memory and see what it got mapped to. */ 第 13 章 制作 DirectX 动画 428 DWORD DDColorMatch(IDirectDrawSurface *pdds, COLORREF rgb) { COLORREF rgbT; HDC hdc; DWORD dw = CLR_INVALID; DDSURFACEDESC ddsd; HRESULT hres; // use GDI SetPixel to color match for us if (rgb != CLR_INVALID && pdds->GetDC(&hdc) == DD_OK) { rgbT = GetPixel(hdc, 0, 0); // save current pixel value SetPixel(hdc, 0, 0, rgb); // set our value pdds->ReleaseDC(hdc); } // now lock the surface so we can read back the converted color ddsd.dwSize = sizeof(ddsd); while ((hres = pdds->Lock(NULL, &ddsd, 0, NULL)) == DDERR_WASSTILLDRAWING) ; if (hres == DD_OK) { dw = *(DWORD *)ddsd.lpSurface; // get DWORD if(ddsd.ddpfPixelFormat.dwRGBBitCount < 32) dw &= (1 << ddsd.ddpfPixelFormat.dwRGBBitCount)-1; // mask it to bpp pdds->Unlock(NULL); } // now put the color that was there back. if (rgb != CLR_INVALID && pdds->GetDC(&hdc) == DD_OK) { SetPixel(hdc, 0, 0, rgbT); pdds->ReleaseDC(hdc); } return dw; } / * DDSetColorKey * set a color key for a surface, given a RGB. * if you pass CLR_INVALID as the color key, the pixel in the upper-left corner will be used. */ HRESULT DDSetColorKey(IDirectDrawSurface *pdds, COLORREF rgb) { DDCOLORKEY ddck; 第 13 章 制作 DirectX 动画 429 ddck.dwColorSpaceLowValue = DDColorMatch(pdds, rgb); ddck.dwColorSpaceHighValue = ddck.dwColorSpaceLowValue; return pdds->SetColorKey(DDCKEY_SRCBLT, &ddck); } 13.6 思考题 z DirectDraw 使用的一般步骤是哪些? z DirectSound 对音频文件格式有哪些要求? z 为什么把资源文件的图片都做在一张图片上比较好? z 本章动画各帧图片之间是如何实现时间延迟的? 《C++ Builder 6 编程实例精解 赵明现》 第 14 章 餐厅结账管理程序 本章重点 本章介绍餐厅结账管理程序的制作。详细讲解 BDE 的使用,数据表的创建和设置,以 及 C++Builder 中数据库相关组件的使用,介绍了利用报表组件设计统计报表的具体过程,还 有制作软件封面的技术。 学习目的 通过本章的学习,您可以: ■ 掌握 BDE 的使用方法 ■ 掌握利用 Database Desktop 创建和设置数据表的方法 ■ 掌握 TTable、TDataSource、TDBGrid 等组件的使用 ■ 掌握数据库的查找、修改等操作的方法 ■ 掌握制作 Master/Detail 类型和 List 类型报表的方法 ■ 掌握软件封面的制作方法 第 14 章 餐厅结账管理程序 431 本章典型效果图 第 14 章 餐厅结账管理程序 432 第 14 章 餐厅结账管理程序 433 14.1 C++Builder 数据库程序开发基础 14.1.1 BDE 简介 在 C++Builder 中,数据库程序中对数据库文件的操作都是通过 BDE(Borland Database Engine)接口完成的。对程序员来说,BDE 起到桥梁的作用,它沟通了应用程序和数据库, 使得程序员只需要向 BDE 发送数据库操作请求,而不用考虑数据库系统的具体要求,BDE 将程序发送的数据库操作请求翻译给数据库,再将数据库的返回信息发送给应用程序。 对 BDE 的设置通过 BDE Administrator 完成,从“开始”菜单中选择“Borland C++Builder 6”程序组,启动“BDE Administrator”,界面如图 14-1 所示。 第 14 章 餐厅结账管理程序 434 图 14-1 BDE Administrator 左侧有两个选项页,在 Configuration 页中,可以为各个数据库驱动程序进行参数设置, 其中 Drivers 子选项列出各种本地和远程数据库连接,System 子选项中则可以设置驱动程序 的语言文字、文件格式、日期、时间的表示方法等。 Database 页用于管理在 BDE 中注册的各个别名(Alias)。在应用程序中,需要先设置别 名,然后才可以访问到该别名所对应的目录中的数据库文件,而改变数据库文件的位置之后, 只需要对 BDE 进行修改,不需要对应用程序做任何更改。打个比方,别名就像映射的一个 驱动器,应用程序中只需要知道该驱动器的盘符,而 BDE 则负责根据需要设置此驱动器对 应的物理2018香港马会开奖现场,可以是本地数据库,也可以是网络上的数据库。 在 Database 页中,利用“Object”->“New”创建一个新的别名 Ch14,通过右边的 Definition 页可以更改其类型、驱动程序、文件存放路径、是否允许使用 BCD 等参数。本章使用的数 据库文件都存放在 Ch14 别名所对应的目录中。 14.1.2 数据库表的建立 在 BDE Administrator 创建数据库目录别名 Ch14 时,选择其驱动程序为 Paradox。与 FoxPro、dBASE 等相比,Paradox 与 C++Builder 的结合更加完美,而且,也足够用来实现一 个标准数据库系统所需要的种种优点。 创建别名并设置其驱动模式之后,就要创建数据库表,之后就可以创建窗体编写代码实 现各种功能了。创建数据库表可以使用 SQL Explorer 和 Database Desktop 等工具,这里使用 Database Desktop,并简单介绍它的用法。 点击“开始”菜单,在“Borland C++Builder 6”程序组选择“Database Desktop”,进入 程序之后选择“File”->“New”->“Table…”,如图 14-2,选择 Table type 为 Paradox7, 第 14 章 餐厅结账管理程序 435 图 14-2 Database Desktop 首先设定数据库表中各个字段的名称(Field Name),需要在 25 个字符以内,不能重名。 需要注意的是,因为 Database Desktop 是英文版的,所以如果要使用中文的字段名,需要在 BDE 中设置中文语言驱动。 然后要为各个字段设置数据类型,在 Type 栏单击鼠标右键,或者按空格键,会弹出如 图 14-3 所示的菜单,从中选择需要的字段数据类型。其中的 Alpha 为字符串型,Memo 为备 注类型,±(Autoincrement)为自增型,自增型的数据由数据库系统自动赋值,由于如此,它 决不会重复,所以一般可以用做数据库表的索引。 第 14 章 餐厅结账管理程序 436 图 14-3 数据库表设计 在 Size 栏可以设置字符串等类型数据的长度,对于 Number 等数据不可以设置长度,但 可以在右侧的“Minimum value”和“Maximum value”设置其最大和最小值。 在 Key 栏中双击鼠标左键,或者按空格键,可以设置/取消对应字段为数据表的关键字。 一个数据表可以有一个或多个关键字,也可以没有关键字,但是关键字字段必须在非关键字 字段上方,也就是说如果一个字段不是 Key,那么它以后的字段都不允许是 Key。 在创建设置一个字段为 Key 时,该字段会自动被设置为索引字段。要为数据表创建副索 引,选择 Table Properties 下拉框中的 Secondary Indexes 项,点击“Define…”按钮,弹出如 图 14-4 所示的对话窗,将需要设为副索引的字段用右箭头添加到右侧 Indexed fields 内。 点击“Save as”保存数据表,指定其别名为 Ch14,如图 14-5 所示。 另外在使用数据库过程中,关联表的使用也是很重要的,也即为两个数据库创建一定的 关系,以维护数据库浏览和引用的完整性。引用完整性是数据库程序员智慧包中最有价值的 工具之一,它能避免用户输入非法的数据或者不小心删除有用的记录。例如本章使用的餐厅 职员数据库,有职员级别(Rank.db)和职员详细信息(Employee.db)两个数据表,而职员 详细信息中有一个字段表示职员的级别,为了维护数据表的浏览引用完整性,就需要在这两 个表之间建立两个表中级别字段的关系,如图 14-6 所示。 建立引用完整性之后,其效果如下: 第 14 章 餐厅结账管理程序 437 z 不允许删除 Rank.db 中与 Employee.db 相关的内容。比如 Employee.db 中有记录的 RankNum 为 A,那么 Rank.db 中就不允许删除 Rank 为 A 的记录。 z 不允许在 Employee.db 中添加记录,如果所添加的记录 RankNum 在 Rank.db 中不存在相 应记录。例如,如果 Rank.db 中没有 Rank 为 C 的记录,那么在 Employee.db 中就不可以 添加 RankNum 为 C 的记录。 在请求这些不允许的操作时,将会弹出“Master record missing”的错误信息,在 C++Builder 中,可以创建自己的异常处理程序来向用户解释到底出了什么错误,为什么不能执行这样的 操作。 另外在 Database Desktop 中也可以实现多个 QBE Query 的合并操作。本章我们不使用此 项功能,所以不再讲述。 图 14-4 创建副索引 图 14-5 保存数据表 第 14 章 餐厅结账管理程序 438 图 14-6 定义引用完整性 图 14-7 数据输入 选择“File”->“Open”->“Table”打开建立的数据表,弹出如图 14-7 所示的数据浏览 /数据界面,按 F9 切换输入和浏览状态。 14.1.3 C++Builder 数据库程序的结构 要使用数据库文件编写程序,首先需要添加数据访问(DataAccess)组件,它负责通过 BDE 与数据库表实现数据交换。然后就是添加数据控制(DataControl)组件,它从数据访问 组件获得数据并显示之,如果数据被修改则将修改数据交给数据访问组件,让它实现数据的 更改。 数据访问组件包括负责对数据库文件进行数据的存取操作,常用的有 TTable、TQuery、 TDataSource 等,TQuery 常用于 SQL 相关的数据库程序,本章只使用 TTable 和 TDataSource, 第 14 章 餐厅结账管理程序 439 所以下节只对它们做详细讲解。 对于数据控制组件,很多都很容易用,如 TDBEdit、TDBLabel、TDBMemo 等等,它们 与 TEdit、TLabel、TMemo 的使用相同,只需要为其指定数据源,即 DataSource 属性,以及 需要显示的字段值。另外,对于可编辑内容的组件,其 ReadOnly 属性决定是否允许修改数 据库内容。 14.2 TTable 组件 在本章的餐厅结账管理程序中,对于数据库的访问使用 TTable 组件。它以表格形式存取 数据库文件中的各个数据,在使用 TTable 组件时必须用 DatabaseName 设定一个数据库别名, 本章的数据库文件都在别名 Ch14 中,本章程序的 TTableDatabaseName 都须设为 Ch14(当然, 也可以设为数据库文件的绝对路径)。然后需要为 TTable 指定 TableName,即数据库文件名。 TQuery 组件没有 TableName 属性,它通过 SQL 语句实现与数据库的连接,这里不使用它, 所以不做讨论。 14.2.1 TTable 组件的属性和方法 TTable 组件的属性和方法分别如表 14-1 和表 14-2 所示。 表 14-1 TTable 组件的属性 属性 说明 Active 它表示是否打开所连接的数据库文件。当 Active 为 false 时,与数据库的连 接断开,不能对数据库进行读写操作,数据感知组件也不能显示和修改数 据。当 Active 为 true 时,即可从数据库读取数据,能否修改数据库数据, 由 CanModify 属性决定。 如果要在程序中更改 TTable 的 TableName,必须先将其 Active 置为 false, 和执行其 Close 方法。 Active 属性置为 true 和 false 与 TTable 的 Open 和 Close 方法效果相同 AutoCalcFields 决定什么时候触发 OnCalcFields 事件、什么时候计算查找字段。如果 AutoCalcFields 置为 true(默认值),那么在各个组件之间切换焦点、在栅 格组件的行列中切换时以及数据被修改时都会触发 OnCalcFields 事件,计 算字段的数据也会重新计算。 当允许修改数据库数据时,会频繁触发 OnCalcFields 事件,所以,可以将 AutoCalcFields 置为 false 以减少 OnCalcFields 事件的触发频率和计算字段 的计算频率 Bof 指示当前记录是否为第一个记录 Database 指定使用的 TDatabase 组件 DatabaseName 指定数据库别名。也可以是路径名 第 14 章 餐厅结账管理程序 440 Eof 指示当前记录是否为最后一个记录 Exclusive 决定程序是否以独享方式使用 Paradox 或 dBASE 型的数据表 Exists 标示在一个数据库别名中是否存在指定名字的数据表,如果不存在,可以 使用 CreatTable 方法创建新表 FieldCount 标示数据表中字段的个数 Fields 指向数据库中各个字段的信息,可以使用该属性访问字段值,如: Edit1->Text = CustTable->Fields->Fields[0]->AsString; FieldValues 通过该属性可以访问当前活动记录指定字段的值,它会自动处理字段数据 格式,如:Customers->FieldValues["CustNo"] = Edit1->Text; Filter 指定过滤数据库的记录的表达式。当使用过滤时,则只能访问到符合过滤 条件的记录。过滤条件表达式如:State = 'CA' or State = 'MA',也可以使用 通配符,如:State = 'M*',如果字段名称中含有空格等特殊字符,需要将 字段名放置在中括号中,如:[Species Name] = 'Gymnothorax mordax' Filtered 决定是否使用过滤方法来访问数据库数据 IndexDefs 此属性包含数据表的索引信息 IndexFieldCount 标示当前索引字段由多少个字段组成。如果当前索引字段是单字段数据, 则返回 1,如果是多字段,则返回当前索引字段引用的字段数目 IndexFieldNames 该属性中列出了可用做索引的所有字段名,为其赋值可以通过字段名指定 当前所用的索引字段 IndexName 设置该属性可以为数据表选择作为索引的字段。需要注意的 IndexName 所 取的值必须在 IndexDefs 中,而且 IndexName 与 IndexFieldNames 互斥,指 定一个属性时,另外一个属性自动清空 KeyExclusive 表示在为索引指定一个范围时,是否包含符合条件的边界数据记录。默认 此值为 false,给包含边界纪录 KeyFieldCount 表示查询时使用 Key 中字段的数目,为 0 时表示使用所有 Key 中所有字段 MasterField 指定一个或多个字段,以与另外的数据表响应字段相对应,从而在数据表 间建立 master-detail 关系 MasterSource 指定 master-detail 关系表的主表数据源 Modified 标示当前数据记录是否被修改 ReadOnly 决定程序对数据库是否只读 RecordCount 返回 TDataSet 中的记录数目。需要注意,一般只在 Paradox 和 dBASE 型的 Table 中使用该属性,SQL Query 中不使用。如果使用了过滤或者为某字段 设置了范围,在 Paradox 的 Table 中,此属性返回过滤后或者范围内的记录 数目,而对于其它类型的 Table,RecordCount 属性不可信,只是估计值 State TDataSet 的状态。可以取 dsInactive、dsBrowse、dsEdit、dsInsert 等 TableName 指定数据库别名中的数据表文件名 TableType 指定数据表类型。一般选为 ttDefault,即根据文件扩展名决定 第 14 章 餐厅结账管理程序 441 表 14-2 TTable 组件的方法 方法 说明 AddIndex 创建新索引 Append 在数据表最后添加一个空记录,并处于编辑状态 AppendRecord 在数据表后添加一个指定数据内容的记录 ApplyRange 激活由 Set/EditRange 方法建立的范围 BatchMove int __fastcall BatchMove(TBDEDataSet* ASource, TBatchMode AMode); 从 ASource 中移动一个数据到当前的 Table 中,AMode 指定移动操作类型 Cancel 在对当前记录数据的修改还没有使用 Post 方法前,Cancel 取消该修改操作 CancelRange 取消当前所有的范围限制操作 ClearFields 清空当前记录所有字段的值 Close 置 TDataSet 的 Active 属性为 false CreateTable 创建一个新数据表 Delete 删除当前记录 DeleteIndex 删除数据表的一个副索引 DeleteTable 删除当前连接的数据表文件 Edit 设置 TDataSet 处于编辑状态 EditKey 设置 TDataSet 处于 dsSetKey,然后通过对缓冲区数据的设置和 GotoKey 的调 用跳到指定数据内容的记录处 EditRangeEnd 调用此方法之后,可以设置限制范围的上限 EditRangeStart 调用此方法之后,可以设置限制范围的下限,须调用 ApplyRange 应用之,如: Customer->EditRangeStart(); Customer->FieldByName("Company")->AsString = Edit1->Text; Customer->EditRangeEnd(); Customer->FieldByName("Company")->AsString = Edit2->Text; Customer->ApplyRange(); // Apply the ranges EmptyTable 删除数据表中所有记录 FetchAll 将数据表中从当前记录到最后记录的内容读取保存在本地。在读取网络数据 库时使用 FieldByName 返回当前记录中指定名字的字段的数据,返回类型为 TField,可以用 TField 的 AsString、AsInteger、AsFloat 等方法转换为指定的类型 FindField 查找并返回指定名称的字段。如: Table1->FindField("CustNo")->AsString = "1234"; FindFirst bool __fastcall FindFirst(void);查找满足指定查找条件的第一个记录,如果成 功,返回 true,并改变当前记录为该记录,如果失败,返回 false,并不改变 当前记录位置 FindLast 查找满足指定条件的最后一个记录,返回值同 FindFirst FindNext 查找满足指定条件的下一个记录,返回值同 FindFirst 第 14 章 餐厅结账管理程序 442 FindPrior 查找满足指定条件的上一个记录,返回值同 FindFirst First 设置数据表中第一个记录为当前记录 FindKey 根据关键字段内容查找记录,如: TVarRec vr = ("Princess Island SCUBA"); Table1->FindKey(&vr, 0); 又如:Table1->FindKey(ARRAYOFCONST(("Princess Island SCUBA"))); 如果查找到记录并设置为当前记录,返回 true,否则不改变当前记录,返回 false FindNearest 查找与指定条件最接近的记录,参数和返回值同 FindKey GotoKey 根据当前设置的关键字精确查找指定记录。参看 SetKey 方法 GotoNearest 查找与 SetKey 指定的关键字最接近的记录 Insert 在数据表的当前所在位置插入一条空记录,设置该记录为当前记录,并处于 编辑状态 InsertRecord 在数据表中插入一个指定内容的记录 IsEmpty 判断数据表是否为空 Last 设置数据表的最后一个记录为当前记录 Locate 查找并设置查找到的记录为当前记录 LockTable 说定一个 Paradox 或 dBASE 类型的数据表。参数 LockType 指定锁定类型, 一个程序可以调用两次 LockTable 分别设定读锁定和写锁定 MoveBy int __fastcall MoveBy(int Distance);将与当前记录相距 Distance 的记录设置为 当前记录 Next 将下一个记录设置为当前记录,常与 First 一起使用遍历整个数据表 Open 激活数据表,即设置 Active 属性为 true Prior 将上一个记录设为当前记录 Post 将对数据的更改写入数据表文件 RenameTable 为本地数据表重命名 SetKey 使数据表属于设置关键字状态 UnlockTable 为本地数据表解锁 14.2.2 TTable 的事件 TTable 组件的事件主要为其对应 TDataSet 的事件,TDataSet 可以响应的事件如表 14-3。 表 14-3 TTable 响应事件 响应事件 说明 AfterCancel 在 TDataSet 取消前一步操作之后触发 AfterClose 在 TDataSet 被关闭之后触发 AfterDelete 在 TDataSet 删除当前记录后触发 第 14 章 餐厅结账管理程序 443 AfterEdit 在 TDataSet 被编辑之后触发 AfterInsert 在 TDataSet 插入一个新记录之后触发 AfterOpen 在 TDataSet 被打开之后触发 AfterPost 在 TDataSet 保存被修改的记录后触发 BeforeCancel 在 TDataSet 取消前一步操作之前触发 BeforeClose 在 TDataSet 被关闭之前触发 BeforeDelete 在 TDataSet 删除当前记录前触发 BeforeEdit 在 TDataSet 被编辑之前触发 BeforeInsert 在 TDataSet 插入一个新记录之前触发 BeforeOpen 在 TDataSet 被打开之前触发 BeforePost 在 TDataSet 保存被修改的记录前触发 OnCalcField 在 TDataSet 中,当计算字段需要重新计算其数据时触发 OnDeleteError 在 TDataSet 删除一个记录的操作产生异常时触发 OnEditError 在 TDataSet 修改或插入一个记录的操作产生异常时触发 OnFilterRecord 在 TDataSet 中记录被过滤时触发 OnNewRecord 在 TDataSet 插入或追加一个记录时触发 OnPostError 在 TDataSet 修改或插入记录的操作产生异常时触发 14.3 餐厅结账管理程序的数据库设计 z 职员级别数据表 Rank.db 数据表结构如下: Field Name Type Size Key 1 RankNum A 1 * 2 RankName A 20 其中 RankNum 标示职员等级,RankName 为该等级的名称。要使用本软件,需要用本人的 ID 登陆,而不同登记的职员登陆后,提供的操作功能有不同。(软件各个菜单提供的功能参见后 面小节的内容)对于系统管理员,所有的功能都可以用,对于经理,则不能用数据库维护等 功能,对于收银员,则不能用报表统计功能等。 数据表内容如下: Rank RankNum RankName 1 A 系统管理员 2 B 经理 3 C 收银员 4 D 厨师 5 E 领班 6 F 服务生 7 G 杂务 第 14 章 餐厅结账管理程序 444 8 H 其他人员 z 职员信息数据表 Employee.db 数据表结构如下: Field Name Type Size Key 1 ID I * 2 Password A 6 3 Name A 15 4 Gender A 2 5 Age S 6 IDCardNum A 18 7 Remark M 100 8 Rank A 1 其中 ID 的最小值设为 1000,给需要四位数字。 为了编写调试程序,自然先要为该数据表添加一些内容,而且程序发行时,也要预先在此数 据库添加至少一个系统管理员用户。随便为之添加几个用户即可,如图 14-7。 参看 14.1.2 和图 14-6 为 Rank.db 和 Employee.db 的职员级别字段之间创建引用完整性。 z 菜品分类数据表 Class.db 数据表结构如下: Field Name Type Size Key 1 ClassNum A 1 * 2 ClassName A 20 * 数据表内容如下: ClassNum ClassName 1 A 凉菜类 2 B 猪牛羊肉类 3 C 鸡鸭鹅禽类 4 D 海鲜类 5 E 火锅类 6 F 时令鲜蔬类 7 G 酒水汤类 8 H 主食 9 I 其他 这是预定的菜品类别,在程序中系统管理员可以对菜品分类进行添加删除和修改。 z 菜谱信息数据表 Dish.db 数据表结构如下: Field Name Type Size Key 1 DishNum + * 2 DishName A 20 * 3 ClassNum A 1 4 DishPrice N 第 14 章 餐厅结账管理程序 445 5 DishUnitName A 6 6 Description M 100 7 DisCount N 其中 DishNum 为自增型,DishName 为菜谱的名字,ClassNum 为该菜谱所属的菜品编号, DishPrice 和 DisCount 分别为菜谱的单价和折扣,DishUnitName 为其单位(如盘、碟等), Description 为对该菜谱的说明描述。 为菜谱信息数据表添加一些菜谱,以便编写和调试程序使用,如下: ClassNum DishNum DishName DishPrice DishUnitName Description DisCount 1 A 1 拍黄瓜 3.00 盘 100.00 2 B 2 回锅肉 5.00 盘 100.00 3 C 3 宫爆鸡丁 3.00 盘 100.00 4 D 4 水煮鱼 20.00 盘 100.00 5 H 5 米饭 0.50 盘 100.00 6 G 6 疙瘩汤 8.00 份 100.00 7 F 7 金针菇 2.00 盘 100.00 8 H 8 刀削面 3.00 碗 100.00 9 I 9 大前门香烟 8.00 盒 100.00 10 I 10 帝豪香烟 10.00 盒 100.00 11 A 11 炒花生米 2.00 盘 100.00 12 E 12 麻辣鸳鸯火锅 20.00 锅 100.00 菜谱信息由系统管理员在程序中添加、删除和修改。 z 餐桌信息数据表 Table.db Field Name Type Size Key 1 TableNum l * 2 TableType A 10 3 TableStatus A 10 其中 TableNum 为餐桌编号,TableType 为餐桌类型,如双人桌、四人桌、包间等,TableStatus 为餐桌状态,可取“空闲”或“使用”。 随便为数据表添加几条记录,如下: TableNum TableType TableStatus 1 1 双人桌 空闲 2 2 双人桌 空闲 3 3 四人桌 空闲 4 4 八人桌 空闲 5 5 包间 空闲 6 6 包间 空闲 z 点菜信息数据表 Ordered.db Field Name Type Size Key 1 DishNum l 2 TableNum l 第 14 章 餐厅结账管理程序 446 3 Date D 4 Time T 5 Amount N 6 Status A 10 7 UserID l 其中 DishNum 为所点菜谱编号,TableNum 为点菜的餐桌,Date 和 Time 为点菜的时间日期, Amount 为此菜的数量,UserID 为执行添加菜单操作的职员 ID,Status 为此菜单的状态,可 取“未结账”和“已结账”。 z 结账账单数据表 Pay.db Field Name Type Size Key 1 TableNum l 2 ShouldPay N 3 ActuallyPay N 4 Charge N 5 Date D 6 Time T 7 Description M 8 UserID l 其中 ShouldPay、ActuallyPay 和 Charge 分别为应付金额、实付金额和找零。 14.4 界面设计与功能实现 本节详细讲述各个窗体的设置和功能实现的代码。其中统计报表功能的实现放在 14.5 节 讲述。 第 14 章 餐厅结账管理程序 447 14.4.1 主界面 图 14-8 FormMain 窗体界面 如图 14-8 所示,为程序主界面,主界面只是根据登陆职员的级别显示对应的功能菜单, 而每个菜单命令都会打开新的窗体。程序功能分为四类,分别对应主菜单的前四个菜单项。 主菜单的结构如图 14-9 所示。 第 14 章 餐厅结账管理程序 448 图 14-9 FormMain 主菜单结构 各个菜单项的功能由其名字可以看出,不同等级的职员所能使用的菜单不同,参看 14.3 节中关于 Rank.db 数据表的说明。 在头文件中,需要添加一个标示当前登陆用户 ID 的变量,如下: public: // User declarations __fastcall TFormMain(TComponent* Owner); int UserID; 主界面窗体 FormMain 的源文件 Unit1.cpp 内容如下: //--------------------------------------------------------------------------- #include #pragma hdrstop #include "UnitLogin.h" //登陆窗体头文件,此窗体在后面讲述 #include "Unit1.h" //主窗体 #include "Unit2.h" //FormClassDishMan,菜谱、菜品数据库维护 #include "Unit3.h" //FormTableMan,餐桌数据库维护 第 14 章 餐厅结账管理程序 449 #include "Unit4.h" //FormListMan,已点菜单数据库维护 #include "Unit5.h" //FormPayMan,结账数据库维护 #include "Unit6.h" //FormEmployeeMan,职员登记和职员信息数据库维护 #include "Unit7.h" //FormOrder,点菜界面 #include "Unit8.h" //FormPay,结账界面 #include "Unit9.h" //QRFormEmployee,职员信息报表 #include "Unit10.h" //QRFormOrdered,菜谱分类销售报表 #include "Unit11.h" //QRFormPay,销售账单报表 //--------------------------------------------------------------------------- #pragma package(smart_init) #pragma resource "*.dfm" TFormMain *FormMain; //--------------------------------------------------------------------------- __fastcall TFormMain::TFormMain(TComponent* Owner) : TForm(Owner) { } //--------------------------------------------------------------------------- void __fastcall TFormMain::FormClose(TObject *Sender, TCloseAction &Action) { //结束程序。因为需要将登陆窗体 FormLogin 作为创建的第一个窗体, //所以这里要通过 Application 来结束应用程序,否则 Close 操作只会 Hide 该窗体 Application->Terminate(); } //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuClassDishManClick(TObject *Sender) { //弹出维护窗口 Hide(); FormClassDishMan->ShowModal(); Show(); } //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuTableManClick(TObject *Sender) { //弹出维护窗口 Hide(); FormTableMan->ShowModal(); Show(); 第 14 章 餐厅结账管理程序 450 } //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuListManClick(TObject *Sender) { Hide(); FormListMan->ShowModal(); Show(); } //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuOrderClick(TObject *Sender) {// Hide(); FormOrder->ShowModal(); Show(); } //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuPayManClick(TObject *Sender) {//结账数据库维护 Hide(); FormPayMan->ShowModal(); Show(); } //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuPayClick(TObject *Sender) {//结账 Hide(); FormPay->ShowModal(); Show(); } //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuEmployeeManClick(TObject *Sender) { Hide(); FormEmployeeMan->ShowModal(); Show(); 第 14 章 餐厅结账管理程序 451 } //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuReLoginClick(TObject *Sender) {//恢复默认的菜单设置 Menu_M->Visible=true; Menu_R->Visible=true; Menu_O->Visible=true; Hide(); //隐藏 FormMain FormLogin->EditID->Text=""; FormLogin->EditPass->Text=""; FormLogin->Show(); //弹出登陆窗体 FormLogin->EditID->SetFocus(); } //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuEmployeeStatClick(TObject *Sender) { Hide(); QRFormEmployee->QuickRep1->Preview(); Show(); } //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuDishStatClick(TObject *Sender) { Hide(); QRFormOrdered->QuickRep1->Preview(); Show(); } //--------------------------------------------------------------------------- void __fastcall TFormMain::MenuBusinessStatClick(TObject *Sender) { Hide(); QRFormPay->QuickRep1->Preview(); Show(); } //--------------------------------------------------------------------------- 第 14 章 餐厅结账管理程序 452 14.4.2 菜品、菜谱数据库维护 图 14-10 FormClassDishMan 窗体界面 如图 14-10 所示,为菜品、菜谱数据库维护界面。为了清晰的表达菜品菜谱的结构,选 用了目录树的方法,将菜品作为根目录的子节点,将各个菜品的菜谱名作为该菜品节点的子 节点,目录树名字为 TreeView1。 右侧有两个 GroupBox,分别显示菜品、菜谱信息。当 TreeView1 中点击菜品节点时,在 GroupBox1 中显示选中的菜品信息。当 TreeView1 中点击菜谱节点时,在 GroupBox2 中显示 选中的菜谱的信息。 添加两个 TTable 组件和两个 TDataSource 组件,它们的 DataBaseName 都设为 Ch14,即 本章设计的数据表所在的目录对应的别名。TableClass 的 TableName 属性设置为 Class.db, TableDish 的 TableName 属性为 Dish.db。设置 DataSourceClass 的 DataSet 属性为 TableClass, DataSourceDish 的 DataSet 属性为 TableDish。 第 14 章 餐厅结账管理程序 453 图 14-11 FormClassDishMan 运行效果 显示菜品信息使用 TDBEdit 组件,两个组件名字分别为 DBEditClassNum 和 DBEditClassName 。将它们与数据表联系起来,需要设置它们的 DataSource 属性为 DataSourceClass,并分别设置它们对应的字段(DataField 属性)为 ClassNum 和 ClassName。 显示菜谱信息也大都是 TDBEdit 组件,对应于 Dish.db 中字段名称,显示这些字段信息 的组件名称分别为 DBEditDishName、DBEditDishNum、DBEditUnitName、DBEditDishPrice、 DBEditDisCount ,菜品的显示使用下拉菜单 TDBComboBox 组件,其名字为 DBComboBoxDishClass,显示菜谱描述信息的是 TDBMemo 组件 DBMemoDishDescription。 设置上面这些显示菜谱信息的组件的 DataSource 为 DataSourceDish,并设置它们对应的显示 字段 DataField 属性。需要注意的是,DBComboBoxDishClass 的 Style 属性设为 csDropDownList,即只能从下拉菜单中选取选项。 然后,将显示菜品、菜谱的所有组件的 ReadOnly 属性设为 true,即只读,不能通过改变 组件中的内容改变数据表中的数据。在程序中,通过点击下面“切换至修改模式”改变这些 组件的 ReadOnly 属性来允许用户修改数据表数据。其中 DBComboBoxDishClass 在设为只读 模式时需要将其 Enabled 属性设为 false,以不让用户选择下拉菜单中的选项,而只作为显示 菜谱所属菜品的组件。 窗体下部一个 TMemo 组件,用于显示帮助信息。下面的按钮分别用于刷新菜品、菜谱 目录树的显示,切换修改或者浏览模式,删除菜品或菜谱以及添加菜品或菜谱。其中,删除 操作需要选中目录树中需要删除的菜品或菜谱节点,添加操作时,如果是添加菜品,则要选 中根节点,如果添加菜谱,则要选中所添加菜谱所属于的菜品所在的节点。 第 14 章 餐厅结账管理程序 454 FormClassDishMan 窗体的运行效果如图 14-11 所示。 首先,在头文件中添加如下内容: public: // User declarations __fastcall TFormClassDishMan(TComponent* Owner); void __fastcall RefreshTreeView(void); void __fastcall MyTreeViewClick(TObject *Sender); void __fastcall RefreshDBComboBoxDishClass(void); 其中,RefreshTreeView 函数用于根据数据表 TableClass 和 TableDish 刷新目录树的显示, RefreshDBComboBoxDishClass 用于根据数据表 TableClass 中的菜品字段的值刷新下拉菜单的 选项,MyTreeViewClick 函数用于处理目录树的鼠标单击事件。MyTreeViewClick 函数的入口 参数为 Sender,在程序中可以将其强制转换为 TTreeView 类型指针,从而对目录树的属性进 行访问和操作,之所以将该函数设为自定义函数是因为在点菜的窗体 FormOrder 中也有一个 相同的目录树,它的鼠标单击事件也要操作和访问 FormClassDishMan 窗体里的 TableClass 和 TableDish 两个数据表,设为自定义函数之后,在 FormOrder 窗体的代码中,只需要将目 录树的 OnClick 的处理函数指定为 FormClassDishMan->MyTreeViewClick 即可。 Unit2.cpp 代码如下: //--------------------------------------------------------------------------- #include #pragma hdrstop #include "Unit1.h" #include "Unit2.h" //--------------------------------------------------------------------------- #pragma package(smart_init) #pragma resource "*.dfm" TFormClassDishMan *FormClassDishMan; //--------------------------------------------------------------------------- __fastcall TFormClassDishMan::TFormClassDishMan(TComponent* Owner) : TForm(Owner) { TableDish->Open(); TableClass->Open(); RefreshTreeView(); RefreshDBComboBoxDishClass(); TreeView1->OnClick=MyTreeViewClick; } //--------------------------------------------------------------------------- void __fastcall TFormClassDishMan::RefreshDBComboBoxDishClass(void) { TableClass->First(); 第 14 章 餐厅结账管理程序 455 for(int i=0;iRecordCount;i++) { //将菜品加入下拉菜单 DBComboBoxDishClass->Items->Add( TableClass->FieldByName("ClassNum")->AsString ); TableClass->Next(); } } //--------------------------------------------------------------------------- void __fastcall TFormClassDishMan::RefreshTreeView(void) { TTreeNode *rNode,*mNode; AnsiString DishClass; int i,j; TreeView1->Items->Clear(); TreeView1->ReadOnly=true; TableClass->First(); TableDish->First(); rNode=TreeView1->Items->Add(NULL,"菜品/菜谱"); for(i=0;iRecordCount;i++) { AnsiString Num=TableClass->FieldByName("ClassNum")->AsString; //将菜品加入树视图 DishClass=TableClass->FieldByName("ClassNum")->AsString + TableClass->FieldByName("ClassName")->AsString; mNode=TreeView1->Items->AddChild(rNode,DishClass); //将 Table2 中对应此菜品的菜谱加入为该菜品的字节点 TableDish->First(); for(j=0;jRecordCount;j++) { if(TableDish->FieldByName("ClassNum")->AsString == Num) { TreeView1->Items->AddChild(mNode, TableDish->FieldByName("DishName")->AsString); } TableDish->Next(); } 第 14 章 餐厅结账管理程序 456 TableClass->Next(); } //展开根节点的一级子目录 rNode->Expand(false); } //--------------------------------------------------------------------------- void __fastcall TFormClassDishMan::MyTreeViewClick(TObject *Sender) { TTreeView *tmpTree=(TTreeView *)Sender; if(tmpTree->Selected->Level == 1) //菜品 { //寻找 Table1 中的相应纪录 TableClass->FindKey(ARRAYOFCONST((tmpTree->Selected->Text[1]))); } else if(tmpTree->Selected->Level == 2)//菜谱 { //根据树视图中的 Text 属性在 Table2 中查找对应纪录 TableDish->IndexFieldNames = "DishName"; TableDish->SetKey(); TableDish->FieldByName("DishName")->Value=tmpTree->Selected->Text; if(!TableDish->GotoKey()) { MessageDlg("找不到该纪录,,建议重新载入数据库", mtError, TMsgDlgButtons() << mbOK, 0); } } } //--------------------------------------------------------------------------- void __fastcall TFormClassDishMan::BitBtnReloadClick(TObject *Sender) { RefreshTreeView(); RefreshDBComboBoxDishClass(); } //--------------------------------------------------------------------------- void __fastcall TFormClassDishMan::BitBtnModifyClick(TObject *Sender) { if(BitBtnModify->Caption == "切换至修改模式") { 第 14 章 餐厅结账管理程序 457 DBEditClassNum->ReadOnly=false; DBEditClassName->ReadOnly=false; DBEditDishNum->ReadOnly=false; DBEditDishName->ReadOnly=false; DBEditDisCount->ReadOnly=false; DBEditUnitName->ReadOnly=false; DBEditDishPrice->ReadOnly=false; DBMemoDishDescription->ReadOnly=false; DBComboBoxDishClass->ReadOnly=false; DBComboBoxDishClass->Enabled=true; BitBtnModify->Caption="切换至浏览模式"; } else { DBEditClassNum->ReadOnly=true; DBEditClassName->ReadOnly=true; DBEditDishNum->ReadOnly=true; DBEditDishName->ReadOnly=true; DBEditDisCount->ReadOnly=true; DBEditUnitName->ReadOnly=true; DBEditDishPrice->ReadOnly=true; DBMemoDishDescription->ReadOnly=true; DBComboBoxDishClass->ReadOnly=true; DBComboBoxDishClass->Enabled=false; BitBtnModify->Caption="切换至修改模式"; } } //--------------------------------------------------------------------------- void __fastcall TFormClassDishMan::BitBtnDeleteClick(TObject *Sender) { if(!TreeView1->Selected) { MessageDlg("请先选中树视图中要删除的项!", mtInformation, TMsgDlgButtons() << mbOK, 0); return; } if(TreeView1->Selected->Level == 1) //菜品 { //寻找 TableClass 中的相应纪录 第 14 章 餐厅结账管理程序 458 if(MessageDlg("确认删除菜品:"+TreeView1->Selected->Text+"?", mtConfirmation, TMsgDlgButtons() << mbYes << mbNo, 0) ==mrYes) { TableClass->FindKey(ARRAYOFCONST((TreeView1->Selected->Text[1]))); TableClass->Delete(); TreeView1->Selected->Delete(); } } else if(TreeView1->Selected->Level == 2)//菜谱 { //根据树视图中的 Text 属性在 TableDish 中查找对应纪录 if(MessageDlg("确认删除菜谱:"+TreeView1->Selected->Text+"?", mtConfirmation, TMsgDlgButtons() << mbYes << mbNo, 0) ==mrYes) { TableDish->FindKey(ARRAYOFCONST((TreeView1->Selected->Text))); TableDish->Delete(); TreeView1->Selected->Delete(); } } //焦点返回目录树 TreeView1->SetFocus(); } //--------------------------------------------------------------------------- void __fastcall TFormClassDishMan::BitBtnAddClick(TObject *Sender) { if(!TreeView1->Selected) { MessageDlg("请选中根节点添加菜品,选中菜品添加菜谱!", mtInformation, TMsgDlgButtons() << mbOK, 0); return; } if(TreeView1->Selected->Level == 0) //根节点,添加菜品 { //在 Table1 中添加纪录 char clsNum=TreeView1->Selected->GetLastChild()->Text[1]; TableClass->Append(); TableClass->FieldValues["ClassNum"] =AnsiString(char(clsNum+1)); 第 14 章 餐厅结账管理程序 459 TableClass->FieldValues["ClassName"] = "未定菜名"; TableClass->Post(); TreeView1->Items->AddChild(TreeView1->Items->GetFirstNode(), TableClass->FieldByName("ClassNum")->AsString + TableClass->FieldByName("ClassName")->AsString); TreeView1->Items->GetFirstNode()->GetLastChild()->Selected=true; TreeView1->SetFocus(); } else if(TreeView1->Selected->Level == 1)//菜品节点,添加菜谱 { //在 Table2 中添加纪录 AnsiString NewDishName; if (InputQuery("新增菜谱", "输入新增菜品的菜名", NewDishName)) { TTreeNode *rNode; TableDish->Append(); TableDish->FieldValues["ClassNum"] = AnsiString(TreeView1->Selected->Text[1]); TableDish->FieldValues["DishName"] = NewDishName; //DishNum 自增型 TableDish->Post(); rNode=TreeView1->Items->AddChild(TreeView1->Selected, TableDish->FieldByName("DishName")->AsString); rNode->Selected=true; TreeView1->SetFocus(); } } } //--------------------------------------------------------------------------- 14.4.3 餐桌库维护 餐桌库维护界面(如图 14-12)中 ,使 用 TDBGrid 组件 DBGrid1 来显示数据表中的数据, 因为餐桌类型和餐桌状态的取值有限制,所以在 DBGrid1 中只显示餐桌编号和餐桌类型,而 且餐桌类型字段是不允许在 DBGrid1 中修改的。 第 14 章 餐厅结账管理程序 460 图 14-12 FormTableMan 窗体界面 图 14-13 编辑 TDBGrid 的列属性 如 FormClassDishMan 窗体中一样,添加 TableTable 和 DataSourceTable 组件,设置 TableTable 的 DatabaseName 为 Ch14,TableName 为 Table.db,DataSourceTable 的 DataSet 为 TableTable。添加 TDBGrid 组件 DBGrid1,调整大小后,双击 DBGrid1,编辑其各列的显示 属性。如图 14-13,添加两个列,并设置它们显示数据的字段名(FieldName)为 TableNum 第 14 章 餐厅结账管理程序 461 和 TableType,更改它们的 Title->Caption 属性分别为“桌号”和“餐桌类型”,其 中 TableType 的 ReadOnly 属性设为 true,即此字段的数据不能在 DBGrid1 中修改。 添加两个组合框组件 DBComboBox1 和 DBComboBox2,分别用于显示和修改餐桌类型 和餐桌状态。设置它们的 Style 为 csDropDownList,DataSource 属性为 DataSourceTable,分 别设置它们对应的字段(DataField 属性)为 TableType 和 TableStatus。 添加导航组件(TDBNavigator)DBNavigator1,它包含 10 个按钮,当设置其与一数据表 连接之后,可以通过这些按钮实现浏览、添加、删除、编辑、刷新等功能。设置其 DataSource 为 DataSourceTable。另外,可以通过设置其 VisibleButtons 属性决定显示哪些按钮,一般还 要为其设置提示文本(Hint 属性),并设置其 ShowHint 属性为 true。默认的提示文本是英文 的,需要改为中文,Hint 属性是一个字符串组类型的数据,共 10 个字符串,每个字符串顺 序对应一个按钮的提示文本,依次对应,不管按钮是否可见。 图 14-14 FormTableMan 运行效果 餐桌管理窗体的运行效果如图 14-14 所示,由于需要的维护操作都已经封装在 TDBNavigator 和 TDBGrid 组件中,所以 Unit3.cpp 中不需要增加什么代码,只需要在窗体初 始化时设置 TableTable 的 Active 为 true 即可(当然,也可以在设计阶段就设置其 Active 属性)。 __fastcall TFormTableMan::TFormTableMan(TComponent* Owner) : TForm(Owner) { TableTable->Open(); } 第 14 章 餐厅结账管理程序 462 14.4.4 已点菜单库维护 图 14-15 FormListMan 窗体界面 图 14-16 FormListMan 运行效果 FormListMan 窗体中有一对 TTable 和 TDataSource,一个 TDBGrid 组件和一个 TDBNavigator 组件,设置它们的 DataSource 属性为 TableOrdered,并 为 DBGrid1 设置列属性 和 Hint 属性。另外,需要项 FormTableMan 中一样在代码中调用 TableOrdered 的 Open 方法。 第 14 章 餐厅结账管理程序 463 14.4.5 结账库维护 图 14-17 FormPayMan 窗体界面 图 14-18 FormPayMan 运行效果 第 14 章 餐厅结账管理程序 464 组件设置参见前面窗体的设置,将各个组件连接到数据表 Pay.db,并设置 TDBGrid 组件 各列的属性。具体操作不再详述。 在窗体初始化时,同样需要调用 TablePay 的 Open 方法。 14.4.6 职员信息和权限库维护 图 14-19 FormEmployeeMan 窗体界面 职员级别和职员信息库的维护放在同一个窗体中,导航组件、TDBGrid 组件、TDBMemo 组件以及 TTable 和 TDataSource 组件的设置,参看前面窗体的设置,这里不再详述。 FormEmplyeeMan 窗体的设计和运行效果分别如图 14-19 和图 14-20 所示。 窗体初始化时,同样需要调用 TableRank 和 TableEmployee 的 Open 方法。 第 14 章 餐厅结账管理程序 465 图 14-20 FormEmployeeMan 运行效果 14.4.7 点(加、退)菜 如图 14-21 所示,点菜(包括加菜和退菜)界面,首先需要有一个列表树显示可以选择 的菜谱,然后需要一个 TListView(列表视图)组件显示已经点的菜谱信息,包括菜谱名称, 数量,单价以及折扣信息。 将 TTable 组件 TableOrdered 与 Ordered.db 连接,设置 DataSourceOrdered 的 DataSet 为 TableOrdered(这里对 Ordered.db 数据表的操作通过本窗体添加的 TTable 和 TDataSource 组 件完成,当然也可以通过 FormListMan->TableOrdered 实现)。 列表树的各个节点由 RefreshTreeView 设置,在此函数中,首先将 FormClassDishMan 窗 体中的菜品、菜谱目录树存入流文件,然后在让 FormOrder 窗体中的目录树从此流文件中读 取目录树的各个节点,并展开根节点。 一个下拉菜单 TComboBox 组件用于选择餐桌桌号,下拉菜单的内容中,还包括餐桌的 状态(空闲,或使用)。要点菜,首先需要在此下拉菜单中选择餐桌号,如果餐桌在空闲状态, 则提示是否将此餐桌置为使用状态,只有使用状态的餐桌才能为其执行点菜操作。下拉菜单 的各个选项的内容由函数 RefreshComboBox 完成,它根据 FormTableMan->TableTable 数据表 的内容实现此操作。 已点的菜单在一个列表视图(TListView)组件中显示,设置其四个列分别为菜名、数量、 第 14 章 餐厅结账管理程序 466 单价和折扣。对此视图的刷新由餐桌选择下拉菜单的 OnChange 事件触发,具体实现在函数 图 14-21 FormOrder 窗体界面 图 14-22 FormOrder 运行效果 ComboBoxTableChange 中,它先判断餐桌是否处于使用状态,如果不是,则询问是否更改, 只有当更改为使用状态,才会根据 TTable 组件 TableOrdered 对应的数据表的内容显示已经点 的菜单。 两个按钮 BitBtnAdd 和 BitBtnDel 分别完成对菜单的添加和删除操作。要添加一道菜, 需要首先选中菜品、菜谱树中的一道菜谱名称,然后点击 BitBtnAdd 按钮,即可完成添加操 第 14 章 餐厅结账管理程序 467 作。同样,选中列表树图中的一道菜,然后点击 BitBtnDel 按钮,则可以删除一份选中的菜。 添加菜谱时,如果菜单中已经有了该菜,则将其数量加一,如果没有此菜,则添加此菜 的记录。删除一道菜时,如果菜单中此菜的数量大于 1,则将其数量减一,否则,删除菜单 中该菜谱的记录。 点菜操作的运行效果如图 14-22 所示。 在头文件 Unit7.h 中添加如下内容: private: // User declarations void __fastcall RefreshTreeView(void); void __fastcall RefreshComboBox(void); int TableChoose; //选择的餐桌编号 源文件 Unit7.cpp 内容如下: //--------------------------------------------------------------------------- #include #pragma hdrstop #include "Unit7.h" #include "Unit1.h" #include "Unit2.h" #include "Unit3.h" //--------------------------------------------------------------------------- #pragma package(smart_init) #pragma resource "*.dfm" TFormOrder *FormOrder; //--------------------------------------------------------------------------- __fastcall TFormOrder::TFormOrder(TComponent* Owner) : TForm(Owner) { //--- TableOrdered->Open(); RefreshTreeView(); TreeViewOrder->OnClick=FormClassDishMan->TreeView1->OnClick; } //--------------------------------------------------------------------------- void __fastcall TFormOrder::FormShow(TObject *Sender) { TableChoose=-1; RefreshComboBox(); } 第 14 章 餐厅结账管理程序 468 //--------------------------------------------------------------------------- void __fastcall TFormOrder::RefreshComboBox(void) { TTable * tmpTable=FormTableMan->TableTable; AnsiString info_str; //从 FormTableMan 中读取所有的餐桌信息,写入下拉菜单中 ComboBoxTable->Items->Clear(); tmpTable->First(); for(int i=0;iRecordCount;i++) { info_str=tmpTable->FieldByName("TableNum")->AsString; info_str+="-"; info_str+=tmpTable->FieldByName("TableType")->AsString; info_str+="-"; info_str+=tmpTable->FieldByName("TableStatus")->AsString; ComboBoxTable->Items->Add(info_str); tmpTable->Next(); } } //--------------------------------------------------------------------------- void __fastcall TFormOrder::RefreshTreeView(void) { TMemoryStream * tmpTreeStream = new TMemoryStream(); FormClassDishMan->TreeView1->SaveToStream(tmpTreeStream); tmpTreeStream->Position=0; TreeViewOrder->LoadFromStream(tmpTreeStream); TreeViewOrder->Items->GetFirstNode()->Expand(false); delete tmpTreeStream; } //--------------------------------------------------------------------------- void __fastcall TFormOrder::ComboBoxTableChange(TObject *Sender) { int pos; if(ComboBoxTable->Text == "") { return; } else 第 14 章 餐厅结账管理程序 469 { pos=ComboBoxTable->Text.Pos("-"); TableChoose=ComboBoxTable->Text.SubString(1,pos-1).ToInt(); //查看选择的餐桌的状态 FormTableMan->TableTable->IndexFieldNames="TableNum"; FormTableMan->TableTable->SetKey(); if(FormTableMan->TableTable->FindKey(ARRAYOFCONST((TableChoose)))) { if(FormTableMan->TableTable->FieldByName("TableStatus")->AsString == "空闲" ) {//询问是否启用该餐桌 if(MessageDlg("该餐桌尚未启用,现在起用吗?", mtConfirmation, TMsgDlgButtons() << mbYes << mbNo, 0) ==mrYes) { FormTableMan->TableTable->Edit(); FormTableMan->TableTable->FieldByName("TableStatus")->AsString="使用"; FormTableMan->TableTable->Post(); RefreshComboBox(); //刷新下拉菜单 //找到 TableChoose 所在的选项 for(int i=0;iItems->Count;i++) { if(ComboBoxTable->Items->Strings[i].SubString(1,1).ToInt() == TableChoose) { ComboBoxTable->ItemIndex=i; //这里会触发 ComboBoxTable 的 OnChange 事件, //所以不用再调用 ComboBoxTableChange(this); break; } } } else { ComboBoxTable->ItemIndex=-1; TableChoose=-1; return; } } } else 第 14 章 餐厅结账管理程序 470 { MessageDlg("找不到该纪录!",mtError, TMsgDlgButtons() << mbOK, 0); } //根据餐桌号对 TableOrdered 进行过滤 TableOrdered->Filter=AnsiString("TableNum = ")+AnsiString(TableChoose) +" AND Status = '未结账'"; TableOrdered->Filtered = true; //将已经有的菜单填入 ListView 中 ListView1->Items->Clear(); TListItem *NewItem; int dishnum; TableOrdered->First(); for(int i=0;iRecordCount;i++) { dishnum=TableOrdered->FieldByName("DishNum")->AsInteger; //根据菜谱编号 dishnum 在 FormClassDishMan->TableDish 中查找对应纪录 FormClassDishMan->TableDish->IndexFieldNames = "DishNum"; FormClassDishMan->TableDish->SetKey(); if(FormClassDishMan->TableDish->FindKey(ARRAYOFCONST((dishnum)))) { TTable *tmpTable=FormClassDishMan->TableDish; NewItem=ListView1->Items->Add(); //菜名 NewItem->Caption=tmpTable->FieldByName("DishName")->AsString; NewItem->SubItems->Add(TableOrdered->FieldByName("Amount")->AsString); NewItem->SubItems->Add(tmpTable->FieldByName("DishPrice")->AsString); NewItem->SubItems->Add(AnsiString(dishnum)); //这一列隐藏的,在视图中看不到 NewItem->SubItems->Add(tmpTable->FieldByName("DisCount")->AsString); } else { MessageDlg("找不到该纪录!",mtError, TMsgDlgButtons() << mbOK, 0); } TableOrdered->Next(); } } } //--------------------------------------------------------------------------- void __fastcall TFormOrder::BitBtnAddClick(TObject *Sender) 第 14 章 餐厅结账管理程序 471 { if(TableChoose == -1) { MessageDlg("请先选择餐桌号!", mtInformation, TMsgDlgButtons() << mbOK, 0); return; } if(TreeViewOrder->Selected->Level != 2) { MessageDlg("请选定一个菜谱名!", mtInformation, TMsgDlgButtons() << mbOK, 0); return; } //根据选择的菜谱名,查找以经点的菜中有没有该菜,如果有,将其数量增一, //如果没有,新增纪录 bool havedish=false; int seldish=FormClassDishMan->TableDish->FieldByName("DishNum")->AsInteger; TableOrdered->First(); for(int i=0;iRecordCount;i++) { if(TableOrdered->FieldByName("DishNum")->AsInteger == seldish) { //数量增一 TableOrdered->Edit(); //设置为可编辑模式 TableOrdered->FieldByName("Amount")->AsInteger +=1; TableOrdered->Post(); ComboBoxTableChange(this); //刷新列表视图的显示 havedish=true; break; } TableOrdered->Next(); } if(!havedish) //需要增加的菜谱名不再已选菜单中 { TableOrdered->Append(); TableOrdered->FieldByName("DishNum")->AsInteger=seldish; TableOrdered->FieldByName("TableNum")->AsInteger=TableChoose; TableOrdered->FieldByName("Amount")->AsFloat=1.0; TableOrdered->FieldByName("Status")->AsString="未结账"; //记录日期、时间 第 14 章 餐厅结账管理程序 472 DateSeparator = '-'; ShortDateFormat = "yyyy/m/d"; TableOrdered->FieldByName("Date")->AsString=DateToStr(Date()); TableOrdered->FieldByName("Time")->AsString=TimeToStr(Time()); TableOrdered->FieldByName("UserID")->AsInteger= FormMain->UserID; TableOrdered->Post(); ComboBoxTableChange(this); //刷新列表视图的显示 } } //--------------------------------------------------------------------------- void __fastcall TFormOrder::BitBtnDelClick(TObject *Sender) { //ListView1->MultiSelect = false; if(ListView1->SelCount != 1 ) //选中一个需要退点的菜名 { MessageDlg("请先从右边选择要退点的菜谱名称!", mtInformation, TMsgDlgButtons() << mbOK, 0); return; } //菜谱编号 int seldish=ListView1->Selected->SubItems->Strings[2].ToInt(); //在 TableOrdered 中查找要退的菜谱编号, //因为 TableOrdered 没有索引,没有 key,所以只能通过循环判断 TableOrdered->First(); for(int i=0;iRecordCount;i++) { if(TableOrdered->FieldByName("DishNum")->AsInteger == seldish) { //如果数量大于 1,则减一,如果数量等于 1,则删除数据库中该条记录 if(TableOrdered->FieldByName("Amount")->AsInteger > 1) { TableOrdered->Edit(); TableOrdered->FieldByName("Amount")->AsInteger -= 1; TableOrdered->Post(); //ComboBoxTableChange(this); ListView1->Selected->SubItems->Strings[0]= ListView1->Selected->SubItems->Strings[0].ToInt() - 1; } else //Amount == 1 第 14 章 餐厅结账管理程序 473 { TableOrdered->Delete(); ComboBoxTableChange(this); } break; //跳出循环 } TableOrdered->Next(); } } //--------------------------------------------------------------------------- 14.4.8 结账 图 14-23 FormPay 窗体界面 与点菜的窗体中一样,FormPay 窗体中对 Pay.db 的操作也是通过本窗体中添加的 TTable 和 TDataSource 组件完成。设置 TablePay 的 DatabaseName 为 Ch14,TableName 为 Pay.db, 设 第 14 章 餐厅结账管理程序 474 图 14-24 FormPay 运行效果 置 DataSourcePay 的 DataSet 为 TablePay。 结账时,先要在组合框 ComboBoxTableInuse(Style 为 csDropDownList)中选择要结账 的餐桌。组合框中列表项的初始化由函数 RefreshComboBox 完成,它根据餐桌管理窗口的 FormTableMan->TableTable 查找数据表中的处于使用状态的餐桌,然后添加道组合框的列表 中。对 RefreshComboBox 的调用放在窗体的 OnShow 事件中,即每次弹出该窗体时,先对组 合框的下拉列表进行更新,以供选择。 选择餐桌后,根据餐桌编号查找 FormOrder->TableOrdered 数据表的内容,将餐桌号为所 选择的餐桌号,且是未结账状态的菜单显示在左侧的列表视图中,然后计算出应付金额,显 示在 TEdit 组件 EditShouldPay 中。 根据实付金额的输入,动态显示找零,这个功能由 EditActuallyPay 的 OnChange 的处理 函数完成。另外,因为应付和找零都是由程序计算所得,不接收用户输入,所以将 EditShouldPay 和 EditCharge 的 ReadOnly 设为 true,且将其 Color 设为 clBtnFace。 TMemo 组件 MemoDescription 用于输入备注信息。三个按钮 BitBtnOK、BitBtnCancel、 BitBtnExit 分别用于确认结账操作、取消结账操作和退出结账界面。 确认结账时,根据三个 TEdit 组件中显示的金额,MemoDescription 中的备注信息以及当 前的系统日期和时间,在 TablePay 中添加结账记录。取消结账操作时,只需要调用一下 FormShow 函数,对界面进行清空和刷新。 窗体 FormPay 的头文件 Unit8.h 中添加如下内容: public: // User declarations __fastcall TFormPay(TComponent* Owner); 第 14 章 餐厅结账管理程序 475 void __fastcall RefreshComboBox(void); int TableChoose; Unit8.cpp 的实现代码如下: //--------------------------------------------------------------------------- #include #pragma hdrstop #include "Unit8.h" #include "Unit7.h" #include "Unit3.h" #include "Unit2.h" #include "Unit1.h" //--------------------------------------------------------------------------- #pragma package(smart_init) #pragma resource "*.dfm" TFormPay *FormPay; //--------------------------------------------------------------------------- __fastcall TFormPay::TFormPay(TComponent* Owner) : TForm(Owner) { TablePay->Open(); } //--------------------------------------------------------------------------- void __fastcall TFormPay::RefreshComboBox(void) { TTable * tmpTable=FormTableMan->TableTable; AnsiString info_str; //从 FormTableMan 中读取所有的餐桌信息,写入下拉菜单中 ComboBoxTableInuse->Items->Clear(); tmpTable->First(); for(int i=0;iRecordCount;i++) { //只显示使用中的餐桌桌号 if(tmpTable->FieldByName("TableStatus")->AsString == "使用") { info_str=tmpTable->FieldByName("TableNum")->AsString; info_str+="-"; info_str+=tmpTable->FieldByName("TableType")->AsString; 第 14 章 餐厅结账管理程序 476 info_str+="-"; info_str+=tmpTable->FieldByName("TableStatus")->AsString; ComboBoxTableInuse->Items->Add(info_str); } tmpTable->Next(); } } //--------------------------------------------------------------------------- void __fastcall TFormPay::FormShow(TObject *Sender) { RefreshComboBox(); ListView1->Items->Clear(); EditShouldPay->Text=""; EditActuallyPay->Text=""; EditCharge->Text=""; MemoDescription->Text=""; BitBtnOK->Enabled=false; BitBtnCancel->Enabled=false; EditActuallyPay->Enabled=false; TableChoose=-1; } //--------------------------------------------------------------------------- void __fastcall TFormPay::ComboBoxTableInuseChange(TObject *Sender) { int pos; if(ComboBoxTableInuse->Text == "") { return; } else { pos=ComboBoxTableInuse->Text.Pos("-"); TableChoose=ComboBoxTableInuse->Text.SubString(1,pos-1).ToInt(); //根据餐桌号对 TableOrdered 进行过滤 FormOrder->TableOrdered->Filter=AnsiString("TableNum = ")+AnsiString(TableChoose) +" AND Status = '未结账'"; FormOrder->TableOrdered->Filtered = true; //将已经有的菜单填入 ListView 中 第 14 章 餐厅结账管理程序 477 ListView1->Items->Clear(); TListItem *NewItem; int dishnum; float Money=0.0; FormOrder->TableOrdered->First(); for(int i=0;iTableOrdered->RecordCount;i++) { dishnum=FormOrder->TableOrdered->FieldByName("DishNum")->AsInteger; //根据菜谱编号 dishnum 在 FormClassDishMan->TableDish 中查找对应纪录 FormClassDishMan->TableDish->IndexFieldNames = "DishNum"; FormClassDishMan->TableDish->SetKey(); if(FormClassDishMan->TableDish->FindKey(ARRAYOFCONST((dishnum)))) { TTable *tmpTable=FormClassDishMan->TableDish; NewItem=ListView1->Items->Add(); //菜名 NewItem->Caption=tmpTable->FieldByName("DishName")->AsString; NewItem->SubItems->Add(FormOrder->TableOrdered->FieldByName("Amount")->AsString); NewItem->SubItems->Add(tmpTable->FieldByName("DishPrice")->AsString); NewItem->SubItems->Add(tmpTable->FieldByName("DisCount")->AsString); //累计需要付的金额 Money+=FormOrder->TableOrdered->FieldByName("Amount")->AsFloat * tmpTable->FieldByName("DishPrice")->AsFloat * tmpTable->FieldByName("DisCount")->AsFloat /100 ; } else { MessageDlg("找不到该纪录!",mtError, TMsgDlgButtons() << mbOK, 0); } FormOrder->TableOrdered->Next(); } //将 Money 显示出来 ,保留两位小数 EditShouldPay->Text=FormatFloat("0.00",Money); BitBtnOK->Enabled=true; BitBtnCancel->Enabled=true; EditActuallyPay->Enabled=true; } } //--------------------------------------------------------------------------- 第 14 章 餐厅结账管理程序 478 void __fastcall TFormPay::BitBtnOKClick(TObject *Sender) { //判断 EditActuallyPay 的内容 if(EditActuallyPay->Text == "") { MessageDlg("请填写【实付金额】!",mtError, TMsgDlgButtons() << mbOK, 0); EditActuallyPay->SetFocus(); return; } if(EditActuallyPay->Text.ToDouble() < EditShouldPay->Text.ToDouble()) { MessageDlg("【实付金额】不能小于【应付金额】!",mtError, TMsgDlgButtons() << mbOK, 0); EditActuallyPay->SetFocus(); return; } //向 TablePay 中写记录 TablePay->Append(); TablePay->FieldByName("TableNum")->AsInteger=TableChoose; TablePay->FieldByName("ShouldPay")->AsFloat=EditShouldPay->Text.ToDouble(); TablePay->FieldByName("ActuallyPay")->AsFloat=EditActuallyPay->Text.ToDouble(); TablePay->FieldByName("Charge")->AsFloat=EditCharge->Text.ToDouble(); //时间日期 DateSeparator = '-'; ShortDateFormat = "yyyy/m/d"; TablePay->FieldByName("Date")->AsString=DateToStr(Date()); TablePay->FieldByName("Time")->AsString=TimeToStr(Time()); TablePay->FieldByName("Description")->AsString = MemoDescription->Text; TablePay->FieldByName("UserID")->AsInteger=FormMain->UserID; TablePay->Post(); //将菜单中的各个菜的状态设为"已结账" int dishnum; FormOrder->TableOrdered->Filter=AnsiString("TableNum = ")+AnsiString(TableChoose) +" AND Status = '未结账'"; FormOrder->TableOrdered->Filtered = true; FormOrder->TableOrdered->First(); //因为 TableOrdered 用了过滤属性,所以更改 Status 之后的记录将不显示 //所以 TableOrdered->RecordCount 会逐渐减小,故,循环中的方法比较特殊 int j=FormOrder->TableOrdered->RecordCount; 第 14 章 餐厅结账管理程序 479 for(int i=0;iTableOrdered->Edit(); FormOrder->TableOrdered->FieldByName("Status")->AsString="已结账"; FormOrder->TableOrdered->Post(); } //设餐桌的状态为"空闲" FormTableMan->TableTable->IndexFieldNames="TableNum"; FormTableMan->TableTable->SetKey(); if(FormTableMan->TableTable->FindKey(ARRAYOFCONST((TableChoose)))) { FormTableMan->TableTable->Edit(); FormTableMan->TableTable->FieldByName("TableStatus")->AsString="空闲"; FormTableMan->TableTable->Post(); } else { MessageDlg("找不到该纪录!",mtError, TMsgDlgButtons() << mbOK, 0); } FormShow(this); //刷新 } //--------------------------------------------------------------------------- void __fastcall TFormPay::BitBtnCancelClick(TObject *Sender) { FormShow(this); //刷新 } //--------------------------------------------------------------------------- void __fastcall TFormPay::BitBtnExitClick(TObject *Sender) { Close(); } //--------------------------------------------------------------------------- void __fastcall TFormPay::EditActuallyPayChange(TObject *Sender) { if(EditActuallyPay->Text != "") { EditCharge->Text=EditActuallyPay->Text.ToDouble() - EditShouldPay->Text.ToDouble(); 第 14 章 餐厅结账管理程序 480 } } //--------------------------------------------------------------------------- 14.4.9 登陆 图 14-25 FormLogin 窗体界面 图 14-26 FormLogin 运行效果 登陆窗体的设计如图 14-25 所示。其中用于输入密码的 TEdit 组件 EditPass 需要将 PasswordChar 属性设为'*'。运行效果如图 14-26 所示。 设计登陆窗体是需要注意,因为设计时一般不会第一个就设计登陆窗体,而一般是设计 了其它功能窗体之后,才设计登陆窗体,而这样的话,第一个设计的窗体会作为其它窗体的 父窗体。在程序运行时,第一个窗体关闭则会终止整个应用程序,而其它窗体关闭则不会。 试图通过设置第一个窗体的 Visible 为 false,FormLogin 的 Visible 为 true 来实现在程序开始 运行时只显示登陆窗体是行不通的。必须将登陆窗体设置为程序运行时的第一个创建的窗体。 第 14 章 餐厅结账管理程序 481 图 14-27 更改创建窗体的顺序 如图 14-27,将 FormLogin 窗体用鼠标拖动到第一个位置,这样程序运行时就会第一个 创建登陆窗体。设置 FormLogin 的 Visible 属性为 true,FormMain 和其它窗体的 Visible 都为 false。当登陆之后,隐藏 FormLogin 窗体,显示 FormMain 窗体,此时,如果关闭 FormMain 窗体,并不会结束应用程序,所以需要在 FormMain 的 OnClose 事件的代码中需要添加 Application->Terminate(),以得到习惯的关闭 FormMain 即结束应用程序的效果。 登陆时,按下确认按钮,即查找 FormEmployeeMan->TableEmployee 中相应 ID 的信息, 比较输入的密码和数据库中的密码字段,登陆成功则根据登陆 ID 的级别设置相应的菜单。 UnitLogin.cpp 代码如下: //--------------------------------------------------------------------------- #include #pragma hdrstop #include "UnitLogin.h" #include "Unit1.h" #include "Unit6.h" //--------------------------------------------------------------------------- 第 14 章 餐厅结账管理程序 482 #pragma package(smart_init) #pragma resource "*.dfm" TFormLogin *FormLogin; //--------------------------------------------------------------------------- __fastcall TFormLogin::TFormLogin(TComponent* Owner) : TForm(Owner) { } //--------------------------------------------------------------------------- void __fastcall TFormLogin::BitBtn1Click(TObject *Sender) { if(EditID->Text != "") { int ID=EditID->Text.ToInt(); FormEmployeeMan->TableEmployee->IndexFieldNames="ID"; FormEmployeeMan->TableEmployee->SetKey(); if(FormEmployeeMan->TableEmployee->FindKey(ARRAYOFCONST((ID))) ) { if(FormEmployeeMan->TableEmployee->FieldByName("Password")->AsString ==EditPass->Text ) { AnsiString rankname; char ranknum; ranknum=FormEmployeeMan->TableEmployee->FieldByName("Rank")->AsString[1]; FormEmployeeMan->TableRank->IndexFieldNames="RankNum"; FormEmployeeMan->TableRank->SetKey(); if(FormEmployeeMan->TableRank->FindKey(ARRAYOFCONST((ranknum))) ) { rankname=FormEmployeeMan->TableRank->FieldByName("RankName")->AsString; } else { MessageDlg("未知级别!",mtError, TMsgDlgButtons() << mbOK, 0); return; //不允许登陆,返回 } //纪录用户 ID FormMain->UserID=ID; FormMain->Caption=AnsiString("餐厅结账管理系统")+ "["+rankname+":"+ID+"]"; //根据用户等级处理显示界面 第 14 章 餐厅结账管理程序 483 if(rankname == "系统管理员") { //所有菜单都显示,不作特殊处理 } else if(rankname == "经理" ) { //不显示数据库维护菜单 FormMain->Menu_M->Visible=false; } else if(rankname == "收银员" ) { //不显示数据库维护和报表 FormMain->Menu_M->Visible=false; FormMain->Menu_R->Visible=false; } else { MessageDlg("尚未定义适合您的功能,请换用管理员、经理、收银员 ID 以便使用相应功能!", mtInformation, TMsgDlgButtons() << mbOK, 0); FormMain->Menu_M->Visible=false; FormMain->Menu_R->Visible=false; FormMain->Menu_O->Visible=false; } FormLogin->Hide(); FormMain->Show(); } else { MessageDlg("密码错误!",mtError, TMsgDlgButtons() << mbOK, 0); EditPass->SetFocus(); } } else { MessageDlg("此 ID 不存在!",mtError, TMsgDlgButtons() << mbOK, 0); EditID->SetFocus(); } } } //--------------------------------------------------------------------------- 第 14 章 餐厅结账管理程序 484 void __fastcall TFormLogin::BitBtn2Click(TObject *Sender) { Application->Terminate(); } //--------------------------------------------------------------------------- 14.5 报表 对于实际应用的程序,一个很重要的功能就是打印,而对于象本章的餐厅管理这样的数 据库程序,对数据库数据的统计和打印输出是必不可少的。 C++Builder 中提供的与报表相关的组件都在组件选项卡的 QReport 页中。它们大多与数 据库联系紧密,可以从各种数据集(如 TTable、TQuery、列表、数组等)中提取数据。 这里不再对各个组件一一讲述其使用,只详细讲述本章的餐厅管理程序中的报表设计过 程,大概描述各个操作的目的。读者如需要详细了解各个组件的使用,请参阅帮助,或者其 它专门的参考书。 餐厅管理程序中,需要设计三个报表,即餐厅职员分类统计报表、菜谱销售分类统计报 表和账单统计报表。其中前两个都是分类统计,使用 Master/Detail 类型的报表,最后一种使 用 List 型的报表。 14.5.1 餐厅职员分类统计报表 图 14-28 餐厅职员分类统计报表设计图 如图 14-28,为职员统计报表的设计图,其中职员按照级别分类。下面分别介绍各个组 件的属性设置。 第 14 章 餐厅结账管理程序 485 首先是两个 TTable 组件,TableRank 和 TableEmployee,设置它们的 DatabaseName 为 Ch14,TableName 分别为 Rank.db 和 Employee.db。添加一个 TDataSource 组件 DataSource1, 设置其 DataSet 为 TableRank。设置 TableEmployee 的 MasterSource 为 DataSource1,IndexName 为 Rank。 然后,需要设置 TableEmployee 的 MasterFields 属性,但是之前需要先编辑 Employee.db 的数据库结构,为其添加一个副索引(如图 14-4 所示),字段选择 Rank。接下来,点击 MasterFields 属性右边的 按钮,在 Field Link Designer 对话窗中选择 Available Index 为 Rank, 选中 Detail Fields 中的 Rank 和 Master Fields 中的 RankNum,单击 Add 按钮,即可,如图 14-29 所示。 图 14-29 Field Link Designer 在窗体中添加 TQuickRep 组件 QuickRep1,设置其 DataSet 为 TableRank。 在 QuickRep1 中添加 5 个 TQRBand 组件,分别命名为 TitleBand、ColumnHeaderBand、 DetailBand、GroupFooterBand、PageFooterBand。添加 TQRSubDetail 组件 QRSubDetail1,添 加 TQRChildBand 组件 ChildBand1。 设置TitleBand的BandType为rbTitle,即作为报表标题。在TitleBang中添加一个TQRLable 组件 QRLabelTitle,调整其大小和字体属性,设置其 Caption 为“餐厅职员分类报表”。 设置 ColumnHeaderBand 的 BandType 属性为 rbColumnHeader,即作为列标题。设置其 LinkBand 属性为 DetailBand,即它里面的标题是针对 DetailBand 中的数据来说的。在其中添 加两个 Lable,分别设其 Caption,如图 14-28。 设置 DetailBand 的 BandType 属性为 rbGroupHeader,即作为每一类的表头。在其中添加 两个 TQRDBText 组件 QRDBTextRankName 和 QRDBTextRankNum,并调整一下它们的字体 属性(作为分类标志的级别信息,应该与其它内容在字体上区别一下),设置这两个组件的 DataSet 为 TableRank,DataField 分别为 RankName 和 RankNum。再为 DetailBand 添加 6 个 第 14 章 餐厅结账管理程序 486 TQRLable 组件,设置它们的 Caption,以及位置等属性,如图 14-28。 图 14-30 职员分类统计报表运行效果 设置 QRSubDetail1 的 DataSet 为 TableEmployee,在此区域中,要根据 DetailBand 中的 级别代码输出 Employee.db 中该级别的所有职员信息。设置 QRSubDetail1 的 FooterBand 属性 为 GroupFooterBand,GroupFooterBand 中显示一个类别中的人员个数,它作为 QRSubDetail1 的脚注。设置 QRSubDetail1 的 HeaderBand 属性为 DetailBand,即 DetailBand 作为其头注。 设置 QRSubDetail1 的 Master 属性为 QuickRep1。在 QRSubDetail1 中添加六个 TQRDBText 组件,分别用于显示职员信息,设置这些组件的 DataSet 为 TableEmployee,并为它们设置相 应的 DataField(如显示 ID 的设置此属性为 ID)。 设置 GroupFooterBand 的 BandType 属性为 rbGroupFooter,即作为一个分组的脚注, QRSubDetail1 中已经设置了 FooterBand 为 GroupFooterBand。在 GroupFooterBand 中添加一 个TQRLable和一个 TQRExpr组件,如图 14-28设置TQRLabel组件的 Caption,对于TQRExpr, 需要设置其计算式,从而统计 QRSubDetail1 中列出的信息个数。点击 TQRExpr 组件中 Expression 属性右边的 按钮,就会弹出计算式编辑窗口,统计列出信息个数的函数为 count。 设置 ChildBand1 的 ParentBand 为 GroupFooterBand,并为 ChildBand1 设置合适的高度。 这个组件的目的是为了在不同的分类列表之间加上一定的空白。 PageFooterBand 作为页面的脚注,设置其 BandType 为 rbPageFooter,在其中添加两个 TQRSysData 组件,如图 14-28,一个用于显示打印时间,一个用于显示页码。QRSysData1 第 14 章 餐厅结账管理程序 487 用于显示时间,设置其 Text 为“Printed”,其 Data 属性为 qrsDate。QRSysData2 用于显示页 码,设置其 Text 为“Page”,Data 属性为 qrsPageNumber。 对于每个分类,最好能用一个边框圈起来,实现起来很简单。设置 DetailBand 的 Frame->DrawLeft、Frame->DrawRight、Frame->DrawTop 属性为 true,设置 QRSubDetail1 的 Frame->DrawLeft 、 Frame->DrawRight 属性为 true ,设置 GroupFooterBand 的 Frame->DrawBottom、Frame->DrawLeft、Frame->DrawRight 属性为 true。通过更改 Frame 中 的 Style 和 Width 可以设置边线的样式和宽度。 最后,将 TableRank 和 TableEmployee 的 Active 属性设为 true,当然,也可以在程序中 利用代码实现。 14.5.2 菜谱销售分类统计报表 如图 14-31,为菜谱销售分类统计报表的设计图,其中各个组件的属性设置跟 14.5.1 节 中一样,所以这里不再详述。 其中需要注意的是 Ordered.db 中没有关键字段,而设置 MasterFields 属性时,需要设置 其中的 DishNum 字段为索引,所以需要在 Ordered.db 中创建副索引。 分类销售总数量统计的 TQRExpr 组件的计算式为 SUM(TableOrdered.Amount)。 报表的运行效果如图 14-32 所示。 图 14-31 菜谱销售分类统计报表设计图 第 14 章 餐厅结账管理程序 488 图 14-32 菜谱销售分类统计报表运行效果 14.5.3 账单统计报表 图 14-33 账单统计报表设计图 账单统计报表的设计图如图 14-33,其中用于统计销售额的 TQRBand 组件的 BandType 第 14 章 餐厅结账管理程序 489 属性为 rbSummary,它里面的 TQRExpr 组件的计算式为 SUM(MasterTable.ShouldPay)。组件 的其它设置参看 14.5.1 节的内容。注意 TablePay 的 Active 属性要设为 true。 账单统计报表的运行效果如图 14-34。 图 14-34 账单统计报表运行效果 14.6 软件封面的制作 餐厅管理程序中有十几个窗体,所以在程序初始化时,需要较长的一段等待时间。一般 对于大型的软件(如 C++Builder6、Microsoft Word 等),都会在这段时间内显示一个软件封 面。这一节,就介绍制作软件封面的技术。 制作软件封面的窗体,如图 14-35,在窗体 FormCover 中添加一个 TImage 组件,为其指 定一个图片(Picture 属性),设置 Image1 的 Stretch 为 true,Align 为 alClient。然后再 Image1 上添加几个透明的(Transparent 属性为 true)TLabel 组件。 设置 FormCover 的 BorderStyle 为 bsNone,即没有窗体边线。设置 Position 属性为 poScreenCenter,即窗体在屏幕中央。 程序初始化操作在 Project1.cpp 文件中,只需要在创建窗体的操作之前先创建并显示封 面窗体,并且在其它窗体创建初始化完成之后删除封面窗体即可。 首先,删除 Project1.cpp 中自动生成的创建封面窗体的代码: 第 14 章 餐厅结账管理程序 490 Application->CreateForm(__classid(TFormCover), &FormCover); 图 14-35 软件封面窗体设计 或者是在 Project Options 对话窗中将 FormCover 窗体从自动创建窗体中删除。 然后,更改 Project1.cpp 的内容,如下,其中黑体部分为添加的代码: //--------------------------------------------------------------------------- #include #include "UnitCover.h" #pragma hdrstop //--------------------------------------------------------------------------- USEFORM("Unit1.cpp", FormMain); ………… USEFORM("UnitCover.cpp", FormCover); //--------------------------------------------------------------------------- WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { try { Application->Initialize(); //-------------------- FormCover=new TFormCover(Application); 第 14 章 餐厅结账管理程序 491 FormCover->Show(); FormCover->Update(); //-------------------- //需要将 FormLogin->Visible 设为 false,否则下面一句代码运行时 //FormLogin就会显示出来 Application->CreateForm(__classid(TFormLogin), &FormLogin); ………… Application->CreateForm(__classid(TQRFormPay), &QRFormPay); //--------------- delete FormCover; //-----这里不需要重设 FormLogin->Visible, //因为程序运行时(下面一句代码)会自动把它设为可见窗体 Application->Run(); } ………… } //--------------------------------------------------------------------------- 封面的运行效果如图 14-36。 图 14-36 软件封面运行效果 第 14 章 餐厅结账管理程序 492 14.7 思考题 z 请编写 TTable 组件各个事件的相应代码,对各种异常情况进行处理,并提醒用户发生了 什么错误 z 如何维护数据库的引用完整性? z 参考第六章关于 TTreeView 和 TListView 组件的使用,为餐厅管理程序中使用到的列表 视图和树视图添加图标,美化界面 z 为程序添加密码更改功能 z 使用 TCCalendar 组件(Samples 页中)编写日历程序,完善主菜单“其它功能”->“日 历”功能
还剩491页未读

继续阅读

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

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

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

下载pdf

pdf贡献者

darkblood

贡献于2014-10-25

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