Eclipse插件开发


ChinaPub 在线购买:http://www.china-pub.com/computers/common/info.asp?id=36806 CSDN 在线阅读:http://book.csdn.net/bookfiles/550/ CowNew 开源团队:http://www.cownew.com 内容介绍: 本书系统地介绍了 SWT、Draw2D、GEF、JET 等与 Eclipse 插件开发相关的基础知识,并且以实际的开发案例来演示 这些知识的实战性应用,通过对这些实际开发案例的学习,读者可以非常轻松地掌握 Eclipse 插件开发的技能,从而开发出满 足个性化需求的插件。 本书以一个简单而实用的枚举生成器作为入门案例,通过该案例读者能学习到扩展点、SWT、JET 等 Eclipse 插件开发的 基本技能;接着对 Eclipse 插件开发中的基础知识进行了介绍,并且对属性视图的使用做了重点介绍;最后以两个具有一定复 杂程度的插件(Hibernate 建模工具和界面设计器)为案例介绍了 SWT、Draw2D、GEF、JET 等技术的综合运用。 第 1 章 Eclipse 插件.... 1 1.1 插件的安装... 1 1.1.1 直接复制安装... 1 1.1.2 links 安装方式... 2 1.1.3 Eclipse 在线安装方式... 3 1.2 内置 JUnit 插件的使用... 5 1.3 可视化 GUI 设计插件 ——Visual Editor 9 1.3.1 Visual Editor 的安装... 9 1.3.2 一个登录界面的开发... 10 1.4 Eclipse 的反编译插件... 21 1.4.1 为什么要反编译... 21 1.4.2 常用 Java 反编译器... 22 1.4.3 反编译不完全的代码的 查看... 23 1.5 WTP 插件使用... 26 第 2 章 Eclipse 插件开发.... 30 2.1 Eclipse 插件开发介绍... 30 2.1.1 开发插件的步骤... 30 2.1.2 Eclipse 插件开发学习资源的 取得... 31 2.2 简单的案例插件功能描述... 31 2.3 插件项目的建立... 33 2.3.1 建立项目... 33 2.3.2 以调试方式运行插件项目... 38 2.4 改造 EnumGeneratorNewWizardPage 类... 39 2.4.1 修改构造函数... 39 2.4.2 修改 createControl 方法... 40 2.4.3 修改 initialize 方法... 41 2.4.4 修改 handleBrowse 方法... 46 2.4.5 修改 dialogChanged 方法... 49 2.4.6 分析 updateStatus 方法... 50 2.4.7 取得界面控件值的方法... 51 2.5 开发枚举项编辑向导页... 51 2.5.1 初始化... 53 2.5.2 相关环境数据的处理... 54 2.5.3 代码生成... 54 2.6 编写代码生成器... 57 2.7 功能演示、打包安装... 64 第 3 章 插件开发导航.... 68 3.1 程序界面的基础——SWT/JFace. 68 3.1.1 SWT 的类库结构... 68 3.1.2 SWT 中的资源管理... 70 3.1.3 在非用户线程中访问 用户线程的 GUI 资源... 70 3.1.4 访问对话框中的值... 72 3.1.5 如何知道部件支持 哪些 style. 73 3.2 SWT 疑难点... 74 3.2.1 Button 部件... 74 3.2.2 Text 部件... 74 3.2.3 Tray. 74 3.2.4 Table. 74 3.2.5 在SWT 中显示 AWT/Swing 对象... 75 3.3 异步作业调度... 76 3.4 对话框... 79 3.4.1 信息提示框... 79 3.4.2 值输入对话框... 80 3.4.3 错误对话框... 81 3.4.4 颜色选择对话框... 82 3.4.5 字体对话框... 83 3.4.6 目录选择对话框... 83 3.4.7 文件选择对话框... 84 3.4.8 自定义对话框及配置保存与 加载... 85 3.5 首选项... 86 3.6 Eclipse 资源 API 和文件系统... 88 3.6.1 资源相关接口的常见方法... 89 3.6.2 方法中 force 参数的意义... 91 3.6.3 资源相关接口的方法使用 示例... 91 3.6.4 在 Eclipse 中没有当前项目... 92 3.7 Java 项目模型... 92 3.7.1 类结构... 92 3.7.2 常用工具类... 94 3.7.3 常用技巧... 95 3.7.4 设定构建路径实战... 100 3.7.5 如何研读 JDT 代码... 105 3.8 插件开发常见的问题... 106 3.8.1 InvocationTargetException 异常的处理... 106 3.8.2 Adaptable 与 Extension Object/Interface 模式... 107 3.8.3 千万不要使用 internal 包... 111 3.8.4 打开视图... 111 3.8.5 查找扩展点的实现插件... 111 3.8.6 项目 nature. 111 3.8.7 透视图开发... 112 3.8.8 关于工具条路径... 113 3.8.9 Eclipse 的日志... 116 第 4 章 属性视图.... 117 4.1 基本使用... 117 4.1.1 IPropertySource 接口说明... 118 4.1.2 对象实现 IPropertySource 接口... 120 4.1.3 对象适配成 IPropertySource 对象... 125 4.2 属性视图高级话题... 128 4.2.1 属性分类... 128 4.2.2 复合属性... 133 4.2.3 常用属性编辑器... 140 4.2.4 自定义属性描述器... 146 第 5 章 开发 Hibernate 插件.... 154 5.1 功能描述... 154 5.2 XML 文件的处理... 158 5.2.1 XML 处理技术比较... 158 5.2.2 Dom4j 的使用... 159 5.2.3 XStream 的使用... 165 5.3 实体模型文件创建向导... 169 5.4 模型的定义和模型文件处理... 176 5.5 实体属性描述器... 187 5.6 实体编辑器... 193 5.6.1 字段的编辑... 193 5.6.2 编辑器基类... 200 5.6.3 实体编辑器核心配置界面... 203 5.6.4 多页实体编辑器... 224 5.7 代码生成... 228 5.7.1 代码生成器接口... 228 5.7.2 代码生成器配置文件... 232 5.7.3 代码生成向导... 235 5.7.4 公共工具类 CommonUtils. 243 5.8 Hibernate 代码生成器... 245 5.8.1 命名策略... 246 5.8.2 工具类... 247 5.8.3 代码生成的 JET 代码... 251 5.9 CowNewStudio 使用实例... 259 第 6 章 基于 GEF 的界面设计工具.... 263 6.1 GEF 简介... 263 6.1.1 Draw2D.. 263 6.1.2 请求与编辑策略... 264 6.1.3 视图与编辑器... 264 6.1.4 GEF 的工作过程... 265 6.2 系统需求... 265 6.2.1 界面设计工具的分类... 265 6.2.2 功能描述... 266 6.3 构建模型... 267 6.4 实现控制器... 275 6.4.1 窗体和组件的控制器... 275 6.4.2 编辑策略... 279 6.4.3 命令对象... 283 6.5 窗体文件创建向导... 287 6.6 组件加载器... 289 6.7 编辑器... 295 6.8 代码生成和构建器... 310 6.8.1 代码生成... 310 6.8.2 构建器... 313 6.8.3 为项目增加构建器... 320 6.9 实现常用组件... 323 6.9.1 标签组件... 323 6.9.2 按钮组件... 327 6.9.3 复选框... 331 6.9.4 编辑框... 336 6.9.5 列表框... 338 6.10 使用演示... 346 前 言 Eclipse 是一款非常优秀的开源 IDE,非常适合 Java 开发,由于支持插件技术,受到了越来越多的开发者 的欢迎。 作为一款优秀的平台,如果我们只是使用 Eclipse 的现有功能进行开发,无疑不能发挥出 Eclipse 的全部 威力,如果能根据需要开发基于 Eclipse 的插件,那么将会大大提高开发效率。现在市场上已经有了几本 Eclipse 的相关书籍,但基本上都是偏重于 Eclipse 的使用,很少有涉及到基于 Eclipse 的插件开发的书籍,即使有讲 述到 Eclipse 插件开发的,其内容也是浅尝辄止,根本没有对有一定复杂程度和实用性的插件开发进行讲解。 Eclipse 的插件体系是非常复杂的,学习门槛也非常高,为了帮助国内开发人员掌握 Eclipse 的插件开发 技术,从而开发出满足自己要求的插件,本书将系统地介绍 Eclipse 插件各方面的知识,并且通过实际的开发 案例来演示这些知识的实战性应用。 书中的对应的 Eclipse 版本为 Eclipse 3.2,可以从 http://www.eclipse.org 网站免费下载。 本书内容安排: 第 1 章介绍常用的 Eclipse插件的安装和使用。第 2 章以一个枚举生成器插件的开发为案例讲解一个简单、 实用的插件的开发步骤。第 3 章介绍 Eclipse 插件开发中常用的基础知识。第 4 章介绍插件对属性视图的支持。 第 5 章以 Hibernate 建模插件为案例讲解有一定复杂程度和实用性的插件的开发。第 6 章以界面设计器插件为 案例讲解基于 GEF 技术的图形插件的开发。 如果您对本书有任何意见和建议,可以发送邮件到 about521@163.com,本书相关的后续资料将会发布到 CowNew 开源团队网站(http://www.cownew.com)中。 杨中科 序言 “自己动手写开发工具”是很多开发人员的梦想,虽然市场上已经有了各种开发工具,但是在一些情况 下还是有编写自己开发工具的需求的: l 使用的编程语言没有合适的开发工具。比如在 Eclipse 出现之前,Python、Ruby、JavaScript 等语言都没 有很好的全面支持代码编写、调试以及重构的开发工具,使用这些语言进行开发非常麻烦。 l 为自己开发的语言配备开发工具。有时我们会开发一款新的开发语言,为了方便使用,我们也需要为其 提供相应的开发工具。 l 为控件库、框架等提供开发工具。Echo2、Tapestry、Spring 等都是非常优秀的产品,但是通过手工编码 的方式使用这些产品仍然是非常麻烦的,如果能配备图形化的开发工具,那么通过简单地鼠标拖拽就可 以快速完成工作。 l 为产品提供二次开发工具。很多公司都有自己的产品,而这些产品一般都提供了二次开发的能力,开发 人员只要进行少量的编码或者配置就可以很轻松的实现针对特定客户的个性化功能。由于二次开发人员 的技术水平相对较差,如果能提供一个图形化的二次开发工具也必将提高二次开发的效率及质量。 对于上面的几种情况,已经有很多开发人员探索着实现了,比如 Boa Constructor 就是一款用 Python 语言 编写的 Python 开发工具,润乾报表提供了用 Swing 技术实现的报表设计器。这 种 所 有功能全盘自己实现的方 式有如下的缺点: l 必须自己处理所有的细节问题。比如实现一个语言的开发工具就必须自己处理语法高亮、语法分析、代 码提示、调试、重构、可视化的界面编辑器以及代码生成等,这些问题的处理对开发人员的要求非常高, 而且开发工作量也非常大。 l 各个工具的差异性非常大,增加了用户的学习成本。 l 不同的工具之间的集成非常困难。由于不同的工具是由各个厂商独立开发出来的,互相之间的集成非常 麻烦,不仅使用的时候需要运行多个工具,而且经常需要在多个实现相似功能的工具之间做出取舍。 Delphi、VS.Net Studio、JBuilder、NetBeans 等都提供了一定的扩展机制,我们只要按照要求编写插件就 能在这些工具中开发扩展功能,但是这些工具提供的扩展功能是非常简单和有限的,我们几乎无法完成编写 开发工具这样复杂的功能。 做为 IDE 界的一匹黑马,Eclipse 在几年内异军突起,很多开源项目或者商业化的产品都提供了相应的 Eclipse 插件,比如 Echo2、GWT、Struts 等开源产品以及 IBM Websphere、Crystal、金蝶、普元等商业公司 的开发工具都基于 Eclipse 进行开发,甚至 Borland 也将新版本的 JBuilder 移植到 Eclipse 上。Eclipse 能够得 到这么多厂商的支持,究其原因有如下几点:免费且开源;开放性;可扩展性强;对开发工具的开发提供了 强大的支持;基于 Eclipse 的产品更专业;各种插件可以组合使用。 免费且开源 大多数开发工具都是按用户数收费的,对于开发人员比较多的公司来说开发工具的支出是一笔不小的费 用,而且基于这些开发工具开发出来的扩展插件在发布的时候也会涉及到授权的问题。Eclipse 是免费使用的, 这样就为公司节省了不小的一笔开支,而且只要遵守 EPL 协议,那么基于 Eclipse 开发的扩展插件可以任意 发布。Eclipse 是开源的,通过研读 Eclipse 的代码,我们能更快的开发出高质量的插件。 开放性 Eclipse 并没有局限于 Java 语言,我们可以开发非 Java 语言的开发插件,比如 Ruby、Python、C/C++、 C#以及 PHP 等语言都有了 Eclipse 上的开发插件。而且 Eclipse 也没有限定插件的应用领域,所以 Eclipse 成 为了很多领域开发工具的基础,不仅 IBM、金蝶、普元等企业级系统开发商选择 Eclipse 做为其开发工具的 基础,而且像风河系统公司、Accelerated 科技、Altera、TI 和 Xilinx 等嵌入式系统公司也将 Eclipse 平台作为 自身开发工具的基础。 可扩展性强 Eclipse 采用微内核架构,其核心只有几兆大小,我们平时使用的代码编辑、调试、查找以及重构等功能 都是以插件的形式提供的。我们不仅可以扩展现有插件,而且还可以提供扩展点,这样其他用户同样可以基 于我们的插件开发扩展插件从而满足用户的个性化需求,这样我们只需要实现我们个性化的功能即可,通用 功能由基础插件来完成。比如我曾经开发过一个 Python 的远程调试插件,由于 PyDev 已经提供了本地调试 的功能,所以我对 PyDev 进行了少量扩展开发就完成了这个插件。 对开发工具的开发提供了强大的支持 Eclipse 提供了新建向导、代码编辑、调试、运行、图形化界面以及代码生成等开发工具常见功能的支持, 这大大简化了一个复杂开发工具的开发。只需数十行代码就可以实现语法高亮、代码提示等代码编辑功能、 只需数百行代码就可以实现调试功能、只需数百行代码就可以实现一个所见即所得的图形化编辑器,这一切 让开发一个专业的开发工具变得如此简单。这样厂商只要按照自己领域相关的逻辑进行定制,其他的基础功 能则由 Eclipse 提供,这 使得厂商能够把更多的精力投入到自己熟悉的业务领域。比如我们要开发一个 Python 的所见即所得的界面绘制工具,那么我们只需要基于 GEF 进行少量开发即可实现一个所见即所得的图形化编 辑器,而生成的 Python 代码的编辑、调试以及重构等功能则由现有的 PyDev 插件来完成。可以想像如果没 有 Eclipse 的话,我们从头开发一个 Python 的图形化编辑器需要我们处理多少的技术难题! 基于 Eclipse 的产品更专业 一个专业的开发工具通常需要考虑很多问题,比如需要考虑被选择对象的属性编辑方式、长时间操作的 进度条展示、编辑窗口的布局方式以及工具选项的配置等问题,这些问题 Eclipse 的开发人员已经替我们考虑 好了,我们开发的插件将自动拥有这些功能,这使得我们的插件显得更加专业。 各种插件可以组合使用 以前每开发一个开发工具,都需要实现代码版本控制等功能,而在 Eclipse 中则已经有了支持 VSS、CVS 和 SVN 等版本控制协议的插件,我们只要实现我们的开发工具即可,这些版本控制插件可以正交的和我们 的插件组合使用,并且用户可以选择任何他们喜欢的版本控制插件,使得我们的工具使用起来更加灵活。 现在市场上已经有了 XML 编辑器、版本控制、UML 绘制工具及 EJB 开发工具等插件,并且这些插件也 有不同的厂商实现的多个版本,这 样 用 户 可以随意挑选他们喜欢的插件,在 同 一个 Eclipse 环境中任意组合这 些插件来完成复杂的功能。 Eclipse 的出现使得 IDE 市场出现了一个新的格局,主流的开发工具都开始向 Eclipse 靠拢,这不仅使得 开发工具的开发变得更容易了,中小型企业甚至个人也能开发一个实用的开发工具出来。这些基于 Eclipse 的开发工具不仅能提高开发效率,而且将用户统一到 Eclipse 平台中,减少了用户的学习成本。相 信 基 于 Eclipse 的插件开发将成为未来开发工具的主流,那么就让我们开始激动人心的 Eclipse 插件开发学习之旅吧! 第2章 Eclipse 插件开发 Eclipse 已经不仅仅是一个开发工具了,它更是一个平台。在 Eclipse 下开发的插件可以不仅限用于 Java 语言,现在已经出现了 C++、Python、C#等语言的开发插件,而且我们还可以把它当成一个应用框架,用它 开发与编程无直接关系的系统,比如信息管理软件、制图软件、聊天软件等。不过目前国内大部分开发人员 还仅仅是把 Eclipse 当成了一个开发工具来使用,没有发挥它的最大潜力。开发人员一直是在网上寻找相应功 能的插件,一旦没有相应的插件或者插件安装失败就抱怨 Eclipse 没有 JBuilder 之类的工具强大。“工欲善其 事,必先利其器”,Eclipse 的插件开发其实并不复杂,我们只要稍加学习,就能开发出满足我们个性化要求 的插件,从而大大提高开发效率。学会 Eclipse 插件开发可在以下几个方面给人们带来方便: l 开发满足用户要求的插件。 l 如果现有的开源第三方插件有一些 bug,我们也可以自己进行修改,而不必依赖于插件的开发者修 补 bug。 l 可以在第三方开源插件的基础上做二次开发,从而使其更能满足个性化要求。 l 在使用一些 Eclipse 插件的时候如果出现问题我们也能更快地发现问题的所在,并快速排除问题。 本章我们将首先介绍插件开发的一些基础知识,然后就以一个具有实用价值的插件为例介绍插件开发的 整个过程,学习完整个例子之后,我们就可以开发一些实用的插件了①。 2.1 Eclipse 插件开发介绍 2.1.1 开发插件的步骤 开发一个插件需要如下几步。 (1) 标识需要进行添加的扩展点以便与插件进行集成。 (2) 根据扩展点的规范来编写扩展代码。 (3) 编写 plugin.xml 文件描述正在提供的扩展以及对应的扩展代码。 如果要完全手动开发插件的话,我们需要自己去查询相应的扩展点、编写扩展点实现代码、编写 plugin.xml 等配置文件,这项工作非常麻烦、非常容易出错,而且插件的测试、部署也非常麻烦,为了简化 插件开发,Eclipse 提供了一个用来开发插件的插件 PDE(Plug-in Development Environment)。 在 PDE 中,每个正在开发的插件都被单个的特殊的 Java 项目所代表,这个项目被称为插件项目。PDE 提供了一个插件创建向导,可以选择各种需要的部分创建插件项目。 在 PDE 中,插件配置工作大部分是在项目多页编辑器中完成的,这个编辑器简化了插件的配置和开发。 插件 Manifest 编辑器用到 3 个文件: META-INF/MANIFEST.MF、plugin.xml 和 build.properties。它允许我们 通过编辑所有必要的属性来描述一个插件,包括它的基本运行时要求、依赖项、扩展和扩展点等。PDE 提供 了一个特别的启动配置类型,允许我们使用它的配置中包含的工作区插件来启动另外一个工作台实例(被称为 运行时工作台),我们可以像调试普通 Java 程序一样调试插件项目。 2.1.2 Eclipse 插件开发学习资源的取得 如果想快速地学习 Eclipse 插件开发的话,我们可以首先学习插件开发入门文档,通过入门项目的开发对 Eclipse 插件开发有一个感性的认识,然后再阅读 Eclipse 帮助文档中有关插件开发的部分(主要在 Platform plug-in Developer Guide、JDT Plug-in Developer Guide、Plug-in Development Environment Guide 这 3 个项目下), 以便对插件开发的知识有进一步了解,然后再阅读一些开源的 Eclipse 插件的源码,学习他人的代码,最后就 可以根据自己的需要尝试进行插件的开发,这个过程经常是迭代的,比如在学习开源插件源码的时候还需要 借助 Eclipse 的帮助文档来查询某个 API 的说明。 Eclipse 插件开发涉及到的知识是比较多的,比如 SWT/JFace、JDTAPI、GEF、EMF 等,插件涉及到的 类也非常多,容 易 使初学者望而生畏。其实 Eclipse 插件开发的学习是循序渐进的,没必要对各个方面都了解 得非常透彻,比如如果不开发图形界面插件,那么就没必要学习 GEF,如果不开发模型相关的插件就没必要 学习 EMF,即便是任何插件开发人员都躲不掉的 SWT/JFace 也没必要全部掌握,只要能用 SWT 编写一个简 单的界面就可以,其他的东西可以一边做一边学。 ① 本章中所有代码位于随书光盘的“代码/ch2/EnumGenerator”下。 2.2 简单的案例插件功能描述 在 Internet 搜索引擎中以“Eclipse 插件开发”为关键字搜索就可以找到数篇讲述 Eclipse 插件开发的经 典入门文章,按着文章中的例子一步一步地做,就可以做出一个显示 Hello world 对话框的例子。这个例子虽 然简单,但是却是一个真正的插件开发项目,通过这个项目我们就可以理解插件的工作原理以及配置文件各 个配置项的作用。 在本节中我们来讲解一个有一定实用价值的简单插件,通过这个插件就可以对 Eclipse 的插件开发有更多 的认识,并且可以立即应用到实际的开发中去。 JDK 1.5 提供的枚举大大简化了我们的开发工作,但是在有的情况下我们暂时还不能使用 JDK 1.5,那么 此时如果需要枚举的话就只能自己写代码,比如: public class CustomerTypeEnum { private String type; public CustomerTypeEnum VIP = new CustomerTypeEnum("VIP"); public CustomerTypeEnum MEMBER = new CustomerTypeEnum("MEMBER"); public CustomerTypeEnum NORMAL = new CustomerTypeEnum("NORMAL"); private CustomerTypeEnum(String type) { super(); this.type = type; } public int hashCode() { final int PRIME = 31; int result = 1; result = PRIME * result + ((type == null) ? 0 : type.hashCode()); return result; } public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; final CustomerTypeEnum other = (CustomerTypeEnum) obj; if (type == null) { if (other.type != null) return false; } else if (!type.equals(other.type)) return false; return true; } } 如果需要很多枚举定义的话就会非常繁琐,因此我们下面就来开发这样一个代码生成器插件来简化这个 操作。 2.3 插件项目的建立 2.3.1 建立项目 插件项目的建立与普通 Java 项目的建立类似。 (1) 在【包资源管理器】中右击,在弹出的快捷菜单中选择【新建】|【项目】命令,在对话框中选择 【插件项目】选项,然后单击【下一步】按钮,在接下来的向导页中输入插件项目的名称、保存位置等信息, 然后单击【下一步】按钮。在接下来的“插件内容”界面中进行如图 2.1 所示的配置。 图 2.1 插件向导 (2) 设置完成后,单击【下一步】按钮,进入如图 2.2 所示的界面。 图 2.2 插件模板选择 (3) 选择【定制插件向导】选项,单击【下一步】按钮,在接下来的“选择模板”界面中,选中【新建 文件向导】复选框,应取消选中其他复选框。单击【下一步】按钮,在“新向导选项”界面中进行如图 2.3 所示的配置后,单击【完成】按钮。 图 2.3 新建向导 (4) 完成以后的项目结构如图 2.4 所示。 图 2.4 插件项目结构 下面看一下各个主要文件的作用。 (1) plugin.xml 这个文件是插件清单文件,它定义了此插件项目中所有的插件: 标记 plugin 内可以定义多个 extension 标记,每个标记表示一个对扩展点的扩充,比如我们这里扩展的是 org.eclipse.ui.newWizards 扩展点,也就是“新建向导”扩展点;category 定义的是对这个扩展点的归类;wizard 标记是 org.eclipse.ui.newWizards 扩展点自定义的格式,name 属性定义的是显示的名称,icon 属性定义的是向 导的图标,category 属性代表此向导的分类,对向导分类可以使得向导看起来更清晰,比如图 2.5 中的 CSS、 HTML 等就属于 Web 分类下。 图 2.5 向导的分类 class 属性表示此扩展点对应的实现类,大部分扩展点的实现都需要编写实现代码,因此需要这个属性来 指定此扩展点使用的是哪个类;id 属性定义的是此 wizard 的唯一标识,作者的习惯是定义成和 class 一样, 这样一般就不会与其他插件的唯一标识冲突了。 需要注意 extension 标记内的 category、wizard 等标记是“org.eclipse.ui.newWizards”扩展点特有的标记, 也就是其他扩展点很可能没有这些标记。这些特有的标记是 Eclipse 供不同的扩展点用来进行属性定义的,这 样灵活性就更加好,每种不同的插件的扩展点的标记定义格式都可以在 Eclipse 帮助文档中找到。 plugin.xml、build.properties 和 MANIFEST.MF 是插件项目中重要的配置文件,共同配置了插件的不同方 面的信息,双击其中任何一个文件都会打开此项目的配置编辑器,3 个文件的配置都在这同一个编辑器中完 成,如图 2.6 所示。 这个编辑器一共有 9 个选项卡,分别是【概述】、【依赖项】、【运行时】、【扩展】、【扩展点】、【构建】、 【MANIFEST.MF】、【plugin.xml】、【build.properties】。其中【概述】、【依赖项】、【运行时】中配置的是 “MANIFEST.MF”文件的内容,【扩展】、【扩展点】中配置的是 plugin.xml 文件中的内容,【构建】配置的 则 是 build.properties 中 的内容,我们既可以在前面这些可视化编辑界面中进行配置,也可以在 【MANIFEST.MF】、【plugin.xml】、【build.properties】这几个选项卡中直接修改配置文件。建议尽量使用可视 化编辑界面来进行配置,这样可以减少很多错误。 在后面的章节中我们会介绍这些配置项的意义。 图 2.6 插件配置编辑器 (2) Activator.java Activator.java 起着此插件的生命周期控制器的作用。 【代码 2-1】插件生命周期控制器: public class Activator extends AbstractUIPlugin { // The plug-in ID public static final String PLUGIN_ID = "EnumGenerator"; // The shared instance private static Activator plugin; public Activator() { plugin = this; } public void start(BundleContext context) throws Exception { super.start(context); } public void stop(BundleContext context) throws Exception { plugin = null; super.stop(context); } /** * Returns the shared instance * @return the shared instance */ public static Activator getDefault() { return plugin; } /** * Returns an image descriptor for the image file at the given * plug-in relative path * * @param path the path * @return the image descriptor */ public static ImageDescriptor getImageDescriptor(String path) { return imageDescriptorFromPlugin(PLUGIN_ID, path); } } 在 Eclipse 3.2 以前的版本中,此类习惯于被命名为**Plugin.java,它使用了单例模式,需 要 通过 getDefault 方法得到此类的实例。当插件被激活的时候 start 方法会被调用,插件可以在这个方法中编写初始化的代码, 当插件被关闭的时候 stop 方法则会被调用。 类中还定义了一个静态方法 getImageDescriptor,用于得到插件目录下的图片资源。 在 Activator 的父类中还定义了很多有用的方法,在这里我们简要地列出常用的一些方法,在 后面的章节 中会展示这些方法的用途。 l getDialogSettings:得到对话框配置类实例。 l getPreferenceStore:得到首选项配置的储存类实例。 l getWorkbench:得到工作台。 l getLog:得到日志记录类实例。 (3) EnumGeneratorNewWizard.java、EnumGeneratoreNewWizardPage.java 这个两个文件是向导界面的实现代码。为什么是两个文件呢?一个向导对话框通常有不止一个界面,因 此整个向导和每个向导界面要分别由不同的类来维护。 EnumGeneratorNewWizard 是 维护所有界面的向导类,在这个例子中只有一个界面,即 EnumGeneratoreNewWizardPage。 插件模板向导生成的 EnumGeneratorNewWizard.java、EnumGeneratoreNewWizardPage.java 这两个类文件并 不能完全满足我们的要求,需要进行修改,因此这里暂时不讲解类中的代码。 2.3.2 以调试方式运行插件项目 在插件项目上右击,在 弹 出的快捷菜单中选择【调试方式】|【Eclipse 应用程序】命令(或运行时工作台), Eclipse 就会启动另一个 Eclipse 实例。 在这个新启动的 Eclipse 中新建的一个 Java 项目,名称为 EnumGenTest,并创建一个源文件夹,在源文 件夹下创建包 com.cownew.enumtest,在包 com.cownew.enumtest 上右击,选择【新建】|【其他】命令,这样 就可以在 EnumGenerator 分组下看到刚才创建的“枚举创建向导”了,如图 2.7 所示。 图 2.7 新建向导页 选择【枚举创建向导】选项,单击【下一步】按钮,进入如图 2.8 所示的界面。 图 2.8 枚举创建向导 这个界面在类 EnumGeneratoreNewWizardPage 中定义。Container 用来指定生成的文件存放在哪个容器下 边(容器是进行 Eclipse 插件开发时要弄懂的概念,这里不详细说明,可以暂时认为源文件夹、普通文件夹、 项目根目录都是容器),单击 Browse 按钮,在弹出的对话框中可以选择源文件夹、项目根目录,甚至还可以 选择输出路径,显然这不满足我们的要求,因为我们生成的枚举类只能放在文件夹下,因此需要对其进行改 造。枚举项目结构如图 2.9 所示。 选择 src/com/cownew/enumtest,然后单击【确定】按钮,在 File name 中输入 TestEnum.java,单击【完 成】按钮,向导为 TestEnum.java 填充了如下内容“This is the initial file contents for *.Java file that should be word-sorted in the Preview page of the multi-page editor”。 图 2.9 枚举项目结构 2.4 改造 EnumGeneratoreNewWizardPage 类 这个类有很多地方不能满足我们的要求,下面一一地进行修改。 2.4.1 修改构造函数 代码 2-2 是修改以后的 EnumGeneratoreNewWizardPage 类的构造函数。 【代码 2-2】修改以后的构造函数: public EnumGeneratoreNewWizardPage(ISelection selection) { super("wizardPage"); setTitle("Multi-page Editor File"); setDescription("This wizard creates a new file with *.Java extension that can be opened by a multi-page editor."); this.selection = selection; } 在构造函数中首先调用父类的构造函数,然后调用 setTitle 为向导页设定标题,调用 setDescription 方法 为页设置描述信息,最后把代表用户选择项的 ISelection 赋值给 selection 私有变量。 向导的标题、向导页的标题、向导页的描述是不同的,如图 2.10 所示。 图 2.10 向导不同的提示显示 “向导的标题”是一个向导窗口的标题,这个标题在这个向导的生命周期内是不变的;“向导页的标题” 是向导中每一页的标题,它标明了此页的作用,“向导页的描述”是对此页功能的进一步解释。 2.4.2 修改 createControl 方法 createControl 方法是 Override 父类的一个方法,框架会调用此方法绘制向导页界面。 【代码 2-3】修改后的 createControl 方法: public void createControl(Composite parent) { Composite container = new Composite(parent, SWT.NULL); GridLayout layout = new GridLayout(); container.setLayout(layout); layout.numColumns = 3; layout.verticalSpacing = 9; Label label = new Label(container, SWT.NULL); label.setText("&Container:"); containerText = new Text(container, SWT.BORDER | SWT.SINGLE); GridData gd = new GridData(GridData.FILL_HORIZONTAL); containerText.setLayoutData(gd); containerText.addModifyListener(new ModifyListener() { public void modifyText(ModifyEvent e) { dialogChanged(); } }); Button button = new Button(container, SWT.PUSH); button.setText("Browse..."); button.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent e) { handleBrowse(); } }); label = new Label(container, SWT.NULL); label.setText("&File name:"); fileText = new Text(container, SWT.BORDER | SWT.SINGLE); gd = new GridData(GridData.FILL_HORIZONTAL); fileText.setLayoutData(gd); fileText.addModifyListener(new ModifyListener() { public void modifyText(ModifyEvent e) { dialogChanged(); } }); initialize(); dialogChanged(); setControl(container); } 我们不能直接在 createControl 传递过来的 parent 上绘制界面,因为这个 parent 代表所有向导页面(包括向 导选择页面)的父 Composite,如果直接在这个 parent 上绘制界面会导致界面显示混乱,所以要调用“Composite container = new Composite(parent, SWT.NULL);”创建一个新的 Composite。 接下来为这个 container 设定布局管理器,在插件项目中采用 GridLayout 是比较方便的。GridLayout 把界 面分成网格,控件都放在这些网格中,与 Swing 的 GridLayout 不同的地方就是它不仅可以为每一列设定不同 的宽度,而且还可以让一个控件占据多个网格。由于插件项目中的界面大部分都是规矩的表单式排列,因此 采用 GridLayout 布局管理器是比较好的。因此这里采用 GridLayout 作为布局管理器,并设定其为 3 列。 创建标记控件、按钮控件、文本框控件等,并为它们设定合适的 GridData。需要注意的是每个控件的 GridData 不能与其他控件共享,也就是说即使两个控件使用的 GridData 全部一样,也要为它们各创建一个 GridData。 在创建控件的时候,给 containerText 控件和 fileText 都增加了 ModifyListener,当控件内容改变的时候, 就会去调用 dialogChanged 方法。我们给【浏览】按钮增加了监听器,这样当按钮被按下的时候 handleBrowse 方法就会被调用以弹出容器选择对话框。 为了减少复杂性,这里指定 fileText 为不可手工编辑。接着调用 initialize 方法进行一些初始化操作,然 后调用 dialogChanged 方法,最后调用 setControl 方法将 container 设定为本向导页的页面。 2.4.3 修改 initialize 方法 initialize 方法被 createControl 方法调用来初始化控件,为了适应需求,我们进行如下的修改。 【代码 2-4】修改后的 initialize 方法: private void initialize() { if (selection != null && selection.isEmpty() == false && selection instanceof IStructuredSelection) { IStructuredSelection ssel = (IStructuredSelection) selection; if (ssel.size() > 1) return; Object obj = ssel.getFirstElement(); if (obj instanceof IResource) { IContainer container; if (obj instanceof IContainer) container = (IContainer) obj; else container = ((IResource) obj).getParent(); containerText.setText(container.getFullPath().toString()); } } fileText.setText("*.Java"); } 这段代码用来把打开向导界面之前在【包资源管理器】视图中选中的对象的容器作为【容器】控件的初 始值,并为 fileText 控件赋初值。 基础知识: ① IResource、IContainer、IContainer、IFolder、IProject、IWorkspaceRoot、IFile 图 2.11 是这些接口的类型层次图(在 IResource 上右击,在弹出的快捷菜单中选择【打开类型层次结构】 命令)。 图 2.11 Eclipse 的资源类继承图 上面这个结构图就代表了 Eclipse 中的所有资源。其中 IFolder 代表文件夹,IProject 代表项目, IWorkspaceRoot 代表工作空间根目录。因为这 3 个资源都可以包含下级资源,所以为它们抽象一个公共接口 IContainer 出来。与 IContainer 同级的 IFile 代表文件。为 IFile 和 IContainer 再抽取一个公共接口 IResource 出来。 ② IStructuredSelection 被选择对象用ISelection来表示,但是ISelection表达的内容太少,因此继承一个子接口 IStructuredSelection 出来,通过这个接口可以得到被选择的对象,而且支持多选。 在 initialize 方法中首先判断 selection 是不是不为空、是不是实现了 IStructuredSelection 接口。请注意, 如 果 在 资 源 视图的某个节点上能通过右键菜单弹出这个向导的话那么 selection 一 定 实 现 了 IStructuredSelection 接口。如果选择了一个对象并且被选择的对象实现了 IResource 接口的话,则继续判断。 如果被选择的对象实现了 IContainer 接口(即被选择的对象是文件夹、项目或者工作空间根目录)的话,就 调用这个 IContainer 的 getFullPath 方法把容器的路径填充到“容器”控件中。如果被选择对象没有实现 IContainer 接口,也就是文件的话,那么其父容器一定实现了 IContainer 接口,所以这里把其父容器的 FullPath 填充到“容器”控件中。 这段代码需要被改造,因为它的实现是把选择对象的容器路径赋值给控件,而我们的要求是把被选择包 的包名赋值给控件。 我们需要判断的是被选择的对象是不是 Java 包。在 JDT 中用 IPackageFragment 类代表的包,它定义在 org.eclipse.jdt.core 下。在文件的 import 部分加入“import org.eclipse.jdt.core. IPackageFragment;”,我们会发现 系统报错,说找不到 IPackageFragment 接口。原来 IPackageFragment 所在的包没有被放到项目的类路径中。 那么如何把包放到类路径中呢?您也许会说,找到相应的 jar 包,然后加入到项目的构建路径中不就可以了 吗? 这是初学插件开发的人员常犯的错,他们经常会问这样的问题:我做的插件在 Eclipse 中开发的时候没有 编辑错误,可是我为什么就运行不了呢? 这是因为 Eclipse 插件的类引用机制比较特别。要弄明白这个问题,首先要弄清楚“插件依赖”的概念。 一个插件不依赖于其他插件是几乎不可能的事情,只有“站在巨人的肩膀上”才能更快更好地做出一个 有实用价值的插件。我们写程序时依赖的一些组件通常都是以 jar 包的形式提供的,比如 XML解析用的dom4j、 日志工具 log4j 等,使用这些包的时候只要将 jar 包加入构建路径就可以了。Eclipse 插件则不同,Eclipse 插 件是有生命周期的,也就是说 Eclipse 插件的 jar 包(Eclipse 插件并不一定以 jar 包形式发布,这里这样说是为 了方便描述)是“活的”,而普通 jar 包是“死的”。我们在开发插件的时候,只要指定此插件依赖的插件的标 识 id 即可,无需知道此插件对应的 jar 包,Eclipse 会自动加载此插件对应的 jar 包。在插件工程中,有个名 字为“插件依赖项”的库列表,如图 2.12 所示。 图 2.12 插件依赖项 这个列表是只读的,无法向其中加入内容,此库列表中的 jar 包是由 PDE 读取插件项目的依赖项以后根 据依赖项的标识自动加载的,只是起到了方便开发的作用,在实际运行的时候并不一定会引用这些列表中所 列的这些包,而且此工程拿到其他版本的 Eclipse 中打开的时候,这个列表中的内容也会随着变化。 那么如何在开发环境中添加依赖项呢? 双击 plugin.xml 打开插件配置编辑器,切换到【依赖性】页,单击【添加】按钮,如图 2.13 所示。 图 2.13 添加依赖项 在弹出的【选择插件】窗口中可以直接输入插件的标识 id,也可以在下方的插件列表中选择。那么如何 知道我们依赖的插件的标识 id 呢?第一种方式就是老老实实地去查 Eclipse 帮助文档,另一种方式就是猜测。 Eclipse 中标准插件的命名是很有规律的,每个不同的插件都放在不同的包中,此插件也以此包作为标识 id, 这样就可以避免冲突。所以当我们要引用一个类的时候,只要尝试着将类的包路径输入【选择插件】文本框 中,然后一级一级地排除包,直到有和下方的插件列表符合的为止。比如接口 IPackageFragment 在包 org.eclipse.jdt.core 下,我们在【依赖性】选项卡中单击【添加】按钮,在【选择插件】文本框中输入 “org.eclipse.jdt.core”就立即可以看到有两个相符项了,如图 2.14 所示。 图 2.14 选择依赖插件 选择第一项,单击【确定】按钮,将此插件引入。再来查看“插件依赖项”,可以看到 “org.eclipse.jdt.core_***.jar”已经被引入构建路径了,如图 2.15 所示。 图 2.15 动态生成的依赖 Jar 包 编写如下代码: if (obj instanceof IPackageFragment) { IPackageFragment pckFragment = (IPackageFragment)obj; containerText.setText(pckFragment.getElementName()); } 运行插件项目,在一个包上右击,运行此向导,可以发现选中的包名被自动填到了【包】文本框中,如 图 2.16 所示。 图 2.16 选择被创建文件所在的包 但是如果我们在一个 Java 文件夹上右击,运行向导的时候,Java 文件所在包的包名就不会自动填充到文 本框中了。可以断定 Java 文件不是 IPackageFragment 类型了,可 是 到底 是 什么类型呢?查帮助文档?到网上 搜索?到 BBS 中发帖提问?当然不用。这些都来得太慢了,程序员最不缺乏的就是探索精神。 以调试方式启动 Eclipse 插件工程,在“if (obj instanceof IPackageFragment)”一句处设置断点,在被调试 的 Eclipse 中的工程下选择一个 Java 文件,右击,选择“枚举创建向导”,单击【下一步】按钮,此时程序就 会在刚才添加的断点处暂挂。选择变量“obj”,右击,在弹出的快捷菜单中选择【检查】命令,此时就会显 示出此变量的类型等信息,如图 2.17 所示。 从图中可以看出 obj 的类型是 CompilationUnit。 根据实验结果来完善代码,添加如下代码: else if(obj instanceof CompilationUnit) { CompilationUnit cu =(CompilationUnit)obj; containerText.setText(cu.getParent().getElementName()); } 图 2.17 调试视图中查看变量类型 这里这样写是没有错误的,但是在 Eclipse 的插件开发中要尽量基于接口编程,CompilationUnit 是实现了 ICompilationUnit 接口的实现类,因此我们要修改代码如下: else if(obj instanceof ICompilationUnit) { ICompilationUnit cu =(ICompilationUnit)obj; //cu 的父容器一定是一个包,所以直接通过 cu.getParent()得到 Java //文件所在的包 containerText.setText(cu.getParent().getElementName()); } ICompilationUnit 在 两 个地方都有定义,分别是“org.eclipse.jdt.core ” 包 下 和 “ org.eclipse. jdt.internal.compiler.env”包下的,要记住这里使用的是第一个。 2.4.4 修改 handleBrowse 方法 当用户单击【浏览】按钮的时候,handleBrowse 方法会被调用以弹出一个容器选择对话框,并把用户选 择的值填充到文本框中。这是不符合我们的需求的,我们的需求是弹出一个包选择对话框,并把用户选择的 值填充到文本框中。 问题的焦点就集中到了如何弹出一个包选择对话框了。如果要自己实现的话需要处理的问题就太多了, 好 在 JDT 为 我们提供了一个工具类 org.eclipse.jdt.ui.JavaUI ,这个工具类中有一个静态方法 createPackageDialog,调用这个方法就可以创建一个包选择对话框。通过查询帮助文档得知 JavaUI 定义在插 件“org.eclipse.jdt.ui”中,因此在使用这个类之前,首先要将“org.eclipse.jdt.ui”加入插件的依赖项。 createPackageDialog 方法有 4 个重载方法,因为我们要创建一个可以选择工程中所有包的包选择对话框, 我们最终敲定使用: public static SelectionDialog createPackageDialog(Shell parent, IJavaProject project, int style) 我们可以把当前向导页的 Shell 传递给第 1 个参数,第 3 个参数传递一个风格就可以。难点在第 2 个参 数,因为它要我们传递要对哪个 Java 项目创建对话框,这个 IJavaProject 接口就代表了一个 Java 项目。 我们现在运行的类是在一个界面中,因此取得外界信息的唯一方式就是构造函数传递过来的 ISelection。 我们在一个 Java 项目中启动“枚举创建向导”之前,一般都会选中项目中的某些元素,比如包、Java 文件、 项目等,我们只能尝试通过它们去突破了。打开熟悉的 ICompilationUnit 接口,来看看它有哪些方法。 在 ICompilationUnit 接口源码中,按 Ctrl+O 快捷键打开此类的类型成员,如图 2.18 所示。 图 2.18 ICompilationUnit 的成员 保持这个提示框不关闭,再次按 Ctrl+O 快捷键打开此类的所有继承的成员,如图 2.19 所示。 图 2.19 ICompilationUnit 以及父接口的成员 在这个列表中发现了我们要寻找的目标:getJavaProject,单击此方法就会转到此方法的声明处,原来这 个方法定义在 IJavaElement 接口中。IJavaElement 是 Java 工程中所有 Java 特有元素(包、源文件、Java 工程 等)的基础接口。选择 IJavaElement,右击,在弹出的快捷菜单中选择【打开类型层次结构】命令,如图 2.20 所示显示出了 IJavaElement 的类体系。 图 2.20 IJavaElement 的类继承图 根据名字可以看出:IClassFile 表示.class 文件,IJavaProject 代表 Java 工程等,这些就代表了所有 Java 项目中能选择的元素。 经过一番简单分析,得到当前 Java 项目的方法完成。 【代码 2-5】得到当前 Java 项目: private IJavaProject getCurrentJavaProject() { if (selection != null && selection.isEmpty() == false && selection instanceof IStructuredSelection) { IStructuredSelection ssel = (IStructuredSelection) selection; Object obj = ssel.getFirstElement(); if(obj instanceof IJavaElement) { return ((IJavaElement)obj).getJavaProject(); } } return null; } 其他的功能都好实现,我们可以直接参考已完成的 handleBrowse 方法。 【代码 2-6】handleBrowse 方法: private void handleBrowse() { IJavaProject JavaProject = getCurrentJavaProject(); if(JavaProject==null) { MessageDialog.openWarning(getShell(), "error", "请在 Java 项目内运行此向导!"); } SelectionDialog dialog = null; try { dialog = JavaUI.createPackageDialog(getShell(),JavaProject, IJavaElementSearchConstants.CONSIDER_REQUIRED_PROJECTS); } catch (JavaModelException e1) { MessageDialog.openWarning(getShell(), "error", e1.getMessage()); e1.printStackTrace(); } if (dialog.open() != Window.OK) { return; } IPackageFragment pck = (IPackageFragment) dialog.getResult()[0]; if (pck != null) { containerText.setText(pck.getElementName()); } } 2.4.5 修改 dialogChanged 方法 当两个文本框中的内容发生变化的时候就调用 dialogChanged 方法,在这个方法中校验界面控件状态是 否合法,并给出提示信息。 【代码 2-7】dialogChanged 方法: private void dialogChanged() { String pckName = packageText.getText(); String fileName = fileText.getText(); if (pckName==null||pckName.length() == 0) { updateStatus("请指定包"); return; } if (fileName==null||fileName.length() == 0) { updateStatus("请输入文件名"); return; } if (fileName.replace('\\', '/').indexOf('/', 1) > 0) { updateStatus("文件名不合法"); return; } int dotLoc = fileName.lastIndexOf('.'); if (dotLoc != -1) { String ext = fileName.substring(dotLoc + 1); if (ext.equalsIgnoreCase("Java") == false) { updateStatus("文件扩展名必须是 \"Java\""); return; } } updateStatus(null); } 由于包文本框是不可手工编辑的,所以此处忽略了对包文本框的合法性校验。判断文件名是否以 .java 结尾时,最好使用正则表达式,不过此处为了方便我们就沿用了自动生成的代码。 2.4.6 分析 updateStatus 方法 【代码 2-8】updateStatus 方法: private void updateStatus(String message) { setErrorMessage(message); setPageComplete(message == null); } 这个方法非常简单,就是把传过来的校验信息显示出来,并决定【下一步】按钮是否可用。调用 setErrorMessage 方法设置向导页的错误信息,而 setPageComplete 方法用来设置【下一步】按钮是否可用。 2.4.7 取得界面控件值的方法 【代码 2-9】取得界面控件值: public String getPackageName() { return packageText.getText(); } public String getFileName() { return fileText.getText(); } 2.5 开发枚举项编辑向导页 这个插件的向导目前只能设定文件保存在哪个包下、文件名是什么,我们还缺少一个指定这个枚举类有 哪些项的向导界面。 因为本节的目的在于快速帮助我们掌握一个简单的实用插件的开发,因此将此向导页简化:整个向导页 只有一个多行文本框,用户在多行文本框中每行输入一个枚举项的名字就可以;我们对输入文本框的内容的 校验也做简化,只校验内容是否为空及各项是否重复,不校验枚举项的命名是否合法。 (1) 首先创建向导页类 所有的向导页类都直接或间接地从 org.eclipse.jface.wizard.WizardPage 继承,因此我们新建一个从 WizardPage 继承的 EnumGenItemDefWizardPage 类。 (2) 画界面 这个界面非常简单,只有一个多行文本框,并且此多行文本框充满整个页面。因此我们对此页面采用 FormLayout 页面布局管理器。FormLayout 是 SWT 2.0 中新增加的布局管理器,在其中可以设定控件与容器 以及容器与容器之间的“附着关系”,这样界面控件就会随着界面大小的变化自动伸展,从而不会造成控件 之间的错位。 【代码 2-10】枚举项编辑向导页界面初始化: public void createControl(Composite parent) { Composite container = new Composite(parent, SWT.NULL); FormLayout layout = new FormLayout(); container.setLayout(layout); FormData data = new FormData(); data.top = new FormAttachment(0, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); data.bottom = new FormAttachment(100, 0); txtItem = new Text(container,SWT.MULTI|SWT.WRAP|SWT.V_SCROLL); txtItem.setLayoutData(data); txtItem.addModifyListener(new ModifyListener(){ public void modifyText(ModifyEvent e) { dialogChanged(); } }); setControl(container); } 代码解析: l FormData 的实例 data 的作用是保证无论界面如何缩放,文本框控件到容器四周的距离都是 0。 l Swing 中一个文本框本身是不带滚动条的,如果要它出现滚动条,那么必须把文本框放入 JScrollPane 控件中。而 SWT 中则无需如此,如果要其有垂直滚动条,只要指定其具有 SWT.V_SCROLL 风格 就可以了,如果要使其有水平滚动条,只要指定其具有 SWT.H_SCROLL 风格就可以了。 l 文本框控件中 SWT.MULTI、SWT.WRAP 两种风格分别代表多行和自动换行。 (3) dialogChanged 方法 在 createControl 方法中,我们为 txtItem 控件增加了一个修改监听器,当用户在文本框中敲入内容的时候 dialogChanged 方法就会被调用。我们在这个方法中主要完成文本框中内容合法性的校验,代码如下。 【代码 2-11】dialogChanged 方法: private void dialogChanged() { String strItems = txtItem.getText(); if(strItems==null||strItems.trim().length()<=0) { updateStatus("请输入枚举项!"); return; } String[] itemArray = strItems.split(LINESEPRATOR); Set set = new HashSet(); for (String item : itemArray) { if(item==null||item.trim().length()<=0) { continue; } if(set.contains(item)) { updateStatus("项重复:"+item); return; } updateStatus(null); set.add(item); } } 需要注意的是在验证用户输入的枚举项是否重复的时候,运用了一个小技巧,首先把文本框中的字符串 按 照 行分隔符分割(LINESEPRATOR 是 一个常量:static final String LINESEPRATOR = System.getProperty("line.separator")),这样就可以得到一个个的枚举项了;然后创建一个集合(Set),遍历这些 枚举项,判断每个项是否已经在集合中存在,如果存在则证明有重复,否则将此项存入这个集合中。这样做 是比较高效的做法,其时间复杂度为 O(n)。 (4) 将此页面加入向导 前面所做的工作都是在定义页面,而 没 有 将 页面加入向导的代码。我们只要为 EnumGeneratorNewWizard 加入 addPages 方法,在最后的位置加入“addPage(new EnumGen- ItemDefWizardPage());”即可。 EnumGeneratorNewWizard 从 Wizard 类继承,并实现了 INewWizard 接口。它是插件的“管家”,负责向 导页的初始化、相关环境数据的处理以及向导单击“完成”之后的业务处理。 2.5.1 初始化 初始化部分主要完成两个工作:添加向导页、将环境输入保存起来备用。在向导页中添加页面必须通过 在 addPages 方法中调用 addPage 方法进行。 【代码 2-12】添加向导页面: public void addPages() { genPage = new EnumGeneratoreNewWizardPage(selection); addPage(genPage); itemDefPage = new EnumGenItemDefWizardPage(); addPage(itemDefPage); } 2.5.2 相关环境数据的处理 在一个向导页启动的时候会调用 public void init(IWorkbench workbench, IStructured- Selection selection)方 法将工作台、当前选择对象传送给向导,由于我们只用到当前 selection,所以只要保存 selection 就可以了。 2.5.3 代码生成 单击向导的【完成】按钮以后,performFinish 方法就会被调用,我们可以在 performFinish 方法中进行代 码生成和保存到磁盘上的操作。 (1) 首先读取两个向导界面中的配置参数: final String packageName = genPage.getPackageName(); final String fileName = genPage.getFileName(); final Set itemDefSet = itemDefPage.getEnumItems(); final IPackageFragmentRoot srcFolderPck = genPage .getPackageFragmentRoot(); final IPackageFragment pckFragment = srcFolderPck .getPackageFragment(packageName); EnumGeneratoreNewWizardPage 从 NewContainerWizardPage 继承,NewContainerWizardPage 中 的 方法 getPackageFragmentRoot 用来取得用户选择的源文件夹,它返回的类型是 IPackageFragmentRoot。 IPackageFragmentRoot 并不仅仅代表源文件夹,它是一组 IPackageFragment 的根,所以它既可以是文件 夹,也可以是 jar 包或者 zip 包。那么什么又是 IPackageFragment 呢?通俗地说,IPackageFragment 代表 Java 中的“包”,但是和“包”又有区别,比如源文件夹 src 中的包 com.cownew.demo 和 Jar 包中的 com.cownew.demo 的名称相同,因此它们是同一个“包”,但是由于这两个包在不同的 IPackageFragmentRoot 下,所以它们是不 同的 IPackageFragment。通过 IPackageFragmentRoot 得到其下某个 IPackageFragment 的方法非常简单,只要 调用 IPackageFragmentRoot 的 getPackageFragment,并把包名作为参数传递进去就可以了。 (2) 由于各种原因,保存代码文件的时间可能会比较长,因此我们为代码生成添加了进度对话框。JFace 对进度对话框提供了很好的支持,我们只要创建一个实现了 IRunnableWithProgress 接口的类的实例,把要运 行的任务放到 IRunnableWithProgress 的 run 方法中即可,代码如下: IRunnableWithProgress op = new IRunnableWithProgress() { public void run(IProgressMonitor monitor) throws InvocationTargetException { try { doFinish(pckFragment, packageName,fileName, monitor, itemDefSet); } catch (CoreException e) { throw new InvocationTargetException(e); } finally { monitor.done(); } } }; try { getContainer().run(true, false, op); } catch (InterruptedException e) { return false; } catch (InvocationTargetException e) { Throwable realException = e.getTargetException(); MessageDialog.openError(getShell(), "Error", realException .getMessage()); return false; } 首先创建一个实现了 IRunnableWithProgress 接口的匿名类,把生成代码的操作放到 run 方法中,此处为 了便于维护,将实际运行的耗时代码放到了自定义的 doFinish 方法中。代码生成的过程中有可能发生异常, 因此进行异常处理,无论任务是否正常完成进度条都要在任务执行完成之后关闭,因此在 finally 中调用 monitor.done()表示任务完成。 现在我们只是创建了一个 IRunnableWithProgress 的实例,还必须启动它,Eclipse 的向导中提供了一个很 简洁的方式运行此任务,那就是 getContainer().run(boolean fork, boolean cancelable, IRunnableWithProgress runnable),其中 fork 代表任务是否要在一个独立的线程中运行,cancelable 表示这个进度对话框是否能被取消, runnable 就代表要运行的任务。如果能被取消的话,当用户在任务运行的时候单击【取消】按钮,就会抛出 InterruptedException 异常,因此我们捕捉此异常并返回 false,表明此次向导操作没有完成。 下面看一看 doFinish 方法的代码。 【代码 2-13】向导完成以后的操作: private void doFinish(IPackageFragment pckFragment, String packageName, String fileName,IProgressMonitor monitor, Set itemDefSet) throws CoreException { monitor.beginTask("Creating " + fileName, 2); final ICompilationUnit cu = pckFragment.createCompilationUnit( fileName,EnumCodeGenUtils.getEnumSourceCode(packageName, fileName,itemDefSet), true, monitor); monitor.worked(1); monitor.setTaskName("Opening file for editing..."); getShell().getDisplay().asyncExec(new Runnable() { public void run() { try { JavaUI.openInEditor(cu); } catch (PartInitException e) { Activator.logException(e); } catch (JavaModelException e) { Activator.logException(e); } } }); monitor.worked(1); } 首先调用 monitor. beginTask ("Creating " + fileName, 2)启动任务,beginTask 的第一参数代表此任务的名 称,会显示在进度对话框上,此名称可以通过 setTaskName 方法动态设置,第二个参数表示此工作有几步。 以后随着任务的一步步完成,我们就同步地调用 monitor.worked 来推荐滚动条的前进。 接下来就是最重要的一步:生成代码并保存到磁盘。由于生成代码相对复杂,我们把生成代码的逻辑封 装到 EnumCodeGenUtils.getEnumSourceCode 方法中,这里假定调用 EnumCodeGenUtils.getEnumSourceCode 方法就可得到枚举的 Java 源码。创建 Java 文件有两种方式:①用 IO 把 Java 代码当成普通文本文件一样创建; ②把 Java 代码当成编辑单元创建。建议使用第二种方法,第一种方法适用于生成普通的文本类文件。本节使 用第二种方式,我们在后面的章节中将会讲解第一种方式的应用。 在得到了其父包的 IPackageFragment 以后,我们创建 Java 文件会很方便,只需调用 IPackageFragment 的 ICompilationUnit createCompilationUnit(String name, String contents, boolean force, IProgressMonitor monitor) 方法即可。第 1 个参数是生成的 Java 文件名(比如 Test.Java),第 2 个参数为 Java 文件的内容,第 3 个参数 force 代表当要生成的文件已经存在的时候是否覆盖原有文件。 最后一步就是用 Eclipse 的 Java 文件编辑器打开生成的文件。由于打开文件编辑器的操作在另一个线程 中,所以此处要采用 asyncExec 进行同步。 在 Eclipse 中用代码打开一个文件有两种方式: l 使用 org.eclipse.ui.ide.IDE 类的静态方法 openEditor,这个方法有 9 个重载方法,以其中一个为例: public static IEditorPart openEditor( IWorkbenchPage page, IFile input); 第 1 个参数代表要在哪个工作台页中打开,第 2 个参数是要打开的文件对象 IFile。这个方法可以用 来打开各种类型的文件。 l 使用 org.eclipse.jdt.ui. JavaUI 的 openInEditor 方法,其方法声明为: public static IEditorPart openInEditor(IJavaElement element) 这种方法只能打开 Java 文件。 如果要使用第一种方法,那么首先需要将 ICompilationUnit 转换成 IFile,这个转换过程有一点烦琐。 ICompilationUnit 接口是继承了 IJavaElement 接口的,因此我们使用 JavaUI 的 openInEditor 方法,将 ICompilationUnit 传递过去就可以了。 2.6 编写代码生成器 计算机的专家们一直在探寻一种能使得重复代码越来越少的方法,函数封装、面向对象、AOP、MDA、 ORM……所有这些相关或者无关的技术都在试图将重复的代码消灭,可是一路走过来,人们突然发现,重复 的代码是不可能被完全消灭的,到了更高的层次一定会有更高级的重复的代码需要我们去对付,因此代码生 成也逐渐不再被妖魔化。网页编辑器、编译器、IDE 等这些非常重要的工具不就是代码生成器吗?只要是系 统经过好的设计,对于剩下的一些重复性的代码与其使用学院派且严重影响性能的方法进行消除,不如使用 代码生成器来完成来得更实在一些。 回到现实中来,在我们开发程序的过程中,特别是开发一些业务系统的过程中,一些重复的代码总是不 可避免的,比如 ORM 中 POJO 代码和配置文件、资料录入界面的代码、数据库 DDL 语句等,这些工作如果 要开发人员去手动完成话,不仅会降低开发效率,而且会带来很多 bug,最重要的是极容易使得开发人员产 生厌倦心理从而消极怠工甚至离职,从而提高了项目的人力资源成本、增大了项目的风险。因此在大一些的 开发团队中都在使用着各种或公开或自酿的代码生成工具,而且越来越多的人开始选择自酿工具,这是因为 使用第三方的代码生成工具往往不能满足自己的个性化需求。 我们可以通过多种方式来写代码生成工具,比如最简单的通过 StringBuffer 拼字符串,或者借助 groovy template、velocity 等工具来完成,这些工具各有千秋,不过由于本书是讲解 Eclipse 的,因此我们就来看一下 在 Eclipse 中有哪些代码生成方案。 1. 使用 StringBuffer 拼接来生成代码 在一些比较简单的代码生成中,这样的方式是比较方便的,但是当生成的代码结构变得越来越复杂的时 候,代码中 stringbuffer.append()与逻辑判断代码搅和在一起,程序变得非常难以维护。 2. 使用 JDT API 中的 AST JDT 会把 Java 代码编译成 AST(Abstract Syntax Tree 抽象语法树),这样复杂的 Java 代码就变成了相对简 单的树状结构,我们就可以通过 AST 来遍历 Java 代码,从而解析代码或者对代码进行修改,Eclipse 中的 Java 代码重构就是基于 AST 来进行的。 在 Eclipse 中 AST 被称为 CompilationUnit,对应的接口就是 ICompilationUnit,通过 Java 代码来生成 CompilationUnit 最简单的方法是使用 IPackageFragment.createCompilationUnit。指定编译单元的名称和内容,于 是在包中创建了编译单元,并返回新的 ICompilationUnit。我们还可以从头创建一个 CompilationUnit,即生成 一个不依赖于 Java 代码的 CompilationUnit,然后在这个 CompilationUnit 上添加类、添加方法、添加代码,然 后调用 JDT 的 AST 解析器将 CompilationUnit 输出成 Java 代码。这种方式是最严谨的方式,但是当要生成的代 码比较复杂的时候程序就变得臃肿无比,而且只能生成 Java 代码,不能生成 XML 配置文件等文件。 3. 使用 JET JET 是 Eclipse 中一个非常强大的代码生成工具,使用 JET 你可以运用类似 JSP 一样的语法,这 样 我们就 可以轻松地编写代码模板。用它可以创建 SQL 语句、XML、Java 源代码等文件的代码生成器。本书将把它 作为代码生成的工具,因此我们在此处重点讲解 JET 的使用。JET 是 EMF 的一部分,要使用它必须首先安 装 EMF 插件。 使用 JET 分为如下几步。 (1) 把项目转化成 JET 项目 要在项目中使用 JET,必须首先把它转化成 JET 项目,方法如下。 ① 在【包资源管理器】视图上右击,在弹出的快捷菜单中选择【新建】|【其他】命令,然后在弹出 的对话框中选择 Java Emitter Templates 下的 Convert Projects to JET Projects 选项,如图 2.21 所示。 ② 单击【下一步】按钮,选择要转化的项目,如图 2.22 所示,然后单击【完成】按钮。向导会在项目 的根目录下创建一个名字为 templates 的文件夹,而且给项目添加了一个 JET Builder,这个构建器会自动将 templates 文件夹下的模板文件进行编译,生成代码。 (2) 设置 JET 在项目上右击,在弹出的快捷菜单中选择【属性】命令,打开 JET Settings 选项卡,在这个选项卡中就 可以修改模板文件夹和源文件夹了,如图 2.23 所示。注意此处必须输入源文件夹的名字,否则在生成代码的 时候就有可能出现代码生成位置出错的问题。 图2.21 选择 JET 转换向导 图2.22 选择被 转换的项目 图 2.23 设置JET 的属性 (3) 创建模板文件 JET 的模板文件的命名规定是在要生成的代码生成器类的文件名后加 jet,比如想命名我们的代码生成器 为 MyGen.java,那么只要把模板命名为 MyGen.javajet 就可以了。因此可在 templates 文件夹下创建一个文件 EnumCodeGenerator.javajet,创建完毕之后,系统会弹出一个错误对话框,如图 2.24 所示。 图 2.24 构建出错对话框 不要惊慌,这并不是说明我们的创建过程有错,而是创建完模板文件以后,JET 构建器就去尝试构建 EnumCodeGenerator.javajet,由于这个文件是空的,所以当然就构建失败报错了。 在 EnumCodeGenerator.javajet 中输入如下代码: <%@ jet package="com.cownew.enumgenerator.wizards" class="EnumCodeGenerator" %> Hello,<%=argument%>! 保存以后,JET 就立即会生成 EnumCodeGenerator.java 文件,内容如下: public class EnumCodeGenerator { protected static String nl; public static synchronized EnumCodeGenerator create( String lineSeparator) { nl = lineSeparator; EnumCodeGenerator result = new EnumCodeGenerator(); nl = null; return result; } protected final String NL = nl == null ? (System.getProperties().getProperty("line.separator")) : nl; protected final String TEXT_1 = " Hello, "; protected final String TEXT_2 = "!"; protected final String TEXT_3 = NL; public String generate(Object argument) { final StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(TEXT_1); stringBuffer.append(argument); stringBuffer.append(TEXT_2); stringBuffer.append(TEXT_3); return stringBuffer.toString(); } } 可以看到 JET 生成的代码采用的也是 StringBuffer 拼装的形式,注意此处生成的代码是无法手工修改的, 因为每次修改以后保存的时候 JET 会自动把代码替换成未修改之前的代码。 (4) 测试模板代码 在 EnumCodeGenUtils 中创建 main 方法,然后输入如下代码: EnumCodeGenerator gen = new EnumCodeGenerator(); System.out.println(gen.generate("Eclipse")); 运行之后控制台中就打印出了:Hello, Eclipse! 我们来对上边的模板代码和测试代码做一下简要的分析: ① <%@ jet package="com.cownew.enumgenerator.wizards" class="EnumCodeGenerator" %> 这是模板的头部分,以“@ jet”开头,这部分主要声明此模板的有关信息,比如生成代码的包路径、类 名、导入的类等,package 属性定义的就是生成代码的包路径,而 class 属性定义的是生成的类名。 ② Hello, <%=argument%>! 这部分就是模板的正文了,和 JSP 语法一样,显示一个变量的方法是<%=变量名>。注意这里的变量 argument 是有特殊含义的,它表示传递给模板的参数。 ③ 代码生成器生成代码的方法是 generate,因为我们经常需要传递一些参数给代码生成器,所以 generate 方法有一个类型为 Object 的参数,此参数在模板中可以用 argument 取得。 对 JET 有 了一个感性的认识之后,我们就来通过实战来操练一下。上一节中 EnumCodeGenUtils.getEnumSourceCode 方法的实现为空,这一节我们就来完成这项关键性的工作。 经过分析,我们发现需要传递给模板代码如下 3 个参数才可以正确地输出代码:枚举类的包名、枚举类 的类名、枚举类的项。因为模板代码的 generate 方法只接受类型为 Object 的一个参数,所以我们需要把这 3 个参数封装到一个 JavaBean 中,如下定义 JavaBean。 【代码 2-14】模板参数类: public class EnumGenArgInfo { private Set items; private String className; private String packageName; public String getPackageName() { return packageName; } public void setPackageName(String packageName) { this.packageName = packageName; } public String getClassName() { return className; } public void setClassName(String className) { this.className = className; } public Set getItems() { return items; } public void setItems(Set items) { this.items = items; } } 接下来我们来写模板文件。 【代码 2-15】模板文件: <%@ jet package="com.cownew.enumgenerator.wizards" class="EnumCodeGenerator" imports="Java.util.*" %> <% EnumGenArgInfo argInfo = (EnumGenArgInfo)argument; Set enumItems = argInfo.getItems(); String className = argInfo.getClassName(); String packageName = argInfo.getPackageName(); %> package <%=packageName%>; public class <%=className%> { private String type; <%for(String item:enumItems){%> public <%=className%> <%=item%> = new <%=className%>("<%=item%>"); <%}%> private <%=className%>(String type) { super(); this.type = type; } public int hashCode() { final int PRIME = 31; int result = 1; result = PRIME * result + ((type == null) ? 0 : type.hashCode()); return result; } public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; final <%=className%> other = (<%=className%>) obj; if (type == null) { if (other.type != null) return false; } else if (!type.equals(other.type)) return false; return true; } } 这个模板文件是非常简单的,有了前面的基础,读懂这个模板文件就非常简单了,这里只讲两点: l 文件头的 imports 属性是用来定义生成的代码的 import 列表的,这个模板中用到了集合类 Set,所以 要用 imports="Java.util.*" 将其导入,否则生成的代码会编译错误。如果要导入多个类,只要把它们 用空格隔开即可,比如: imports= imports="Java.util.* Java.sql.Date" 不能使用其他分隔符。 l 由于传递进来的参数是一个 JavaBean,因此需要把 argument 进行一次转型操作: EnumGenArgInfo argInfo = (EnumGenArgInfo)argument; 编写下面的代码测试一下这个代码模板: public static void main(String[] args) { EnumCodeGenerator gen = new EnumCodeGenerator(); EnumGenArgInfo argInfo = new EnumGenArgInfo(); argInfo.setClassName("MyEnum"); Set items = new HashSet(); items.add("VIP"); items.add("MM"); argInfo.setItems(items); argInfo.setPackageName("com.cownew"); System.out.println(gen.generate(argInfo)); } 运行之后发现输出的代码完全正确。 这样我们就可以来完成 EnumCodeGenUtils 类的 getEnumSourceCode 方法。 【代码 2-16】完成后的 getEnumSourceCode 方法: public static String getEnumSourceCode(String packageName, String fileName, Set itemDefSet) { Pattern pattern = Pattern.compile("(.+).Java"); Matcher mat = pattern.matcher(fileName); mat.find(); String className = mat.group(1); EnumCodeGenerator gen = new EnumCodeGenerator(); EnumGenArgInfo argInfo = new EnumGenArgInfo(); argInfo.setClassName(className); argInfo.setItems(itemDefSet); argInfo.setPackageName(packageName); return gen.generate(argInfo); } 这里用到了正则表达式来从 Java 文件名中提取类名,使用的是 JDK 中的正则表达式实现,对于正则表 达式,我们可以去查阅相关资料,正则表达式是一个非常好用的工具,掌握以后能轻松解决很多字符串解析 相关的问题,并为学习编译原理打下基础。 2.7 功能演示、打包安装 所有的代码编写工作都已经完成,让我们来测试一下。在项目 EnumGenerator 上右击,在弹出的快捷菜 单中选择【运行方式】|【Eclipse 应用程序】命令,片刻以后测试工作台就会启动完毕。 在要新建枚举的包上右击,在弹出的快捷菜单中选择选择【新建】|【其他】命令,在向导对话框中选 择 EnumGenerator 下的【枚举创建向导】选项,如图 2.25 所示。 单击【下一步】按钮,创建一个颜色枚举,如图 2.26 所示,进行设置。 单击【下一步】按钮,在定义枚举项界面中输入要定义的颜色项目,如图 2.27 所示。 图 2.25 枚举创建向导 图 2.26 设定枚举类的属性 图 2.27 设定枚举项 单击【完成】按钮,就可以看到自动生成了如下的代码。 package com.cownew.enumtest; public class ColorEnum { private String type; public ColorEnum RED = new ColorEnum("RED"); public ColorEnum YELLOW = new ColorEnum("YELLOW"); public ColorEnum BLUE = new ColorEnum("BLUE"); public ColorEnum BLACK = new ColorEnum("BLACK"); public ColorEnum GREEN = new ColorEnum("GREEN"); private ColorEnum(String type) { super(); this.type = type; } public int hashCode() { final int PRIME = 31; int result = 1; result = PRIME * result + ((type == null) ? 0 : type.hashCode()); return result; } public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; final ColorEnum other = (ColorEnum) obj; if (type == null) { if (other.type != null) return false; } else if (!type.equals(other.type)) return false; return true; } } 这里运行的例子是运行在插件开发环境中的,如果要是给用户使用的话必须将其导出,成为可以部署的 安装包。 导出生成可以部署的安装包是非常简单的,只需要在【包资源管理器】中右击,在弹出的快捷菜单中选 择【导出】命令,选中【导出】向导页中的【插件开发】目录下的“可部署的插件和段”,单击【下一步】 按钮,如图 2.28 所示。 图 2.28 导出插件 选中 EnumGenerator 项,然后在【目录】文本框中填入要导出到哪个目录下(也可以选择下边的【归档文 件】而将插件导出为 jar 包),单击【完成】按钮即可。 本章以一个简单而实用的例子介绍了 Eclipse 的插件开发。这里所写的程序还有很多并不是很完美的地 方,甚至有一些明显的 bug,为了不花费太多的精力在这些细节上,我们没有完全展开叙述。相信读者会在 学习和实战中发现更多的问题并通过摸索解决这些问题,如果能把学到的东西应用到实际开发中去的话,本 章的目的也就达到了。 第 3 章 插件开发导航 在上一章中,介绍了一个完整的插件的开发,相信读者已经掌握了插件开发的基本知识。不过我们只是 介绍了每一步怎么去做,却没有介绍为什么要这么做以及相关的知识点。本章将会把插件开发相关的常用知 识学习一遍。插件开发涉及到的知识是非常多的,这些知识都可以通过阅读 Eclipse 的帮助文档以及阅读开源 项目的代码获得,所以这里没有必要去介绍每一个细节,我们只会把知识点介绍一下,读者无须强行记忆, 只要知道这一章是做什么的,并且遇到问题时能去查询相关的资料就可以了。 3.1 程序界面的基础——SWT/JFace AWT、Swing 是 Java 标准库中的图形化界面框架,但是由于其在性能、稳定性、美观性等方面有很多问 题,导致用 Java 开发出来的成熟 GUI 项目非常少,而且即使是成熟的 Java GUI 系统,比如 JBuilder、NetBeans 等的界面也是遭人诟病的。在 IBM 开始开发 Eclipse 的时候,开发人员都强烈反对使用 Swing 开发 Eclipse, 因此 IBM 决定采用 Smalltalk GUI 的实现方式来开发一个新的 Java 界面框架,这个框架也就是 SWT。 与 AWT/Swing 不同,SWT 底层采用的是 JNI native 调用本地操作系统的 API,只有本地操作系统实现不 了的部件才去自己绘制,因此 SWT 的效率是非常高的。 经过不断成熟和发展,SWT 现在已经成为与 Eclipse 无关的开发包了,也就是用 SWT 做出来的程序只要 没有调用Eclipse平台的东西,那么它就可以脱离Eclipse运行。由于本章中讲到的SWT界面都是运行在Eclipse 中的,因此我们不再介绍单独运行 SWT 程序的方法。 SWT 是对操作系统 GUI API 的封装,因此没有做更多应用层次的封装,比如要显示一个对话框,就要自 己去画【确定】、【取消】按钮,要弹出消息对话框就要自己去写数行代码。为了简化 SWT 的开发,IBM 开 发出了 JFace,JFace 不是与 SWT 格格不入的,JFace 就是调用 SWT 实现了更多实际应用开发中要用到的公 共类。SWT 和 JFace 的关系就像 Windows 开发中 Windows SDK 和 MFC 的关系一样,我们在开发的时候应 当尽量去使用 JFace 的东西,只有当 JFace 的东西不满足我们要求的时候才去直接求助 SWT。 3.1.1 SWT 的类库结构 SWT 的所有类都在 org.eclipse.swt 包下。最重要的类就是 Widget,它是所有界面对象的基类,类图如图 3.1 所示。 Widget 的直接子类有 Caret(插入光标)、Menu(菜单)、ScrollBar(滚动条)、Tray(系统托盘图标)等。Widget 的子类 Item 下的类是一些无法独立于其他部件的部件,比如 MenuItem(菜单项)、TableItem(表格项)、 TrayItem(系统托盘图标项)、TreeItem(树项)等。Widget 的子类 Control 是一个比较庞大的基类,大部分 SWT 部件都在此类下,其直接子类有 Button(按钮)、Label(标记)、ProgressBar(进度条)等。Control 的子类 Scrollable 是所有可以带滚动条的对象的基类,比如 Text(文本框)、List(列表框)等。Scrollable 的子类 Composite 是 SWT 中一个重要的类,它是所有可以容纳其他部件的类的基类,其子类有 Browser(浏览器)、Combo(下拉列表框)、 Group(组合框)、Table(表格)、Tree(树)等。 图 3.1 SWT 的类结构图 上面从类层次的角度研究了 SWT 的类结构,下面再来看一下 SWT 的包结构: l org.eclipse.swt 下有 SWT,SWTException 和 SWTError 类。SWT 中定义了 SWT 中的公共常量,包 括部件风格、消息常量等;SWTException 和 SWTError 则是 SWT 中异常的基类。 l org.eclipse.swt.widgets包下定义了常用、核心SWT窗口小部件(widget)的公有API类定义。如Display、 Shell、Button、Menu 等。一般编写 GUI 程序用到的 Widget 大部分都在这个包下。 l org.eclipse.swt.events 包中提供了对 SWT 事件监视器(Event Listener)的支持,如 Button 的 SelectionListener、Mouse 的 MouseListener、MouseMoveListener 和 MouseTrackListener 等,还有与 这些 Listener 对应的 Adapter 实现类和 Event 类。 l org.eclipse.swt.layout 包中定义了 SWT 的布局管理器,其中有 FillLayout、GridLayout 和 RowLayout 三种。 l org.eclipse.swt.graphics 包中包含了 SWT 中 graphic 类,如 Color、Font 和 Image 等,这个包下的类 的资源管理方式和其他部件略有不同,3.1.2 节中将会介绍。 l org.eclipse.swt.printer 提供了对打印的支持。 l org.eclipse.swt.custom 包中包含了一些可自定义的窗口小部件,它们是学习开发自定义 SWT 部件的 很好的例子。 l org.eclipse.swt.dnd 提供了对拖放操作的支持。 3.1.2 SWT 中的资源管理 AWT/Swing 的资源管理使用的是 Java 提供的垃圾回收机制,但是由于 GUI 是非常消耗资源的,要求对 象不被使用的时候立即被回收,而垃圾回收机制是无时间保证的。这对系统资源的处理会是致命的,比如程 序在一个循环语句中去加载数万张图片,对其进行加盖印章处理后再进行保存,常规的处理方式是每次调入 一张,修改保存,然后就立即释放该图片资源,而后再循环调入下一张图片,这对操作系统而言,任何时刻 程序占用的仅仅是一张图片的资源。但如果资源管理完全交给垃圾回收机制去处理,也许会是在循环语句结 束后,JVM 才会去释放图片资源,其结果可能是你的程序还没有运行结束,系统就已经内存溢出了。而 SWT 则创新性地抛弃了 Java 提供的垃圾回收机制,让资源由开发者进行生命周期管理,即显式地释放已经分配的 任何操作系统资源(调用 dispose 方法释放资源)。其法则就是:①如果您创建对象,则您必须销毁它;②父部 件被销毁,子部件也同时被销毁。具体实施起来有如下规则: l 如果使用构造函数来创建图形对象或窗口小部件,使用完时必须显式地将其销毁。 l 当调用一个包含子部件的部件(即 Composite 的子类)的 dispose 方法时,将递归地调用其所有子部件 的 dispose 方法。因此无需手动去释放这些子部件的资源。 l 如果部件不是您创建的,而是调用其他类的某个方法得到的,则不要将其除去,这是因为它不是您 创建的。 很多开发人员在看到“父部件被销毁,子部件也同时被销毁”这一条的时候就认为自己又不用去管理资 源的释放工作了,因为只要销毁根部件,那么所有的子部件就都会被销毁了。这 是 十 分 危险的误解,因为 SWT 中还有一部分资源不是继承自 Widget 的,也就是不可能有父部件,这样就需要程序员手动去释放资源。例如 org.eclipse.swt.graphics 下的类,这些类都继承了 Resource 类,Resource 中也定义了 dispose 方法。这些类有 Color(颜色)、Cursor(鼠标指针)、Font(字体)、GC(图形设备设置)、Image(图片)等。比如您为按钮设定了一种 字体,那么必须在销毁这个按钮的时候手动去释放字体对象。 当然并不是所有的 SWT 对象都需要程序员去释放的,比如 org.eclipse.swt.graphics 包下的 Point(点)、 Rectangle(矩形)、RGB 等类是没有 dispose 方法的,因此只有把它们交给 JVM 的垃圾回收机制去管理了。 3.1.3 在非用户线程中访问用户线程的 GUI 资源 在非用户线程中对用户线程的 GUI 资源进行访问的时候,如果不进行同步的话就会造成不可预料的问 题。AWT/Swing 中并没有强制在非用户线程中访问用户线程的 GUI 资源的时候要进行同步,而 SWT 则进行 了同步控制,这样就可以预防这些不可预料的问题。在 SWT 中,通常存在一个被称为“用户线程”的唯一 线程,只有在这个线程中才能调用对组件或某些图形 API 的访问操作。如果在非用户线程中程序直接调用这 些访问操作,那么 SWTExcepiton 异常会被抛出。 下面看一个例子: Runnable r = new Runnable() { public void run() { for (int i = 0; i < 100; i++) { try { wait(1000); } catch (InterruptedException e) { } text.setText(new Integer(i).toString()); } } }; 我们启动一个线程,在这个线程中,每隔一秒为界面文本控件赋值一次,运行后就会抛出 SWT 异常。 解决这个问题的方法也是非常简单的,那就是通过 Display 类的 syncExec(Runnable)和 asyncExec (Runnable)这两个方法去实现: Runnable r = new Runnable() { public void run() { for (int i = 0; i < 100; i++) { try { wait(1000); } catch (InterruptedException e) { } final int j = i; display.asyncExec(new Runnable() { public void run() { text.setText(new Integer(j).toString()); } }); } } }; 方法 syncExec()和 asyncExec()的区别在于前者要在指定的线程执行结束后才返回,而后者无论指定的线 程是否执行都会立即返回到当前线程。 3.1.4 访问对话框中的值 在程序中经常会弹出一些对话框,要求用户输入一些值,然后根据用户输入的值来进行后续操作。比如 在程序中弹出一个对话框要求用户输入姓名和国家,然后根据输入的值显示问候语。 定义 SettingDialog 类: public class SettingDialog extends Dialog { private Text txtName; private Text txtCountry; ... public String getName() { return txtName.getText(); } public String getCountry() { return txtCountry.getText(); } } 主界面: SettingDialog dlg = … ; dlg.open(); if(dlg.open()==Window.OK) { MessageDialog.openInformation(shell, "", dlg.getName()+" is from "+ dlg.getCountry()); } 当运行的时候就会抛出如下异常: org.eclipse.swt.SWTException: Widget is disposed 这是为什么呢? 让我们来看一下 org.eclipse.jface.window.Window 类,它是 SettingDialog 的间接父类,在 Window 类的 close 方法中将界面控件全部销毁掉了,当我们关闭一个界面的时候就调用了 close 方法,这样当窗口已经关闭的 时候,我们再去调用 dlg.getName()的话,getName 方法就会去访问 txtName 控件,可是 txtName 已经被销毁 掉了,不能被访问了,所以就抛出了 Widget is disposed 这个异常消息。 那么我们应该怎么修改呢?既然不能在窗口关闭以后访问界面控件对象,那么只有在关闭之前来把要访 问的控件值提前保存起来了。做如下修改: public class SettingDialog extends Dialog { private Text txtName; private Text txtCountry; private String name; private String country; ... protected void okPressed() { name = txtName.getText(); country = txtCountry.getText(); super.okPressed(); } public String getName() { return name; } public String getCountry() { return country; } } 当单击【确定】按钮的时候,okPressed 方法会被调用,我们在调用父类的 okPressed 之前将控件的值保 存起来就可以了,并且改写了 get 方法,让它返回我们保存的值。 3.1.5 如何知道部件支持哪些 style SWT 中的部件的构造函数都有一个 int style 参数,这个参数表示要创建的部件的风格。 这个参数的类型是整数而非枚举,那么如何确定它支持哪些值呢?答案就是查看 JavaDoc。以 Text 部件 为例,查看 Text 的源码,定位到它的构造函数处,如图 3.2 所示。 图 3.2 构造函数的 JavaDoc JavaDoc 中明确地指出支持如下的风格: SWT.SINGLE、SWT.MULTI、SWT.READ_ONLY、SWT.WRAP。 SWT 中定义的常量都是掩码形式的,比如: public static final int MULTI = 1 << 1; //即二进制的 10 public static final int WRAP = 1 << 6; //即二进制的 1000000 可以用“或”操作符来进行风格的组合,比如指定文本框为多行并且自动换行,只要如下调用即可: Text txt = new Text(parent,SWT.MULTI|SWT.WRAP); 3.2 SWT 疑难点 SWT 的 API 数量是非常多的,不过对于熟悉 Swing 或者其他语言的界面开发者来说,只要借助帮助文 档一般都可以很快掌握其使用。所以就不打算把所有部件的使用方式再重复一遍了,这里只介绍一些需要重 点注意的控件方法。 3.2.1 Button 部件 在 GUI 术语中, 和 这两个部件分别被叫做复选框和单选按钮,也就是它们也是按钮的一种,但是 无论是 Delphi 中的 VCL、.NET Framework 还是 Swing,都把它们看作是与 Button 不同的部件。当程序员开 始学习 SWT 的时候,突然发现 SWT 中没有 Checkbox、RadioButton 控件了,难道 SWT 不支持吗? 当然是不是了,SWT 中把 、 和 看成了按钮部件的不同样式,不再用不同的类区分它 们。当新建一个 Button 实例的时候,只要指定风格为 SWT.CHECK 或者 SWT. RADIO 就可以了。 3.2.2 Text 部件 与 Swing 中不同,SWT 中的单行文本框和多行文本框都是用 Text 部件表示,只是通过 SWT.SINGLE、 SWT.MULTI 两种不同的风格来区分。Text 本身支持滚动条(SWT.H_SCROLL 和 SWT.V_SCROLL 风格),无 需像 Swing 中那样要把文本框包在 ScrollPane 中才可以。 3.2.3 Tray Tray 是 SWT 提供的一个系统托盘类,通过这个类可以在系统托盘中增加图标。这看起来很酷,不过要 尽力避免使用它,因为目前此部件还不能做到完全地跨平台。 3.2.4 Table 表格控件是一个非常常用的控件,可以用来实现大数据量的展示和编辑,SWT 中的 Table 控件就是提供 这样功能的一个控件,与 Swing 的 JTable 比起来 Table 的可用性更好。下面就来看一下对 Table 的主要操作 方式。 (1) 给 Table 增加列: TableColumn colName = new TableColumn(table,SWT.LEFT); colName.setText("名称"); colName.setWidth(100); (2) 给 Table 增加行: TableItem item = new TableItem(table, SWT.NONE); (3) 为单元格增加编辑器: TableEditor editor = new TableEditor(table); Text text = new Text(table, SWT.NONE); editor.grabHorizontal = true; editor.setEditor(text, item, 5); editor = new TableEditor(table); Table 的单元格默认是没有任何编辑器的,当然单元格也就是只读的了,如果我们要编辑单元格的话就必 须为单元格设定编辑器,编辑器可以是文本框、复选框、单选按钮等。 下面这段代码就是为 item 这一行的第 5 列增加一个文本编辑器,同理也可以为其增加复选框编辑器: editor = new TableEditor(table); Button button = new Button(table, SWT.CHECK); button.pack(); editor.minimumWidth = button.getSize().x; editor.horizontalAlignment = SWT.CENTER; editor.setEditor(button, item, 5); 3.2.5 在 SWT 中显示 AWT/Swing 对象 SWT在设计之初是想兼容AWT/Swing的,但是由于SUN 的极度不配合,导致SWT最终没有能兼容AWT。 这就造成 AWT/Swing 中原有的一些很好用的代码无法移植到 SWT 中的问题。比如做图形化报表通常使用 JFreeChart,但是 JFreeChart 只能显示在 AWT/Swing 中,没有提供 SWT 的支持,是否代表我们在 SWT 中就 不能使用它了呢?当然不是了。我们可以用 SWT_AWT 桥接器来解决这个问题。 调用 SWT_AWT 的 getFrame 方法就可以把一个 Composite 面板转换成 AWT 中的 Frame,这样就可以在 这个 Frame 中进行任何 AWT/Swing 相关的操作了。 比如使用 JFreeChart: Composite drawarea = new Composite(parent, SWT.EMBEDDED); drawarea.setLayout(new FillLayout()); Frame canvasFrame = SWT_AWT.new_Frame(drawarea); canvas = new Java.awt.Canvas() { public void paint(Graphics g) { super.paint(g); if (chart != null) chart.draw((Graphics2D)g, getBounds()); } }; canvasFrame.add(canvas); 如果在 Frame 中使用了 AWT/Swing 部件,我们也完全可以在 SWT 代码中访问 AWT/Swing 部件,由于这些 AWT/Swing 部件不是在一个 UI 线程中的,所以在访问的时候要进行同步。 Display.getDefault().asyncExec(new Runnable){ public void run() { awtButton.setText("I am AWT Button"); } }; 还有一些 Java 的图形化应用也可以通过这种方式来支持 SWT,比如一些成熟的 Java GIS 应用。 3.3 异步作业调度 编程的时候经常会遇到一些长时间的操作,比如读取大量文件并进行解析、从远端服务器读取文件、进 行复杂的数据库操作等,如果处理不好的话,会造成程序好像死掉了一样。令人震惊的是,很多程序员对此 并不在乎,因为他们知道程序为什么而“死掉了”,并向用户解释说程序在做什么,不用担心,只要等就可 以了。如果站在用户的角度思考一下就知道这种想法有多么可怕。 这里讲作者经历过的事情:曾经开发过一个从超大 XML 文件(大于 10M)中导入数据并插入到数据库中 的功能,由于在导入每一条数据的时候都要把和这条数据有关的数据从数据库中取出来,然后进行一定的处 理后再插入到数据库中,所以耗时是非常长的,一般都要耗时半个小时以上。在做第一个版本的时候没有考 虑进度条,当把程序发给用户的时候,用户用了一会儿就打电话过来:“那个程序死掉了,帮我看看吧!”, 通过向他解释这是正常的,他这才将信将疑地放下电话,没过了 5 分钟,又打电话过来“怎么还是死的,你 们怎么做的程序,我要投诉你!”。后来终于导入成功了,但是从用户的反馈来看,他们是十分的不满意。后 来在给这个程序开发 bug 修复补丁的时候顺手给程序加上了进度条的功能,随时报告当前的进度,几乎没有 增加工作量。谁知发给客户以后,客户赞扬说:“这个版本改进比较大呀,好多了,不错!现在我都是单击 完【导入】按钮以后就去做别的事情了,时不时地回来看看导入进度!”——作者这才深刻的意识到“进度 条”这个在技术人员看起来微不足道的小功能在改善用户体验方面有多么重要的作用。 后来在去客户现场做支持的时候看到的一幕又感到猛然一惊。所做的那个数据导入功能是 ERP 系统中的 一部分,这个 ERP 系统是一次可以打开多个内部窗口的(类似于 Windows 中的 MDI),用户可以在一个窗口 中录单,切换到另一个窗口中制作报表,或者切换到另一个窗口发邮件。看到用户在打开那个数据导入窗口, 单击【导入】按钮后就切换到另外一个窗口进行录单操作了。天呀,如果没有提供那个进度条的功能,那么 用户单击【导入】按钮以后整个 ERP 系统就“死掉了”,用户就无法进行任何操作,也就无法做任何工作, 难道这半个多小时要他去上网聊 QQ、翻纸牌吗? 在这一点上 Eclipse 做的无疑是非常好的。当我们新建一个项目的时候,如果项目的初始化时间比较长, Eclipse 就会弹出一个带滚动条的窗口,提示用户正在初始化;对于一些耗时非常长的操作,比如从 CVS 检 出代码,Eclipse 会弹出一个带有【在后台运行】按钮的进度对话框,如图 3.3 所示,用户单击【在后台运行】 按钮以后,这个对话框就会关闭,这 样 用户 就 可以在 Eclipse 中进行其他的操作了,避免了长时间等待所造成 的时间浪费。 图 3.3 进度条 我们最常接触的就是 IProgressMonitor 了,在 很 多 方法中都要求传递此接口的实例,比如编辑器的 doSave 方法就是如下声明的: public void doSave(IProgressMonitor monitor) 通过这个接口就可以操控进度条来显示我们当前的保存进度了。不过 IProgressMonitor 并不是进度条对 话框,它要“依靠”一个进度显示器来把进度显示出来,比如最常见的进度对话框 ProgressMonitorDialog。 部分任务在运行的时候可以由用户选择取消,当用户取消任务的时候,IProgressMonitor 的 isCanceled 方 法会返回 true,因此我们在任务进行的时候要实时地去调用 isCanceled 方法,当发现任务被取消的时候要尽 快结束任务。 我们可以使用 Java 的标准接口 Runnable 来实现多线程任务运行,不过在 Eclipse 中又有了新的选择,那 就是 IRunnableWithProgress,其声明如下: public interface IRunnableWithProgress { public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException; } 这个类的使用和 Runnable 非常相似,只要把任务放到 run 方法中就可以了,最重要的是可以调用 monitor 来对当前进度显示进行控制。下面就是一个完整的进度条演示例子。 ProgressMonitorDialog dialog = new ProgressMonitorDialog(shell); dialog.run(true, true, new IRunnableWithProgress() { public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { final int ticks = 10000; monitor.beginTask("开始操作", ticks); try { for (int i = 0; i < ticks; i++) { if (monitor.isCanceled()) throw new InterruptedException(); monitor.worked(1); } } finally { monitor.done(); } } }); 调用 beginTask 方法来完成任务,ticks 参数表示此任务有多少工作量,调用 worked 方法报告自上次报告 以来当前完成的任务数量,在循环中不断通过 isCanceled 方法判断当前任务是否被用户取消。需要注意,要 在 finally 中调用 done 方法完成任务,否则会出现进度对话框无法正常关闭的情况。 除了 ProgressMonitorDialog 外,在 Eclipse 中还可以通过其他方式显示进度,比如 IWorkbenchWindow 通 过在工作台窗口的状态行中显示进度来实现此界面,WizardDialog 在向导状态行中显示长时间运行的操作。 除了可以自己构造进度对话框来显示进度之外,我们还可以调用平台的进度服务,而且 Eclipse 也推荐使 用平台的进度服务,这样可以使所有插件都将具有一致的进度表示。平台的进度服务定义为接口 IProgressService,我们可以通过 PlatformUI.getWorkbench(). getProgressService 方法来调用系统的进度服务, 例如: IProgressService progressService = PlatformUI.getWorkbench() .getProgressService(); progressService.busyCursorWhile(new IRunnableWithProgress() { public void run(IProgressMonitor monitor) { //执行耗时的操作 } }); 在调用 Eclipse的方法或者第三方插件的一些方法的时候,有的方法要求传递一个实现了 IProgressMonitor 的实例进去,如果我们无法传递或者无需传递的时候,最好不要传递 null 值进去,而是要传递 NullProgressMonitor 的一个实例进去,此类位于 org.eclipse.core.runtime 包下,它实现了 IProgressMonitor 接 口,但是所有方法都是给的空实现,传递此类就避免了被调用方法没有进行空指针判断而造成的麻烦。 3.4 对 话 框 3.4.1 信息提示框 信息提示框对应的类为 org.eclipse.jface.dialogs.MessageDialog,它定义了如下主要方法。 (1) 确认对话框(如图 3.4 所示): public static boolean openConfirm(Shell parent, String title, String message) 图 3.4 确认对话框 (2) 错误信息框: public static void openError(Shell parent, String title, String message) (3) 普通消息框: public static void openInformation(Shell parent, String title, String message) (4) 询问对话框(如图 3.5 所示): public static boolean openQuestion(Shell parent, String title, String message) 图 3.5 询问对话框 注意询问对话框与确认对话框方法的区别。 (5) 警告对话框: public static void openWarning(Shell parent, String title, String message) 3.4.2 值输入对话框 在和用户交互的时候,对于一些复杂的信息,可能需要通过自定义的对话框进行采集,而对于像简单的 字符串之类的信息,则可以通过弹出值输入对话框的方式进行采集。 值输入对话框定义在 org.eclipse.jface.dialogs.InputDialog 中,与消息对话框不同,这个类是必须实例化才 能使用的,其构造函数为: public InputDialog(Shell parentShell, String dialogTitle, String dialogMessage, String initialValue, IInputValidator validator) 参数 dialogTitle 为标题,dialogMessage 为显示的消息,initialValue 为对话框中的初始值,validator 为值 校验器。当 validator 为 null 的时候,不对对话框中的值做校验,而非 null 的时候需要做校验。 IInputValidator 接口定义如下: public interface IInputValidator { public String isValid(String newText); } 当 isValid 返回非空的时候,值校验不通过,并且把 isValid 返回的值作为错误信息显示。 使用值输入对话框的例子如下: InputDialog inputDlg = new InputDialog(shell,"输入","请输入您的年龄","20", new IInputValidator(){ public String isValid(String newText) { int i; try { i = Integer.parseInt(newText); } catch (NumberFormatException e) { return "年龄必须为整数!"; } if(i<0) { return "兄弟来自反物质世界?年龄不可能为负吧!"; } if(i>150) { return "您也太高寿了吧!"; } return null; } }); if(inputDlg.open()==Window.OK) { System.out.println(inputDlg.getValue()); } 运行以后当在对话框中输入“-20”的时候就会提示错误,如图 3.6 所示。 图 3.6 输入对话框 3.4.3 错误对话框 错误对话框定义在 org.eclipse.jface.dialogs.ErrorDialog 中,它有两个重载的 openError 静态方法,与其他 对话框不同的是有一个 IStatus status 参数,这个参数用来设置错误信息,一般我们使用它的一个实现类 org.eclipse.core.runtime. Status,Status 中定义了两个静态常量 OK_STATUS、CANCEL_STATUS,我们可以使 用它们,如果它们不能满足要求,就要调用 Status 的构造函数进行实例化,其构造函数如下: public Status(int severity, String pluginId, int code, String message, Throwable exception) l severity 表示错误的程度,可取值为 OK、ERROR、INFO、WARNING、CANCEL。 l pluginId 为调用插件的插件 id,一般定义在对应插件的 Activator 中。 l code 为错误代码。 l message 为错误消息。 l exception 为要抛出的异常。 显示效果如图 3.7 所示。 图 3.7 错误对话框 3.4.4 颜色选择对话框 颜色选择对话框定义在 org.eclipse.swt.widgets.ColorDialog 中,其调用方法与普通对话框没有什么不同。 例如: ColorDialog colorDlg = new ColorDialog(shell); RGB rgb = colorDlg.open(); if(rgb!=null) { Color color = null; try { color = new Color(shell.getDisplay(),rgb); //使用 color... } finally { if(color!=null) color.dispose(); } } 这里得到返回值的方式非常值得研究,对话框并没有直接返回 Color,而是返回一个 RGB 对象的实例, 由调用者来根据 RGB 构造 Color,这正好符合了 SWT 中资源管理的一个原则:“谁创建谁销毁”。如果 ColorDialog 返回值是 Color 类型,那么必须由 ColorDialog 负责销毁,可是 ColorDialog 不知道什么时候去销 毁,所以 ColorDialog 就返回了一个由 JVM 去负责销毁的对象 RGB,此对象包含了需要的信息,由调用者去 构造,类似的用法在下面的字体对话框中也可以看到。 ColorDialog 还有一个 setRGB 方法可以用来给颜色对话框设置初始值。 3.4.5 字体对话框 字体对话框定义在 org.eclipse.swt.widgets.FontDialog 中,调用方法如下: FontDialog fontDlg = new FontDialog(shell); FontData fontData = fontDlg.open(); if(fontData!=null) { Font font = null; try { font = new Font(shell.getDisplay(),fontData); //使用 font... } finally { if(font!=null) font.dispose(); } } 和颜色对话框类似,字体对话框返回的字体信息是保存在由 JVM 负责资源回收的 FontData 对象中的, 由调用者来根据 FontData 对象构造字体对象。FontDialog 有一个 setFontList 方法可以用来设置初始值。 3.4.6 目录选择对话框 目录选择对话框定义在 org.eclipse.swt.widgets.DirectoryDialog 中,调用方法如下: DirectoryDialog dirDlg = new DirectoryDialog(shell); String dir = dirDlg.open(); if(dir!=null) { System.out.println(dir); } DirectoryDialog 中定义了如下几个方法。 l setText:为对话框设置窗口标题。 l setMessage:为对话框设置提示信息。 l setFilterPath:为对话框设置初始路径。 下面的代码执行以后的效果如图 3.8 所示。 DirectoryDialog dirDlg = new DirectoryDialog(shell); dirDlg.setText("这里是 Text"); dirDlg.setMessage("这里是 Message"); dirDlg.setFilterPath("c:/Downloads"); String dir = dirDlg.open(); 图 3.8 目录选择对话框 3.4.7 文件选择对话框 与 .Net、Delphi、VB 等框架中的文件对话框不同,SWT 中的保存对话框和打开对话框都定义在 org.eclipse.swt.widgets.FileDialog 类中,只要在构造函数中指定不同的风格即可。 打开对话框: FileDialog fileDlg = new FileDialog(shell,SWT.OPEN); 保存对话框: FileDialog fileDlg = new FileDialog(shell,SWT.SAVE); FileDialog 中定义了如下几个方法。 l setFileName:设定初始文件名。 l setFilterExtensions:设定文件名过滤器。 l setFilterPath:设定初始路径。 l setText:设定对话框标题。 l getFileNames:以数组形式返回选中的多个文件名。 l getFilterPath:返回选中的路径。 调用例子: FileDialog fileDlg = new FileDialog(shell,SWT.OPEN|SWT.MULTI); fileDlg.setFilterExtensions(new String[]{"*.mp3","*.wmv","*.rm"}); fileDlg.setFilterPath("F:/资料/My Music"); fileDlg.setText("请选择要打开的音乐文件(支持多选)"); String filePath = fileDlg.open(); if(filePath!=null) { System.out.println("路径:"+fileDlg.getFilterPath()); String[] files = fileDlg.getFileNames(); for(int i=0,n=files.length;i 新建一个类 TestPerspective,实现 IPerspectiveFactory 接口,代码如下: package cownew.cownew.perspectiveTest; import org.eclipse.ui.IPageLayout; import org.eclipse.ui.IPerspectiveFactory; public class TestPerspective implements IPerspectiveFactory { public void createInitialLayout(IPageLayout layout) { layout.addView("org.eclipse.jdt.ui.PackagesView", IPageLayout.LEFT, 0.3f, IPageLayout.ID_EDITOR_AREA); layout.addView("org.eclipse.jdt.ui.JavadocView", IPageLayout.TOP, 0.3f, IPageLayout.ID_EDITOR_AREA); layout.addView("org.eclipse.ui.views.PropertySheet", IPageLayout.BOTTOM, 0.3f, IPageLayout.ID_EDITOR_AREA); } } IPageLayout 的 addView 方法的作用是向透视图中添加视图,它需要以下 4 个参数: l 视图的唯一标识,与 plugin.xml 中定义的一致。 l 参考部分中的相对位置,可以是 IPageLayout.TOP、IPageLayout.BOTTOM、IPageLayout.LEFT 或 IPageLayout.RIGHT。 l 参考部分中当前占有的空间比率,值范围在 0.05f~0.95f 之间。 l 参考部分唯一标识;例中使用的是编辑区域(IPageLayout.ID_EDITOR_AREA)。 运行以后,效果如图 3.22 所示。 图 3.22 演示透视图 还可以调用 addActionSet 等方法向透视图中添加菜单、工具条等。 3.8.8 关于工具条路径 在运行 Eclipse 插件向导中的“Hello,World!”向导的时候,很多读者都会提出这样的问题:菜单和工 具栏按钮只能显示在那个地方吗?能不能把菜单项加入主菜单的【文件】选项下呢? 下面来看一下 plugin.xml 文件。 menu 标记中定义了一个新的菜单,并且用 separator 定义了子菜单。 action 标记中 menubarPath 定义了此 action 对应的菜单项的路径,toolbarPath 定义了此 action 对应的工具 条的路径,这两个属性的值都为本插件特有的,只要修改它们就可以把它们放到其他位置。Eclipse 的标准菜 单都定义了固定的 path,只要把我们的 action 的 path 指向它们即可,下面来稍作修改: 运行后效果图如图 3.23 所示,可以看到菜单已经放到了我们期望的位置。 图3.23 运行后的效果图 (1) 下面是常见的菜单的菜单路径。 l file/fileStart:【文件】的开始区。 l file/new/additions:【文件】的【新建】菜单内部的【附加】组。 l file/new.ext:【文件】的【新建】区。 l file/close.ext:【文件】的【关闭】区。 l file/save.ext:【文件】的【保存】区。 l file/print.ext:【文件】的【打印】区。 l file/open.ext:【文件】的【打开】区。 l file/import.ext:【文件】的【导入】区。 l file/additions:【文件】的【附加】区。 l file/mru:【文件】的【最近的文档】区。 l file/fileEnd:【文件】的【结束】区。 l edit/editStart:【编辑】的【开始】区。 l edit/undo.ext:【编辑】的【撤销】区。 l edit/cut.ext:【编辑】的【剪切】区。 l edit/find.ext:【编辑】的【查找】区。 l edit/add.ext:【编辑】的【添加】区。 l edit/fileEnd:【编辑】的【结束】区。 l edit/additions:【编辑】的【附加】区。 l org.eclipse.jdt.ui.refactoring.menu:【重构】区。 l project/projStart:【项目】的【开始】区。 l project/open.ext:【项目】的【打开】区。 l project/build.ext:【项目】的【建立】区。 l project/additions:【项目】的【附加】区。 l project/projEnd:【项目】的【结束】区。 l org.eclipse.ui.run:【运行】区。 l window/additions:【窗口】的【附加】区。 l window/additionsend:【窗口】的【结束】区。 l help/helpStart:【帮助】的【开始】区。 l help/group.main.ext:【帮助】的【主要组】区。 l help/group.tutorials:【帮助】的【教程组】区。 l help/group.tools:【帮助】的【工具组】区。 l help/group.updates:【帮助】的【更新组】区。 l help/helpEnd:【帮助】的【结束】区。 l help/additions:【帮助】的【附加】区。 l help/group.about.ext:【帮助】的【关于】区。 (2) 下面是常见的工具条的工具条路径。 l org.eclipse.ui.workbench.file/new.ext:【文件】的【新建】区。 l org.eclipse.ui.workbench.file/save.ext:【文件】的【保存】区。 l org.eclipse.ui.workbench.file/print.ext:【文件】的【打印】区。 l org.eclipse.ui.workbench.file/build.ext:【文件】的【建立】区。 l org.eclipse.ui.workbench.navigate:【导航】区。 l org.eclipse.debug.ui.launchActionSet:【启动】区。 l org.eclipse.search.searchActionSet:【搜索】区。 3.8.9 Eclipse 的日志 在调试插件的时候经常遇到插件运行异常的情况,Eclipse 默认并不会将异常打印到控制台而是记录到它 的日志系统中去;我们把插件给用户使用的时候也许出现各种问题,这时候我们最希望得到的就是其系统日 志,这样可以对插件的运行状况进行分析。查看日志的方法如下。 选择【窗口】|【显示视图】|【其他】|【PDE 运行时】|【错误日志】命令,在这个视图中就显示了系 统的日志。 图 3.24 错误日志视图 每一条消息就是一条日志,双击每一条就可以查看消息的详细信息(对于异常消息一般显示的是异常堆 栈),如图 3.25 所示。 图 3.25 事件详细信息 还可以单击导出日志”图标 将日志导出成文件。建议在开发调试插件的时候打开此视图,以便及时发 现系统运行异常。 第 6 章 基于 GEF 的界面设计工具① 在有的情况下,像 CowNewStudio 这样的普通界面的编辑器是不能满足要求的,比如工作流编辑器、界 面设计器、UML 建模工具等,如果要完成这样功能的插件就必须使用图形界面来完成。为了方便开发图形 ① 本章代码位于随书光盘的“代码\ch6\UIDesigner”目录下。 化的插件,Eclipse 提供了 GEF 框架这个图形编辑框架,使用 GEF 可以很容易地实现一个实用的图形化编辑 器。本 章 我们将会以一个界面设计工具来讲解 GEF 的使用,用 户 可以很容易地将这个界面设计工具改造为报 表设计器、工作流编辑器等实用的工具。 6.2 系 统 需 求 本章选择界面设计工具作为 GEF 的开发案例,这 主 要 是 考虑到报表设计器、工作流编辑器等与具体的业 务结合过于紧密,在开发过程中会涉及到很多与 GEF 没有直接关系的东西,而界面设计器则相对比较单纯, 并且会涉及到 GEF 中大部分主流的内容,我们可以根据需要很容易地将界面设计器改造成符合特定业务需要 的工具。 6.2.1 界面设计工具的分类 以往一切界面代码都是要开发人员手工书写,这无疑增加了开发难度,Delphi、VB 等工具的出现扭转了 这个局面,使用这些工具开发人员只要在组件面板上拖拖拽拽就可以完成界面的设计,做到了“所见即所得” 的开发方式。按照界面的保存方式来划分,GUI 设计工具可以分成如下 3 类: l 基于界面文件的纯代码生成方式。 l 代码生成与界面文件结合的方式。 l 无界面文件方式。 (1) 基于界面文件的纯代码生成:NetBeans 是这类工具的典型代表,NetBeans 中与界面设计有关的有两 种文件:*.form 文件和*.java 文件。*.form 文件中是以 XML 格式描述界面布局和组件的属性等信息;*. java 文件则是通过解析*.form 文件生成的代码,生成的界面代码主要位于 initComponents 方法中,这个方法在 NetBeans IDE 中是无法手工编辑的。在用户拖拉组件的时候,NetBeans 就将拖拉的组件描述增加到*.form 文 件中,并且即时将新的代码生成到*.java 文件中。这样实现的好处有如下几点:IDE 实现容易,IDE 的开发 人员只要关注于如何将界面信息转化为*.form 文件和如何将*.form 文件解析生成*.java 代码即可,无需关心 用户修改*.java 代码造成的反向解析问题;*.java 文件可以脱离*.form 而存在,也就是*.form 文件只是在设计 期有意义,而在运行期是无用的。其缺点是用户无法手工修改生成的代码。 (2) 代码生成与界面文件结合:Delphi 和 VB 是这类工具的典型代表。以 Delphi 为例,在 Delphi 中新建 以后界面以后将会存在两个文件:.dfm 和.pas,.dfm 描述了界面布局和组件的属性等信息,.pas 则定义了组 件的变量和事件处理函数。在编译的时候.dfm 被编译到可执行文件中,运行的时候动态解析.dfm 文件来构建 界面。与 NetBeans 不同的就是.dfm 文件是有运行期的意义的,如果没有.dfm 文件,程序将无法编译运行。 这样的方式通常只适用于 Delphi、VB 这样代码和 IDE 结合过于紧密的语言,很难将生成的代码进行手工修 改。 (3) 无界面文件方式:Eclipse 的 Visual Editor 是最经典的例子。使用 Visual Editor 进行 GUI 绘制的时候, 只存在一个*.java 文件,Visual Editor 将用户绘制的界面直接解析为*.java 代码,如果用户修改了*.java 代码, Visual Editor 会运行一个虚拟机,在虚拟机中运行用户修改后的*.java 文件,然后就可以得到运行时的程序界 面并将这个界面绘制到窗口设计器中了。这样做可以将所有的界面信息都集成到一个文件中,并且支持用户 手工修改生成的代码;由于设计器中的界面是通过另外一个虚拟机运行而得到的,在界面设计器中看到的界 面就是运行时的界面,这样保证了真正的“所见即所得”。这样做的坏处也是明显的,由于需要重新启动一 个虚拟机,导致了速度很慢,资源占用比较高,使用 Visual Editor 的时候经常造成 Eclipse 内存不足退出。 由于 HTML 代码本身就是一个树状模型,无需进行代码和模型间的转换,所以网页设计器就不存在上面 所说的这些类别了。 为了降低开发难度,我们在案例系统中采用基于界面文件的纯代码生成方式,用户绘制的界面信息保存 在单独的*.ui 文件中,在用户修改界面并保存的时候,我们自定义的界面构建器就会被启动,界面构建器会 读取并解析*.ui 文件然后生成*.java 文件。如果今后由于系统需求(主要是要求允许开发人员修改生成的代码) 需要改用无界面文件方式的话就可以借鉴 Visual Editor 的思路。不过如果完全采用 Visual Editor 的无界面文 件方式的话会导致资源占用太大,因此可以采用了另外一种思路,也就是在内存中为每个界面维护一个对象 模型(树状结构),在用户绘制界面的时候去修改这个对象模型,在用户保存界面的时候去解析这个对象模型 生成*.java 源代码;在由*.java 源代码加载绘制设计器中的界面的时候,首先通过解析*.java 源代码生成源代 码的抽象语法树(将*.java 源代码解析为抽象语法树的过程可以使用 JDT 来完成),然后解析这个抽象语法树 生成界面的对象模型,这样就可以很轻松地绘制界面了。这样做不仅有 Visual Editor 的优点,而且占用资源 比较小;不过由于手工修改代码的千差万别,如果开发人员修改的代码采用了比较生僻的语法,有可能造成 用户修改的代码无法正确地解析为对象模型,造成*.java 源代码加载绘制设计器中的界面的时候发生异常, 解决这个问题的唯一一个办法就是建议开发人员尽量采用常用的代码来修改生成的界面代码。 6.2.2 功能描述 作为界面设计器必须的功能,系统必须提供对按钮、编辑框、标签、复选按钮、单选按钮、下拉列表框、 列表框等基本组件及其基本属性的支持,为了降低系统开发的复杂性,这里不要求提供对用户自定义组件的 支持,不提供对 JPanel、JScrollPane、JTabbedPane 等复合组件的支持。 用户可以将组件从组件面板中拖放到界面编辑器中,并能够对组件进行移动、缩放、删除、修改等操作, 为了使得开发人员能够更加容易地对组件进行布局,编辑器必须提供对选定的多个组件进行对齐(上下左右 4 个方向)、等宽、等高等操作。 当用户将组件从组件面板中拖放到界面编辑器的时候,编辑器必须为组件提供一个默认的 Id,此 Id 不能 与其他组件的 Id 重复;用户可以修改生成的 Id,但是修改后的 Id 同样不能与其他组件的 Id 重复。 界面编辑器只须提供按绝对坐标进行定位的 XYLayout 即可;当用户对界面进行修改并且保存以后会立 即生成的对应的 Java 代码,Java 代码使用 Swing 图形框架进行实现。 为了降低系统的开发难度,可以将窗体模型对象直接序列化作为界面文件,当打开界面文件的时候再将 此文件反序列化为窗体模型对象。这样做会导致多版本序列化问题,当模型发生较大变化的时候有可能造成 系统工作不正常,因此应该考虑提供对以后使用其他方式保存界面文件的支持。 6.3 构 建 模 型 完成一个完整的 GEF 应用通常需要如下几个步骤。 (1) 建立模型。 (2) 建立视图(Figure)。 (3) 建立控制器(EditPart)。 (4) 建立编辑策略和 Command。 可以看到建立模型是完成系统的第一步。模型是根据具体系统的需求来得到的,比如界面设计器中的模 型包括代表窗口的 Form、代表按钮的 Button、代表编辑框的 Edit 和代表复选按钮的 CheckBox 等对象。模型 的一个职责是负责将自身的改变通知给控制器,因此可编写一个提供这些功能的抽象类 Element,其他模型 必须从 Element 继承。 【代码 6-1】模型抽象类: package com.cownew.uidesigner.model; import Java.beans.PropertyChangeListener; import Java.beans.PropertyChangeSupport; import Java.io.IOException; import Java.io.ObjectInputStream; import Java.io.Serializable; abstract public class Element implements Serializable { static final long serialVersionUID = 1; transient protected PropertyChangeSupport listeners = new PropertyChangeSupport(this); public void addPropertyChangeListener(PropertyChangeListener listener) { listeners.addPropertyChangeListener(listener); } protected void firePropertyChange(String prop, Object old, Object newValue) { listeners.firePropertyChange(prop, old, newValue); } protected void fireStructureChange(String prop, Object child) { listeners.firePropertyChange(prop, null, child); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); listeners = new PropertyChangeSupport(this); } public void removePropertyChangeListener(PropertyChangeListener listener) { listeners.removePropertyChangeListener(listener); } } 由于模型对象要被序列化保存到界面文件中,所以 Element 类要实现 Serializable 接口;Element 类中提 供了 addPropertyChangeListener、firePropertyChange、fireStructureChange 和 removePropertyChangeListener 方 法用来供控制器注册监听器、通知监听器模型改变、移除监听器,这几个方法使用 JDK 内 置的 PropertyChangeSupport 来实现;添加的监听器有可能是无法序列化的对象,为了防止序列化的时候造成异常, 这里将 listeners 声明为不可序列化的,并且编写了 readObject 方法用来改变默认的序列化方式。 表示整个界面的类为 Form,Form 用来容纳所有定义的组件。 【代码 6-2】界面模型类: package com.cownew.uidesigner.model; import Java.io.ByteArrayInputStream; import Java.io.ByteArrayOutputStream; import Java.io.IOException; import Java.io.InputStream; import Java.io.ObjectInputStream; import Java.io.ObjectOutputStream; import Java.util.ArrayList; import Java.util.List; public class Form extends Element { static final long serialVersionUID = 1; public static String COMPONENTS = "components"; protected List components = new ArrayList(); public void addComponent(Component component) { components.add(component); fireStructureChange(COMPONENTS, components); } public void removeComponent(Component component) { components.remove(component); fireStructureChange(COMPONENTS, components); } public List getComponents() { return this.components; } public InputStream getAsStream() throws IOException { ByteArrayOutputStream os = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(os); out.writeObject(this); out.close(); InputStream istream = new ByteArrayInputStream(os.toByteArray()); os.close(); return istream; } public static Form makeFromStream(InputStream istream) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(istream); Form form = (Form) ois.readObject(); ois.close(); return form; } } components 属性中保存的是界面中所有的组件,可以通过 addComponent 和 removeComponent 方法向 界面中添加组件,也可以通过 getComponents 方法得到界面中所有的组件;为了支持将界面序列化为界面 文件,Form 类提供了 getAsStream 方法用来返回界面对象序列化的流;为了将界面对象序列化的流反向解 析为 Form 对象,相应地提供了静态方法 makeFromStream。getAsStream 方法和 makeFromStream 方法的实 现都是简单的流操作,对 Java 中的流操作不熟悉的读者可以参考相应的资料。 系统中所有的组件都有 Id、位置等属性,为了抽象出来这些公共的属性,我们提供了组件基类 Component。 【代码 6-3】组件模型基类: package com.cownew.uidesigner.model; import Java.util.List; import org.eclipse.draw2d.geometry.Rectangle; import org.eclipse.jface.viewers.ICellEditorValidator; import org.eclipse.ui.views.properties.IPropertyDescriptor; import org.eclipse.ui.views.properties.IPropertySource; import org.eclipse.ui.views.properties.TextPropertyDescriptor; import com.cownew.uidesigner.common.ComponentIdManager; abstract public class Component extends Element implements IPropertySource { static final long serialVersionUID = 4; public static final String BOUNDS = "BOUNDS"; public static final String ID = "ID"; private Rectangle bounds; private String id = "component"; private Form form; public Form getForm() { return form; } public void setForm(Form form) { this.form = form; } public Rectangle getBounds() { return bounds; } public void setBounds(Rectangle bounds) { this.bounds = bounds; firePropertyChange(BOUNDS, null, bounds); } public void setId(String id) { if (this.id.equals(id)) { return; } this.id = id; firePropertyChange(ID, null, id); } public String getId() { return id; } public Object getEditableValue() { return this; } public IPropertyDescriptor[] getPropertyDescriptors() { TextPropertyDescriptor idDesc = new TextPropertyDescriptor(ID, "Id"); idDesc.setValidator(new ICellEditorValidator() { public String isValid(Object value) { String id = (String) value; ComponentIdManager idMgr = new ComponentIdManager(getForm()); // 防止 id 重名 if (idMgr.isIdConflicted(id, Component.this)) { return "组件 Id:" + id + "已经存在"; } return null; } }); IPropertyDescriptor[] descriptors = new IPropertyDescriptor[] { idDesc }; return descriptors; } public Object getPropertyValue(Object id) { if (id.equals(ID)) { return getId(); } return null; } public boolean isPropertySet(Object id) { return true; } public void resetPropertyValue(Object id) { } public void setPropertyValue(Object id, Object value) { if (id == ID) { setId((String) value); } } /** * 生成代码片段(生成代码的逻辑本不应该和模型混在一起,不过由于代码生成逻辑目前不是 很复杂,所以暂时这么做 * @param parentId 父组件的 id * @return 代码段,List 中每一个元素是一个类型为 String 的代码行 */ public abstract List generateCode(String parentId); } 为了简化实现,这里 Component 直接实现了 IPropertySource 接口,而不像上一章讲的那样使用适配对象 来实现 IPropertySource 接口;bounds、id 属性分别代表组件的位置和 Id;为了能在组件中方便地引用组件所 在的窗口,将组件所在的窗口对象保存在 form 属性中。 为了校验用户输入的组件 Id 是否重复,在 getPropertyDescriptors 方法中我们为代表 id 的属性描述器 idDesc 增加了验证器,在验证器中调用 ComponentIdManager 进行 Id 唯一性验证,关于 ComponentIdManager 的实现我们将在后边介绍。 generateCode 是用来进行代码生成的,参数 parentId 表示组件的父组件 Id,返回值为 List 类型,List 中 每一个元素为一个代码行,代码行只用处理本行内的缩进即可,无需考虑其上下文环境的缩进。按照面向对 象的理论,生成代码不属于模型的职责,应该将 generateCode 抽取到一个单独的类中完成,不应该和模型混 在一起,不过由于目前代码生成逻辑比较简单,所以暂时将代码生成逻辑放在模型中,以后可以根据需要进 行重构。 Component 中我们进行 Id 唯一性验证的时候使用了 ComponentIdManager,Component- IdManager 是用来 进行唯一组件 Id 生成和组件 Id 唯一性校验的管理器,其实现如下。 【代码 6-4】组件 Id 管理器: package com.cownew.uidesigner.common; import Java.util.List; import com.cownew.ctk.common.NumberUtils; import com.cownew.uidesigner.model.Component; import com.cownew.uidesigner.model.Form; /** * 组件 Id 管理器 * @author 杨中科 */ public class ComponentIdManager { private Form form; public ComponentIdManager(Form form) { super(); this.form = form; } /** * 用来生成此 UI 中唯一的组件 id * @param component * @return */ public String generateId(Component component) { String modelName = component.getClass() .getSimpleName().toLowerCase(); List components = form.getComponents(); // 同类组件的最大序列号 int maxSeqNum = 0; for (Component comp : components) { String compId = comp.getId(); if (compId.startsWith(modelName)) { String tail = compId.substring(modelName.length(), compId .length()); if (NumberUtils.isInteger(tail)) { int intTail = Integer.parseInt(tail); if (intTail > maxSeqNum) { maxSeqNum = intTail; } } } } return modelName + (maxSeqNum + 1); } /** * id 是否重复 * @param id * @param component * @return */ public boolean isIdConflicted(String id, Component component) { List components = form.getComponents(); for (Component comp : components) { String compId = comp.getId(); if (compId.equals(id) && component != comp) { return true; } } return false; } } ComponentIdManager 接受一个窗体的对象作为参数,这样这个管理器就成为这个窗体的组件 Id 管理器 了;generateId 方法用来创建组件 component 的唯一 Id,在 generateId 方法中遍历窗体中的每一个组件,并计 算出所有组件中序列号最大的序列号,然后将这个序列号作为这个组件的 Id;isIdConflicted 方法用来判断组 件 component 的 Id 是否重复,在 isIdConflicted 方法中遍历窗体中的每一个组件,如果一个不是 component 的组件的 Id 也等于 id 的话则说明 id 重复了。 6.8 代码生成和构建器 上一节我们实现了界面编辑器,但是如果只有界面文件的话这个界面设计工具是没有任何意义的,我们 必须提供由界面文件生成 Java 代码的功能,为了能够及时地将用户绘制的界面转换为 Java 代码,我们可以 使用构建器来保证在界面文件保存的时候立即能够生成 Java 代码。 6.8.1 代码生成 与前面的章节类似,这里的代码生成同样使用 JET 来完成,下面即是生成 Java 代码的 JET 代码。 【代码 6-27】代码生成 JET 代码: <%@ jet package="com.cownew.uidesigner.builder" imports = "Java.util.* com.cownew.uidesigner.model.*" class="JavaCodeGenerator" %> <% ArgInfo argInfo = (ArgInfo)argument; Form form = argInfo.getForm(); String className = argInfo.getClassName(); String packageName = argInfo.getPackageName(); %> package <%=packageName%>; import Javax.swing.*; import Java.awt.*; public class <%=className%> extends JFrame { private static final long serialVersionUID = 1L; private JPanel jContentPane = null; public static void main(String[] args) { <%=className%> frame = new <%=className%>(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } public <%=className%>() { super(); initialize(); } private void initialize() { this.setSize(800, 600); this.setContentPane(getJContentPane()); this.setTitle("JFrame"); } private JPanel getJContentPane() { if (jContentPane != null) { return jContentPane; } jContentPane = new JPanel(); jContentPane.setLayout(null); <% List components = form.getComponents(); for(Component component:components) { List srcList = component.generateCode("jContentPane"); for(String src:srcList) { %> <%=src%> <% } } %> return jContentPane; } } 这段 JET 代码是非常简单的,稍微有 Swing 基础的人都能看懂,这里不再做过多解释。需要注意的就是 传递给 JET 代码参数的 ArgInfo 类是定义的一个 JavaBean,通过这个 JavaBean 可以将要生成代码的 Form 对 象、生成的类名和生成的包名等信息传递给 JET。 【代码 6-28】代码生成信息类: package com.cownew.uidesigner.builder; import com.cownew.uidesigner.model.Form; public class ArgInfo { private Form form; private String className; private String packageName; protected String getPackageName() { return packageName; } protected void setPackageName(String packageName) { this.packageName = packageName; } protected String getClassName() { return className; } protected void setClassName(String className) { this.className = className; } protected Form getForm() { return form; } protected void setForm(Form form) { this.form = form; } } 6.8.2 构建器 构建器又叫增量式项目构建器,只要相关项目中的资源发生改变,构建器就会自动执行。比如,当创建 或者修改 Java 源代码文件的时候,Java 构建器就会构建这个 Java 源代码文件生成类文件。如果批处理这些 修改的话,构建器将会收到包含所有发生变化的资源列表的唯一一条通知消息,而不是针对每一个变化的资 源均收到一条通知消息。需要注意的就是构建器必须增量地执行,也就是只有那些发生改变的派生资源进行 构建,如果每次改变一个资源都要对有所资源进行构建的话就会非常占用资源。 Eclipse 中的构建器一般从 IncrementalProjectBuilder 派生,IncrementalProjectBuilder 类中定义了一个抽象 方法 build,这个方法的签名如下: protected IProject[] build(int kind, Map args, IProgressMonitor monitor) 参数含义如下: l kind ——表示构建类型,有如下几个有效值:FULL_BUILD 、 INCREMENTAL_BUILD 和 AUTO_BUILD。FULL_BUILD 指示构建器应当重新构建所有的资源;INCREMENTAL_BUILD 指示 构建器只重新构建有更新的资源;AUTO_BUILD 和 INCREMENTAL_BUILD 一样,不过构建过程由 增量式构建自动触发(自动构建选项要打开)。 l args——指定构建器参数的一个映射,这些参数以参数名为 key,这个参数也可以为空,表示空映射。 l monitor——进度监视器。 除了 build 方法之外,IncrementalProjectBuilder 类中还定义了下面几个重要方法: l forgetLastBuildState()——请求构建器忘记以前构建的时候可能缓存的状态。如果构建过程被中断或 者取消,则需要子类调用此方法以防止下次构建出错。 l getDelta()——返回上次运行构建器以后资源的更改情况,如果没有任何更改则返回 null。 l getProject()——返回当前被构建的项目。 l isInterrupted()——返回是否对该构建过程发出了中断请求。 界面文件构建器 UIBuilder 同样从 IncrementalProjectBuilder 派生出来,第一个要实现的方法就是 build。 【代码 6-29】界面文件构建器: public class UIBuilder extends IncrementalProjectBuilder { // BuilderId 比较奇怪,它是由“插件 id”+“.”+“构建器 id”组成的 public static final String BUILDER_ID = "com.cownew.uidesigner.UIBuilder"; protected IProject[] build(int kind, Map args, IProgressMonitor monitor) throws CoreException { if (kind == FULL_BUILD) { fullBuild(monitor); } else { IResourceDelta delta = getDelta(getProject()); if (delta == null) { fullBuild(monitor); } else { incrementalBuild(delta, monitor); } } return null; } private void fullBuild(final IProgressMonitor monitor) throws CoreException { getProject().accept(new UIFullBuildVisitor(monitor)); } private void incrementalBuild(IResourceDelta delta, IProgressMonitor monitor) throws CoreException { delta.accept(new UIDeltaVisitor(monitor)); } } UIBuilder 的核心方法就是 build,在 build 中根据 kind 的不同取值来调用 fullBuild 或者 incrementalBuild 进行全面构建或者增量构建。当进行全面构建的时候,我们在项目上调用 accept 方法,使用自定义的 UIFullBuildVisitor 遍历构建所有资源;当进行增加构建的时候我们在增量资源上调用 accept 方法,使用自定 义的 UIDeltaVisitor 遍历构建所有更改的资源。 这里的 UIFullBuildVisitor 和 UIDeltaVisitor 都使用的是设计模式中最常用的访问者模式。首先来看较简 单的 UIFullBuildVisitor。 【代码 6-30】全构建访问者: class UIFullBuildVisitor implements IResourceVisitor { private IProgressMonitor monitor; public UIFullBuildVisitor(IProgressMonitor monitor) { this.monitor = monitor; } public boolean visit(IResource resource) { if (!(resource instanceof IFile)) { return true; } String ext = resource.getFileExtension(); if (!ext.equalsIgnoreCase("ui")) { return true; } BuilderUtils.buildUI((IFile) resource, monitor); return true; } } 此类的核心就是 visit 方法,在 visit 方法中判断以后被构建的文件以.ui 为扩展名才进行构建,构建的具 体过程委托给自定义的 BuilderUtils 工具类的 buildUI 方法来完成。 UIDeltaVisitor 的实现与 UIFullBuildVisitor 比起来略显复杂。 【代码 6-31】增量构建访问者: class UIDeltaVisitor implements IResourceDeltaVisitor { private IProgressMonitor monitor; public UIDeltaVisitor(IProgressMonitor monitor) { this.monitor = monitor; } public boolean visit(IResourceDelta delta) throws CoreException { IResource resource = delta.getResource(); if (!(resource instanceof IFile)) { // 返回值表示是否继续遍历 return true; } String ext = resource.getFileExtension(); if (!ext.equalsIgnoreCase("ui")) { return true; } IFile file = (IFile) resource; switch (delta.getKind()) { // 增加了一个 AUI 文件 case IResourceDelta.ADDED: BuilderUtils.buildUI(file, monitor); break; // AUI 文件被删除 case IResourceDelta.REMOVED: BuilderUtils.deleteJava(file, monitor); break; // AUI 文件被编辑 case IResourceDelta.CHANGED: BuilderUtils.buildUI(file, monitor); break; } return true; } } 在 visit 方法中调用 delta 的 getKind()方法来判断更改的类型是什么,如果更改的类型是文件增加(ADDED) 或者文件修改(CHANGED)的话则调用工具类 BuilderUtils 的 buildUI 方法重新构建界面文件;如果更改的类 型是文件被删除(REMOVED)则调用工具类 BuilderUtils 的 deleteJava 方法删除界面文件生成的 Java 源代码。 构建器工具类 BuilderUtils 的实现非常清晰明了,可以直接阅读代码进行研究,这里不做过多的解释。 【代码 6-32】构建器工具类: package com.cownew.uidesigner.builder; import Java.io.IOException; import Java.io.InputStream; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IPackageFragment; import org.eclipse.jdt.core.JavaCore; import com.cownew.ctk.io.ResourceUtils; import com.cownew.uidesigner.Activator; import com.cownew.uidesigner.common.CodeGenUtils; import com.cownew.uidesigner.model.Form; public class BuilderUtils { /** * 构建界面文件 file * * @param file * @param monitor */ public static void buildUI(IFile file, IProgressMonitor monitor) { IJavaElement JavaElement = JavaCore.create(file.getParent()); // 当第二次构建的时候 bin 目录中的 ui 也会被构建一次 // 这样就造成 JavaElement 为 null 了,所以需要判断一下 if ((JavaElement instanceof IPackageFragment) == false) { return; } IPackageFragment pckFragment = (IPackageFragment) JavaElement; // 得到短文件名 String fileName = file.getName(); int extLen = file.getFileExtension().length() + 1; // 去掉扩展名就得到文件对应的类名 String className = fileName.substring(0, fileName.length() - extLen); // 构建参数 ArgInfo argInfo = new ArgInfo(); argInfo.setClassName(className); argInfo.setPackageName(pckFragment.getElementName()); InputStream instream = null; try { instream = file.getContents(); // 得到界面文件的模型对象 Form form = Form.makeFromStream(instream); argInfo.setForm(form); JavaCodeGenerator codeGen = new JavaCodeGenerator(); // 生成界面文件对应的代码 String code = codeGen.generate(argInfo); // 得到界面文件对应的 Java 源代码文件名 IFile JavaPath = uiFileToJavaFile(file); // 将代码保存到 Java 源代码文件 CodeGenUtils.saveToFile(JavaPath, code, monitor); } catch (CoreException e) { Activator.logException(e); } catch (IOException e) { Activator.logException(e); } catch (ClassNotFoundException e) { Activator.logException(e); } finally { ResourceUtils.close(instream); } } private static IFile uiFileToJavaFile(IFile file) { String uiPathName = file.getName(); // 将 ui 后缀替换为 Java 后缀就得到源码文件名了 // 有点不严谨,有待改进 String JavaName = uiPathName.replace(".ui", ".Java"); IFile pyPath = file.getParent().getFile(new Path(JavaName)); return pyPath; } /** * 删除界面文件 file 对应的 Java 源码文件 * * @param file * @param monitor */ public static void deleteJava(IFile file, IProgressMonitor monitor) { // 得到界面文件对应的 Java 文件 IFile JavaPath = uiFileToJavaFile(file); try { // 删除对应的 Java 文件 JavaPath.delete(true, monitor); } catch (CoreException e) { Activator.logException(e); } } } 这样界面文件构建器就完成了,最后将构建器添加到 plugin.xml 中即可: 6.8.3 为项目增加构建器 构建器必须被安装到具体的项目中才能发挥作用。最常见的为项目增加构建器的方式是自定义一个项目 新建向导,在这个向导中为新建的项目增加构建器,但是这样做对于我们的界面设计工具来说就过于复杂了。 为项目增加构建器其实只要修改项目的项目描述即可,因此我们准备为系统增加一个右键菜单项【添加 UI 构建器】,当用户选择某个项目的时候这个菜单项就显示出来,用户单击这个菜单项就可以轻松地为已有的 项目增加构建器。 Eclipse 中的菜单项一般都对应一个 Action 类,这里的菜单项就是 AddBuilderAction,它实现了 IObjectActionDelegate 接口。 【代码 6-33】增加构建器的 Action: package com.cownew.uidesigner.builder; import Java.util.Iterator; import org.eclipse.core.resources.ICommand; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProjectDescription; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.jface.action.IAction; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.ui.IObjectActionDelegate; import org.eclipse.ui.IWorkbenchPart; import com.cownew.uidesigner.Activator; public class AddBuilderAction implements IObjectActionDelegate { private ISelection selection; public void run(IAction action) { if (selection instanceof IStructuredSelection) { for (Iterator it = ((IStructuredSelection) selection) .iterator(); it.hasNext();) { Object element = it.next(); IProject project = null; if (element instanceof IProject) { project = (IProject) element; } else if (element instanceof IAdaptable) { project = (IProject) ((IAdaptable) element) .getAdapter(IProject.class); } if (project != null) { addUIBuilder(project); } } } } public void selectionChanged(IAction action, ISelection selection) { this.selection = selection; } public void setActivePart(IAction action, IWorkbenchPart targetPart) { } private void addUIBuilder(IProject project) { try { //得到项目描述 IProjectDescription desc = project.getDescription(); //得到项目所有的构建器 ICommand[] commands = desc.getBuildSpec(); for (int i = 0; i < commands.length; ++i) { if (commands[i].getBuilderName() .equals(UIBuilder.BUILDER_ID)) { return; } } ICommand[] newCommands = new ICommand[commands.length + 1]; System.arraycopy(commands, 0, newCommands, 0, commands.length); //新建一个构建器 ICommand command = desc.newCommand(); command.setBuilderName(UIBuilder.BUILDER_ID); newCommands[newCommands.length - 1] = command; //将新设置的构建器添加进去 desc.setBuildSpec(newCommands); project.setDescription(desc, null); } catch (CoreException e) { Activator.logException(e); } } } 可以看到代码的核心方法就是 addUIBuilder,在 addUIBuilder 中首先从项目的项目描述中得到所有定义 好的构建器,然后调用 IProjectDescription 的 newCommand 方法得到一个新的构建器,并把构建器设置为这 个新构建器的 Id,最后将设置后的构建器重新添加到项目描述中。需要特别注意的是这里设置的新构建器的 Id 的组成方式是“插件 id”+“.”+“构建器 id”,也就是“com.cownew.uidesigner.UIBuilder”,而非构建器的 ID“UIBuilder”,这一点是值得特别注意的。 最后需要将 AddBuilderAction 添加到 plugin.xml 中去: 6.9 实现常用组件 前面实现了界面设计工具的所有基础功能,但是没有提供常用的组件,比如按钮、标签和编辑框的,因 此本节来实现这些组件,这样界面设计工具就能成为实用的工具了。为了减少不必要的篇幅,这里只实现标 签、按钮、复选按钮、编辑按钮和列表框等 5 种组件,可以根据需要增加其他的组件。 6.9.1 标签组件 标签是程序中最常用的组件之一,标签组件的一个基本特征就是能够设定标签上的文字,为了简化实现, 这里只为标签组件设定一个 “标签文字”的属性。实现一个组件需要实现模型、视图(Figure)、控制器(Part)3 个类,首先来看一下标签组件的模型类。 【代码 6-34】标签模型: package com.cownew.uidesigner.components.label; import Java.util.ArrayList; import Java.util.List; import org.eclipse.ui.views.properties.IPropertyDescriptor; import org.eclipse.ui.views.properties.TextPropertyDescriptor; import com.cownew.ctk.common.StringUtils; import com.cownew.uidesigner.common.CodeGenUtils; import com.cownew.uidesigner.common.ComponentUtils; import com.cownew.uidesigner.model.Component; public class Label extends Component { private static final long serialVersionUID = 1L; public static final String TEXT = "TEXT"; private static IPropertyDescriptor[] descriptors = new IPropertyDescriptor[]{ new TextPropertyDescriptor(TEXT, "Text") }; private String text; public Label() { super(); setText("Label"); } public void setText(String text) { String oldValue = this.text; this.text = text; //通知视图改变 this.firePropertyChange(TEXT, oldValue, text); } protected String getText() { return text; } public IPropertyDescriptor[] getPropertyDescriptors() { IPropertyDescriptor[] sd = super.getPropertyDescriptors(); return ComponentUtils.mergePropDesc(sd, descriptors); } public Object getPropertyValue(Object id) { Object v = super.getPropertyValue(id); if (v != null) { return v; } if (id.equals(TEXT)) { return getText(); } return null; } public void setPropertyValue(Object id, Object value) { super.setPropertyValue(id, value); if (id.equals(TEXT)) { setText((String) value); } } public List generateCode(String parentId) { List list = new ArrayList(); StringBuffer line1 = new StringBuffer(); line1.append("JLabel ").append(getId()).append("= new JLabel(") .append(StringUtils.doubleQuoted(getText())).append(");"); list.add(line1.toString()); StringBuffer line2 = new StringBuffer(); line2.append(getId()).append(".setBounds(").append( CodeGenUtils.translateBounds(getBounds())).append(");"); list.add(line2.toString()); StringBuffer line3 = new StringBuffer(); line3.append(parentId).append(".add(").append( getId()).append(");"); list.add(line3.toString()); return list; } } 模型类Label 提供了设置和读取标签文字的方法 setText、getText,并且覆盖了 getPropertyDescriptors、 getPropertyValue 和 setPropertyValue 等方法以提供对 Text 属性的属性视图支持。由于父类 Component 已经提 供了对 Id、Bounds 等属性的属性视图支持,所以在 getPropertyDescriptors 方法中首先取得父类中的属性描述 器,然后调用 ComponentUtils 工具类的 mergePropDesc 方法来将父类的属性描述器与 Text 属性的属性描述器 融合。 在 generateCode 方法中实现了代码的生成,生成的代码有 3 行,第一行是调用 JLabel 的构造函数生成标 签组件实例,第二行调用 setBounds 方法为组件设定大小和位置,最后一行是将标签组件添加到父组件中去。 接着来看标签视图类的实现。 【代码 6-35】标签视图: package com.cownew.uidesigner.components.label; import org.eclipse.draw2d.Figure; import org.eclipse.draw2d.Graphics; import org.eclipse.draw2d.Label; import org.eclipse.draw2d.PositionConstants; public class LabelFigure extends Figure { private Label label; private String text; public LabelFigure() { super(); label = new Label(); label.setLabelAlignment(PositionConstants.LEFT); add(label); } public void setText(String text) { this.text = text; this.repaint(); } protected void paintFigure(Graphics gc) { super.paintFigure(gc); label.setBounds(getBounds()); label.setText(text); } } Draw2d 内置的 Label 提供了模拟标签组件的功能,因此我们将 Label 作为我们标签组件的显示组件。在构造 函数中,首先创建 Label 类的实例,然后设定 Label 为左对齐,最后调用 add 方法将 Label 添加到视图中去; setText 方法用来修改视图中的标签文字,当文字被修改以后就调用 repaint 方法进行界面重绘;当界面第一次 绘制或者被要求重绘的时候 paintFigure 方法就会被调用,在这个方法中设定 label 的大小、位置和标签文字。 最后需要实现的就是组件的控制器。 【代码 6-36】标签控制器: package com.cownew.uidesigner.components.label; import org.eclipse.draw2d.IFigure; import com.cownew.uidesigner.model.Component; import com.cownew.uidesigner.parts.ComponentPart; public class LabelPart extends ComponentPart { protected IFigure createFigure() { return new LabelFigure(); } protected void doRefreshFigure(IFigure figure, Component component) { LabelFigure lf = (LabelFigure)figure; Label label = (Label)component; lf.setText(label.getText()); } } LabelPart 的实现相对来说比较简单,首先实现 createFigure 方法以供生成控制器对应的视图,接着实现 doRefreshFigure 方法用来根据模型刷新视图。 6.9.2 按钮组件 按钮也是程序中经常用到的组件之一,下面就来实现按钮组件。熟悉 Swing 的朋友都知道,Swing 中的 按钮、单选按钮、复选按钮等组件都从一个抽象类 AbstractButton 中派生而来,为了抽象出这些组件的共同 特征,我们也同样抽象出一 AbstractButton 模型类出来,这样按钮、单选框、复选框等组件就可以从 AbstractButton 派生并很容易地实现各自的模型类了。 【代码 6-37】抽象按钮模型: package com.cownew.uidesigner.model; import org.eclipse.ui.views.properties.IPropertyDescriptor; import org.eclipse.ui.views.properties.TextPropertyDescriptor; import com.cownew.uidesigner.common.ComponentUtils; public abstract class AbstractButton extends Component { public static final String TEXT = "TEXT"; private static IPropertyDescriptor[] descriptors = new IPropertyDescriptor[] { new TextPropertyDescriptor( TEXT, "Text") }; private String text; public AbstractButton() { super(); text = ""; } public String getText() { return text; } public void setText(String text) { String oldValue = getText(); this.text = text; this.firePropertyChange(TEXT, oldValue, text); } public IPropertyDescriptor[] getPropertyDescriptors() { IPropertyDescriptor[] sd = super.getPropertyDescriptors(); return ComponentUtils.mergePropDesc(sd, descriptors); } public Object getPropertyValue(Object id) { Object v = super.getPropertyValue(id); if (v != null) { return v; } if (id.equals(TEXT)) { return getText(); } return null; } public void setPropertyValue(Object id, Object value) { super.setPropertyValue(id, value); if (id.equals(TEXT)) { setText((String) value); } } } 可以看到 AbstractButton 中提供了对 Text 这个属性的设置、读取和属性视图的支持,这样子类组件模型 只要实现各自特有的逻辑即可。 有了 AbstractButton 以后按钮组件的模型类就很容易实现了。 【代码 6-38】按钮模型: package com.cownew.uidesigner.components.button; import Java.util.ArrayList; import Java.util.List; import com.cownew.ctk.common.StringUtils; import com.cownew.uidesigner.common.CodeGenUtils; import com.cownew.uidesigner.model.AbstractButton; public class Button extends AbstractButton { private static final long serialVersionUID = 1L; public List generateCode(String parentId) { List list = new ArrayList(); StringBuffer line1 = new StringBuffer(); line1.append("JButton ").append(getId()).append("= new JButton(") .append(StringUtils.doubleQuoted(getText())).append(");"); list.add(line1.toString()); StringBuffer line2 = new StringBuffer(); line2.append(getId()).append(".setBounds(").append( CodeGenUtils.translateBounds(getBounds())).append(");"); list.add(line2.toString()); StringBuffer line3 = new StringBuffer(); line3.append(parentId).append(".add(").append(getId()).append(");"); list.add(line3.toString()); return list; } } 由于AbstractButton 中已经提供了对 Text 属性的支持,所以 Button 类中只需要提供代码生成的逻辑,这 里代码生成的逻辑实现和 Label 是非常相似的,这里就不做过多的介绍。 下面来看一下按钮视图的实现。 【代码 6-39】按钮视图: package com.cownew.uidesigner.components.button; import org.eclipse.draw2d.ColorConstants; import org.eclipse.draw2d.Figure; import org.eclipse.draw2d.Graphics; import org.eclipse.draw2d.Label; import org.eclipse.draw2d.SchemeBorder; import org.eclipse.swt.graphics.Color; public class ButtonFigure extends Figure { private Label label; private String text; protected static final SchemeBorder.Scheme SCHEME_FRAME = new SchemeBorder.Scheme( new Color[] {ColorConstants.button, ColorConstants.buttonLightest, ColorConstants.button }, new Color[] {ColorConstants.buttonDarkest, ColorConstants.buttonDarker, ColorConstants.button }); public ButtonFigure() { super(); label = new Label(); label.setBorder(new SchemeBorder(SCHEME_FRAME)); add(label); } public void setText(String text) { this.text = text; this.repaint(); } protected void paintFigure(Graphics g) { super.paintFigure(g); label.setBounds(getBounds()); label.setText(text); } } Draw2d 中的 Button 是从 Clickable 派生出来的,也就是这个 Button 是可以响应鼠标事件的,但是我们却 希望视图中的按钮是只可以具有按钮的外观而不可以有按钮的行为的。经过研究 Button 的代码我们发现, Draw2d 的 Button 的视图部分同样是使用 Label 来实现的,只是为 Label 增加了一个边框而已,这样 Label 就 有了按钮的外观,因此 ButtonFigure 中我们同样采用 Label 来作为视图组件,唯一的区别就是为这个 Label 添加了 SchemeBorder 类型的边框。 最后需要实现的就是组件的控制器。 【代码 6-40】按钮控制器: package com.cownew.uidesigner.components.button; import org.eclipse.draw2d.IFigure; import com.cownew.uidesigner.model.Component; import com.cownew.uidesigner.parts.ComponentPart; public class ButtonPart extends ComponentPart { protected IFigure createFigure() { return new ButtonFigure(); } protected void doRefreshFigure(IFigure figure, Component component) { ButtonFigure bf = (ButtonFigure) figure; Button btn = (Button) component; bf.setText(btn.getText()); } } ButtonPart 的实现和标签控制器的实现非常类似。首先实现 createFigure 方法以供生成控制器对应的视图, 接着实现 doRefreshFigure 方法用来根据模型刷新视图。 6.9.3 复选框 Swing 中的 JCheckBox、JRadioButton 等组件都从 JToggleButton 派生,为了抽象出这些组件的共同特征, 我们也同样抽象出一 ToggleButton 模型类出来,这 样 单 选按钮、复选按钮等组件的模型就可以从 ToggleButton 派生并很容易的实现各自的模型类了。 【代码 6-41】开关按钮模型: package com.cownew.uidesigner.model; import org.eclipse.ui.views.properties.IPropertyDescriptor; import com.cownew.Eclipse.properties.BooleanPropertyDescriptor; import com.cownew.uidesigner.common.ComponentUtils; public abstract class ToggleButton extends AbstractButton { private static final long serialVersionUID = 1L; public static final String SELECTED = "Selected"; private static IPropertyDescriptor[] descriptors = new IPropertyDescriptor[] { new BooleanPropertyDescriptor(SELECTED, "Selected") }; private boolean selected; public ToggleButton() { super(); selected = true; } public boolean isSelected() { return selected; } public void setSelected(boolean selected) { Boolean oldValue = Boolean.valueOf(isSelected()); this.selected = selected; this.firePropertyChange(SELECTED, oldValue, Boolean.valueOf(selected)); } public IPropertyDescriptor[] getPropertyDescriptors() { IPropertyDescriptor[] sd = super.getPropertyDescriptors(); return ComponentUtils.mergePropDesc(sd, descriptors); } public Object getPropertyValue(Object id) { Object v = super.getPropertyValue(id); if (v != null) { return v; } if (id.equals(SELECTED)) { return Boolean.valueOf(isSelected()); } return null; } public void setPropertyValue(Object id, Object value) { super.setPropertyValue(id, value); if (id.equals(SELECTED)) { Boolean b = (Boolean) value; setSelected(b.booleanValue()); } } } 可以看到 ToggleButton 中提供了对 Selected 这个属性的设置、读取和属性视图的支持,这样子类组件模 型只要实现各自特有的逻辑即可。 有了 ToggleButton 以后复选按钮组件的模型类就很容易实现了。 【代码 6-42】复选框模型: package com.cownew.uidesigner.components.checkbox; import Java.util.ArrayList; import Java.util.List; import com.cownew.ctk.common.StringUtils; import com.cownew.uidesigner.common.CodeGenUtils; import com.cownew.uidesigner.model.ToggleButton; public class CheckBox extends ToggleButton { public List generateCode(String parentId) { List list = new ArrayList(); StringBuffer line1 = new StringBuffer(); line1.append("JCheckBox ").append(getId()).append("= new JCheckBox(") .append(StringUtils.doubleQuoted(getText())).append(");"); list.add(line1.toString()); StringBuffer line2 = new StringBuffer(); line2.append(getId()).append(".setBounds(").append( CodeGenUtils.translateBounds(getBounds())).append(");"); list.add(line2.toString()); StringBuffer line3 = new StringBuffer(); line3.append(getId()).append(".setSelected(").append(isSelected()) .append(");"); list.add(line3.toString()); StringBuffer line4 = new StringBuffer(); line4.append(parentId).append(".add(").append(getId()).append(");"); list.add(line4.toString()); return list; } } 由于 ToggleButton 中已经提供了对 Selected 属性的支持,所以 CheckBox 类中只需要提供代码生成的逻 辑,这里代码生成的逻辑实现和 Label 是非常相似的,不同的地方就是提供了对 Selected 属性的代码生成的 支持。 下面来看一下复选按钮视图的实现。 【代码 6-43】复选框视图: package com.cownew.uidesigner.components.checkbox; import Java.io.InputStream; import org.eclipse.draw2d.Figure; import org.eclipse.draw2d.Graphics; import org.eclipse.draw2d.Label; import org.eclipse.draw2d.PositionConstants; import org.eclipse.swt.graphics.Image; import com.cownew.ctk.io.ResourceUtils; public class CheckBoxFigure extends Figure { private Label label; private boolean selected; private String text; static final Image UNCHECKED = createImage("off.gif"); static final Image CHECKED = createImage("on.gif"); private static Image createImage(String name) { InputStream stream = (CheckBoxFigure.class).getResourceAsStream(name); Image image = new Image(null, stream); ResourceUtils.close(stream); return image; } public CheckBoxFigure() { super(); label = new Label(); label.setLabelAlignment(PositionConstants.LEFT); add(label); } public void setText(String text) { this.text = text; this.repaint(); } public void setSelected(boolean selected) { this.selected = selected; this.repaint(); } protected void paintFigure(Graphics g) { super.paintFigure(g); label.setBounds(getBounds()); label.setIcon(selected ? CHECKED : UNCHECKED); label.setText(text); } } Draw2d 的 Label 可以设置一个图标,这个图标将会显示在标签的左边,根据这个特性,可以使用 Label 来实现复选按钮的外观。将选中、非选中两种状态的图标(分别是 on.gif 和 off.gif )放在 CheckBoxFigure 包 下,这样使用 createImage 方法就可以得到这个图标,在 paintFigure 中根据 selected 属性值来为 Label 设置不 同的图标,这样就可以达到复选框的外观效果。 最后需要实现的就是组件的控制器。 【代码 6-44】复选框控制器: package com.cownew.uidesigner.components.checkbox; import org.eclipse.draw2d.IFigure; import com.cownew.uidesigner.model.Component; import com.cownew.uidesigner.parts.ComponentPart; public class CheckBoxPart extends ComponentPart { protected IFigure createFigure() { return new CheckBoxFigure(); } protected void doRefreshFigure(IFigure figure, Component component) { CheckBoxFigure cbf = (CheckBoxFigure) figure; CheckBox label = (CheckBox) component; cbf.setText(label.getText()); cbf.setSelected(label.isSelected()); } } 6.9.4 编辑框 编辑框的实现是比较简单的,首先看编辑框模型类。 【代码 6-45】编辑框模型: package com.cownew.uidesigner.components.edit; import Java.util.ArrayList; import Java.util.List; import com.cownew.uidesigner.common.CodeGenUtils; import com.cownew.uidesigner.model.Component; public class Edit extends Component { private static final long serialVersionUID = 1L; public List generateCode(String parentId) { List list = new ArrayList(); StringBuffer line1 = new StringBuffer(); line1.append("JTextField ").append(getId()) .append("= new JTextField(").append(");"); list.add(line1.toString()); StringBuffer line2 = new StringBuffer(); line2.append(getId()).append(".setBounds(").append( CodeGenUtils.translateBounds(getBounds())).append(");"); list.add(line2.toString()); StringBuffer line3 = new StringBuffer(); line3.append(parentId).append(".add(").append(getId()).append(");"); list.add(line3.toString()); return list; } } 编辑框视图的实现也同样使用 Label 来实现编辑框的外观。 【代码 6-46】编辑框视图: package com.cownew.uidesigner.components.edit; import org.eclipse.draw2d.ColorConstants; import org.eclipse.draw2d.Figure; import org.eclipse.draw2d.Graphics; import org.eclipse.draw2d.Label; import org.eclipse.draw2d.SchemeBorder; import org.eclipse.swt.graphics.Color; public class EditFigure extends Figure { private Label label; protected static final SchemeBorder.Scheme SCHEME_FRAME = new SchemeBorder.Scheme( new Color[] { ColorConstants.lightGreen, ColorConstants.lightGreen, ColorConstants.lightGreen }); public EditFigure() { super(); label = new Label(); label.setBorder(new SchemeBorder(SCHEME_FRAME)); add(label); } protected void paintFigure(Graphics g) { super.paintFigure(g); label.setBounds(getBounds()); } } 最后是编辑框的控制器。 【代码 6-47】编辑框控制器: package com.cownew.uidesigner.components.edit; import org.eclipse.draw2d.IFigure; import com.cownew.uidesigner.model.Component; import com.cownew.uidesigner.parts.ComponentPart; public class EditPart extends ComponentPart { protected IFigure createFigure() { return new EditFigure(); } protected void doRefreshFigure(IFigure figure, Component component) { } } 6.9.5 列表框 列表框最基本的特征就是允许设置列表框中的数据,因此为列表框组件模型提供 Items 属性,列表框组 件模型实现如下。 【代码 6-48】列表框模型: package com.cownew.uidesigner.components.listbox; import Java.util.ArrayList; import Java.util.List; import org.eclipse.ui.views.properties.IPropertyDescriptor; import com.cownew.ctk.common.EnvironmentUtils; import com.cownew.ctk.common.StringUtils; import com.cownew.Eclipse.properties.MultiLineStringPropertyDescriptor; import com.cownew.uidesigner.common.CodeGenUtils; import com.cownew.uidesigner.common.ComponentUtils; import com.cownew.uidesigner.model.Component; public class ListBox extends Component { private static final long serialVersionUID = 1L; public static final String ITEMS = "ITEMS"; private static IPropertyDescriptor[] descriptors = new IPropertyDescriptor[] { new MultiLineStringPropertyDescriptor(ITEMS, "items") }; private String items; public ListBox() { super(); items = ""; } public String getItems() { return items; } public void setItems(String items) { String oldValue = getItems(); this.items = items; this.firePropertyChange(ITEMS, oldValue, items); } public IPropertyDescriptor[] getPropertyDescriptors() { IPropertyDescriptor[] sd = super.getPropertyDescriptors(); return ComponentUtils.mergePropDesc(sd, descriptors); } public Object getPropertyValue(Object id) { Object v = super.getPropertyValue(id); if (v != null) { return v; } if (id.equals(ITEMS)) { return getItems(); } return null; } public void setPropertyValue(Object id, Object value) { super.setPropertyValue(id, value); if (id.equals(ITEMS)) { setItems((String) value); } } public List generateCode(String parentId) { List list = new ArrayList(); StringBuffer listData = new StringBuffer(); listData.append("new String[]{"); String[] contents = getItems().split( EnvironmentUtils.getLineSeparator()); for (int i = 0, n = contents.length; i < n; i++) { listData.append(StringUtils.doubleQuoted(contents[i])); if (i < contents.length - 1) { listData.append(","); } } listData.append("}"); StringBuffer line1 = new StringBuffer(); line1.append("JList ").append(getId()).append("= new JList(").append( listData).append(");"); list.add(line1.toString()); StringBuffer line2 = new StringBuffer(); line2.append(getId()).append(".setBounds(").append( CodeGenUtils.translateBounds(getBounds())).append(");"); list.add(line2.toString()); StringBuffer line3 = new StringBuffer(); line3.append(parentId).append(".add(").append(getId()).append(");"); list.add(line3.toString()); return list; } } 模型类中 getPropertyDescriptors、getPropertyValue、setPropertyValue 以及 generateCode 方法的实现和其 他组件的实现是非常类似的,唯一需要注意的就是 Items 的属性描述器,因为 Eclipse 中没有提供 Items 这样 多行文本的属性描述器,因此定义了多行文本属性描述器 MultiLineStringPropertyDescriptor。 【代码 6-49】多行文本属性描述器: package com.cownew.Eclipse.properties; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.viewers.CellEditor; import org.eclipse.jface.viewers.DialogCellEditor; import org.eclipse.jface.viewers.ICellEditorValidator; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.views.properties.PropertyDescriptor; public class MultiLineStringPropertyDescriptor extends PropertyDescriptor { public MultiLineStringPropertyDescriptor(Object id, String displayName) { super(id, displayName); } public CellEditor createPropertyEditor(Composite parent) { CellEditor editor = new MultiLineStringCellEditor(parent); if (getValidator() != null) { editor.setValidator(getValidator()); } return editor; } protected ICellEditorValidator getValidator() { return new ICellEditorValidator() { public String isValid(Object value) { if (value == null) { return "Cannot be null"; } return null; } }; } } MultiLineStringCellEditor 是从 DialogCellEditor 派生的多行文本单元格编辑器,用来提供对话框形式的多 行文本编辑功能。 【代码 6-50】多行文本单元格编辑器: class MultiLineStringCellEditor extends DialogCellEditor { public MultiLineStringCellEditor(Composite parent) { super(parent); } protected Object openDialogBox(Control ctrl) { String oldValue = (String) getValue(); if (oldValue == null) { oldValue = ""; } MultiLineStringDialog dlg = new MultiLineStringDialog(ctrl.getShell(), oldValue); if (dlg.open() == MultiLineStringDialog.OK) { return dlg.getValue(); } return oldValue; } } 当用户单击编辑器中的浏览按钮的时候,openDialogBox 方法就会被调用,以弹出对话框,在 openDialogBox 方法中弹出多行文本对话框 MultiLineStringDialog,并且将对话框中的编辑值作为返回值填充 到编辑器中。多行文本对话框 MultiLineStringDialog 的实现如下。 【代码 6-51】多行文本对话框: class MultiLineStringDialog extends Dialog { private Text text; private String value; private String initValue; protected MultiLineStringDialog(Shell shell, String initValue) { super(shell); if (initValue == null) { this.initValue = ""; } else { this.initValue = initValue; } } protected Control createDialogArea(Composite parent) { Composite composite = (Composite) super.createDialogArea(parent); FillLayout layout = new FillLayout(); composite.setLayout(layout); text = new Text(composite, SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL); text.setText(initValue); return composite; } protected Point getInitialSize() { return new Point(300, 200); } public boolean close() { value = text.getText(); return super.close(); } public String getValue() { return value; } } Draw2d 中的 Label 可以支持多行文本,当其中的文本中含有换行符的时候 Label 会自动实现视图中的文 本换行,利用这个特性可以非常容易地实现列表框的视图。下面来看一下列表框视图的实现。 【代码 6-52】列表框视图: package com.cownew.uidesigner.components.listbox; import org.eclipse.draw2d.ColorConstants; import org.eclipse.draw2d.Figure; import org.eclipse.draw2d.Graphics; import org.eclipse.draw2d.Label; import org.eclipse.draw2d.SchemeBorder; import org.eclipse.swt.graphics.Color; public class ListBoxFigure extends Figure { private String items; private Label innerList = null; protected static final SchemeBorder.Scheme SCHEME_FRAME = new SchemeBorder.Scheme( new Color[] {ColorConstants.lightGreen, ColorConstants.lightGreen, ColorConstants.lightGreen }, new Color[] {ColorConstants.lightGreen, ColorConstants.lightGreen, ColorConstants.lightGreen }); public ListBoxFigure() { super(); setBorder(new SchemeBorder(SCHEME_FRAME)); innerList = new Label(); innerList.setLabelAlignment(Label.LEFT); innerList.setTextAlignment(Label.TOP); add(innerList); } public void setItems(String items) { this.items = items; this.repaint(); } protected void paintFigure(Graphics graphics) { super.paintFigure(graphics); innerList.setBounds(getBounds()); //Label 可以放置带换行符的多行文字,所以可以自动实现多行效果 innerList.setText(items); } } 最后需要实现的就是组件的控制器。 【代码 6-53】列表框控制器: package com.cownew.uidesigner.components.listbox; import org.eclipse.draw2d.IFigure; import com.cownew.uidesigner.model.Component; import com.cownew.uidesigner.parts.ComponentPart; public class ListBoxPart extends ComponentPart { protected IFigure createFigure() { return new ListBoxFigure(); } protected void doRefreshFigure(IFigure figure, Component component) { ListBoxFigure bf = (ListBoxFigure)figure; ListBox btn = (ListBox)component; bf.setItems(btn.getItems()); } } 6.10 使 用 演 示 至此,一个简单的界面设计工具就完成了,下面来看一看运行效果。 (1) 首先创建一个 Java 项目,并创建包 com.cownew.demo,在包 com.cownew.demo 上右击,在弹出的 快捷菜单中选择【新建】|【其他】命令,接着会弹出如图 6.1 所示的对话框(【选择向导】界面)。 图 6.1 【新建】对话框 (2) 在对话框中选择【UI 编辑器】选项,单击【下一步】按钮,进入如图 6.2 所示的界面。 图 6.2 设置文件名 (3) 在【文件名】文本框中填入“MyApp.ui”,单击【完成】按钮,Eclipse 就会打开新建的界面文件, 如图 6.3 所示。 图 6.3 界面设计器 (4) 编辑器左侧是组件面板,从组件面板中拖放需要的控件到图形编辑区,并且修改相应的属性。设计 好的界面如图 6.4 所示。 图 6.4 设计好的界面 在界面文件的目录下可以看到自动生成的 MyApp.java 文件,代码如下。 【代码 6-54】生成的界面代码: package com.cownew.demo; import Javax.swing.*; import Java.awt.*; public class MyApp extends JFrame { private static final long serialVersionUID = 1L; private JPanel jContentPane = null; public static void main(String[] args) { MyApp frame = new MyApp(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } public MyApp() { super(); initialize(); } private void initialize() { this.setSize(800, 600); this.setContentPane(getJContentPane()); this.setTitle("JFrame"); } private JPanel getJContentPane() { if (jContentPane != null) { return jContentPane; } jContentPane = new JPanel(); jContentPane.setLayout(null); JLabel labelName= new JLabel("姓名:"); labelName.setBounds(new Rectangle(21,27,60,22)); jContentPane.add(labelName); JTextField edtName= new JTextField(); edtName.setBounds(new Rectangle(93,26,91,22)); jContentPane.add(edtName); JCheckBox checkbox1= new JCheckBox("是否会员"); checkbox1.setBounds(new Rectangle(23,59,117,22)); checkbox1.setSelected(true); jContentPane.add(checkbox1); JList listbox1= new JList(new String[]{"VIP 会员","黄金会员", "白银会员","青铜会员"}); listbox1.setBounds(new Rectangle(25,83,141,64)); jContentPane.add(listbox1); JButton button1= new JButton("确定"); button1.setBounds(new Rectangle(191,82,60,22)); jContentPane.add(button1); JButton button2= new JButton("取消"); button2.setBounds(new Rectangle(192,121,60,22)); jContentPane.add(button2); return jContentPane; } } 运行此 Java 文件,效果如图 6.5 所示。 图 6.5 运行效果
还剩118页未读

继续阅读

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

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

需要 20 金币 [ 分享pdf获得金币 ] 42 人已下载

下载pdf

pdf贡献者

jaffa

贡献于2010-12-22

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