转自:http://blogs.sun.com/swing/
对于未知技术的原理,我总喜欢自己想像它是如何实现的。我上学时互联网搜索技术还没有怎么流行。大家学习新技术的方法,总是第一去查教科书,第二去找论 文,第三去问师兄。而我想知道的技术往往都是教科书上找不到,论文中很少提及的,师兄们很少去想的事情。于是很多时候就自己想像它们是如何实现的。
图形界面设计技术对我来说就是这样。我很早以前就对这种技术很感兴趣,但是能够查找到的资源很少。我常常自己冥想图形设计界面是如何画出来的;描述界面的 元数据和代码是如何解析和映射的;界面组件属性是如何设置和查看的。后来随着对于Swing原理和技术的深入了解,使得我对实现界面设计工具越来越胸有成 主,终于在2002年某个时候,大胆动手,一气呵成。后来又分别在2003年和2005年分别做过一个,对于其中具体实现技术又有了更深刻的理解。
言归正传,前文提到的三个问题也是Swing界面设计工具首先要解决三个问题。
Swing图形设计工具界面的绘制一般使用Swing的Renderer思想实现。Renderer思想在前面许多文章中都有提及,其基本原理是将渲染组 件的图形对象传递给其他组件诸如paint、paintAll、paintChildren、paintComponent等渲染方法,让该组件代理完成 具体界面的绘制。这种技术经常在JTable、JList、JTree和JComboBox等复杂Swing组件中使用。Renderer思想是代码复用 和动态绑定思想在Swing架构中的具体体现。它包含的一个深层含义是,对于具体组件外观的绘制,负责渲染的宿主组件不需要知道被绘制组件的绘制是如何进 行的,只要将自己的图形设备对象传递给被绘制组件的渲染方法就可以了。当然现实中的图形界面设计工具还需要渲染其他辅助图形,如当前选定组件的边框等。而 这些都不会是什么问题了。
因此Swing界面设计工具一般首先要在内存中构建被设计界面的组件树,当要绘制设计工具的界面时,只需将其Graphics对象传递给组件树顶层容器的paint方法就可以了。
如何构建被设计界面组件树呢?这个界面组件树的数据结构由描述界面的元数据,甚至是界面源代码码解析生成。界面元数据或源代码的解析生成是界面设计工具中 最关键的技术之一。目前主要有三种解决方法。第一种是Delphi、VB为代表的资源文件方式,第二种是NetBeans Mattise为代表的xml界面描述文件方式,第三种是Eclipse VE为代表的代码解析和生成技术。Java界面设计工具一般采用后两种方式。
界面描述文件方法相对比较简单。其核心思想是将界面组件树采用xml文件(或其它格式)的方式保存起来,以后或者预先生成静态界面源代码代码,或者在运行 时根据它动态生成界面。由于xml的解析要比java代码的解析简单得多,所以采用这种方式实现比较简单,而且界面设计动作容易控制。缺点是必须维持描述 文件和源代码文件的同步,另外这种xml配置文件语法结构往往是非公开的,使得它设计的界面文件不能被其他界面设计工具所复用。另外程序员不能随意修改生 成代码,描述文件和源代码之间生成关系是单方向的。
第三种直接将Java界面源代码解析成组件树。这种方法也有两种做法,一种是对代码进行语法语义分析,并构建出要设计的界面组件树来;一种是使用Java 编译器将源文件编译成最终class,使用类加载器动态加载并反射生成最终组件树。前者缺点是非常复杂,需要丰富编译知识;优点是不需要将源代码编译通 过。后者优点是解析非常简单,全部交给了编译器完成;缺点是如果源码存在编译错误,就无法最终生成设计界面。
第三个基本技术,即组件属性的反射一般使用Java Beans技术实现。Java平台提供了解决界面设计工具组件反射的工具,如BeanIntrospector、BeanDescriptor等类,它们 建立在Java反射技术的基础上,提供解析和设置JavaBean(主要是组件对象)属性的机制。用来查看和设置组件属性的控件通常称作属性页,在 Swing可以通过表格JTable结合Renderer/Editor思想实现。
今天的文章先就Swing界面设计工具的基本思想和技术做一个简单的概述,后面将就这三个方面的技术在图形界面设计工具的应用进行详细地描述。
解决复杂问题常用的一个方法是分析综合法,或曰分而治之,即将复杂的问题分解成简单的子问题。解决了每个子问题后,再将它们集成起来。若每个子 问题还是太复杂,则可重复此过程直至简单到容易解决为止。如果问题本身比较复杂,但又不容易继续分下去,则可试着将问题模型进行简化,在此基础上开发出原 型后,再陆续添加其其他功能,逐步丰富直至接近问题的原始规模。
Swing界面设计工具是比较复杂的软件系统。在开发之前,我们应该先将它进行分析简化。根据以前的经验,这类工具的功能模块大体上分为三个部分:
1.界面设计组件,以所见即所得的可视化方式显示当前正在设计的界面,功能包括组件的选取和编辑两大部分,其中选取根据类型可分为单选、多选、区域选择、 组件拖放、大小调整等等,组件的编辑包括组件的添加、删除、复制、剪切、粘帖、对齐、布局管理等等。除这些功能外,界面设计组件还应向属性编辑页、组件选 择面板、组件选择工具栏等系统其他部分提供事件和动作接口。
2.界面设计辅助工具,如属性、事件、布局的查看和编辑工具属性页,设计组件树的浏览、导航和选择,组件选择工具栏,以及界面设计工具的编辑操作所对应的工具栏。
3.元数据及代码解析和生成工具。此部分虽不可见,但却是界面设计工具的难点和核心。它往往是评价一个界面设计工具优劣的重要标准。
本人打算采取迭代模式开发这个系统,先依序开发各个子模块,然后最后将他们组合起来。每个阶段都会涉及设计->编码->重构三个元素,程序在不断演化中前进。
首先考虑界面设计组件的开发。如前文所述,其功能可分为组件选择和编辑两方面。组件选择模块的特点是在运行时可以共享一套状态数据,比如当前选择的组件,当前拖拽的组件,当前鼠标的位置,当前选择矩形的边界等等。
实现的思想和技术其实前文都已提到。首先要实现其可视化设计界面的组件。首先可假设当前正在设计的界面组件树已经在内存构建出来了,其根就是一个 Component,在渲染时只要将界面设计的paint方法中,调用根组件的渲染方法paint就能渲染出来。其方法大致如下(在其UI类中):
public void paint(Graphics g, JComponent c) { if (component != null) { //如果当前被设计的组件不为空,则画出该组件来 paintDesignedComponent(g); } if ((selected_components != null) &&!selected_components.isEmpty()) { //如果当前选中的组件不为空,则画出他们的resizing边框来 paintResizing(g); } if (selection_bounds != null) { //如果当前正在选择组件,画出选择框来 g.setColor(SELECTION_COLOR); g.drawRect(selection_bounds.x + LEFT_PAD, selection_bounds.y + TOP_PAD, selection_bounds.width, selection_bounds.height); } } private void paintResizing(Graphics g, JComponent c) { //当前这些选中的组件是否是可以resize的 //带有布局管理器的容器组件内的组件不应该能resizeable //另外如果所选择的组件跨了几个容器,它们也不应该能resizeable boolean resizable = isSelectedResizable(); for (Component comp : selected_components) { Rectangle bounds = getRelativeBounds(comp); g.setColor(SELECTION_COLOR); int x = bounds.x + LEFT_PAD; int y = bounds.y + TOP_PAD; int w = bounds.width; int h = bounds.height; //画出被选择组件的高亮边框 g.drawRect(x, y, w, h); if (resizable) { //如果是resizable,要画出其拖拽点矩形 drawResizingThumbs(g, x, y, w, h); } } } private void paintDesignedComponent(Graphics g, JComponent c) { //这儿和以前讲过的一样,再使用Renderer技术时,要保证渲染组件是非缓冲的 ArrayList dbcomponents = new ArrayList(); disableBuffer(component, dbcomponents); int width = component.getWidth(); int height = component.getHeight(); //创建相对根组件的剪切窗口图形对象 Graphics clipg = g.create(LEFT_PAD, TOP_PAD, width, height); //使用跟组件渲染 component.paint(clipg); clipg.dispose(); int x = LEFT_PAD - BORDER_THICK; int y = TOP_PAD - BORDER_THICK; int w = width + 2 * BORDER_THICK; int h = height + 2 * BORDER_THICK; //画出外边框来 areaBorder.paintBorder(component, g, x, y, w, h); resetBuffer(dbcomponents); } |
时间不多了,今天先写到这儿吧。明天再详细讲述如何给界面设计工具加上选择、键盘、编辑等功能。这儿有初步的代码下载。已经能进行组件选择、拖放、添加、删除、复制、粘贴等简单的编辑动作了。下面是个截图:
更新:
这两天做了进一步重构,添加了一些新的功能和注释。现在的设计界面工具可以进行布局管理,添加了添加提示、添加、删除、复制、剪切、粘帖、对齐等功能,把组件选择工具条做了修改,可以添加容器类等等。下面是几个截图:
我经常觉得写个复杂软件容易,描述清其设计结构却往往很困难。这个项目就是这样,有一种无从下手的感觉。目前这个工具在规模上已经超过三分之一 了,为了照顾其灵活性、可扩展性、易用性等指标,它已经过了三四次重构,目前已膨胀到十三个包、八十多个源文件。如何描述清其结构和设计理念不是件简单的 事。
这个项目的名字叫swing_designer,其功能还只局限于基本的界面设计包括组件编辑和布局管理。对于组件树的浏览、组件属性编辑、组件事件编辑以及代码解析和生成还做任何涉及。说的简单些,目前其实是实现了一个的大组件,能进行简单界面设计的复杂组件。
同其它标准Swing组件一样,它是遵循MVC模型设计的。如果你读过前面关于swing体系结构的文章,就会对这一结构比较了解。考虑到这是个并非通用组件,因此虽然设计成了包含UI类的Swing架构,但对于UI类没有进行过多设计。
项目所包含的类包如下:
其中dyno.swing.designer.beans、dyno.swing.designer.beans.events和 dyno.swing.designer.models是理解这个工具最关键的三个包,它们分别实现了该组件V(视图)、C(行为)和M(模型)逻辑。 dyno.swing.designer.beans定义了基本组件SwingDesigner及一些主要扩展接口。 dyno.swing.designer.beans. events定义了组件的鼠标和键盘事件处理器,及由SwingDesigner发出的事件和事件处理器接口。 dyno.swing.designer.beans.models定义了用来保存临时设计状态的数据,如当前选中的组件,当前鼠标拖拽区域,当前被添加 的组件及BeanInfo,当前鼠标位置等等。
注意界面设计的元数据保存在SwingDesigner中,其根节点是SwingDesigner的成员变量rootComponent。关于如何存储设 计界面的数据,我考虑过很多方案。以前用的方法是为设计组件和设计数据各建立一个树。这种方法虽然灵活,但两个树之间的状态同步却是一个非常麻烦的事情。 现在这个项目中采用一个树,即设计界面的组件树。其变量名保存于Component的name属性中,其他描述数据保存于自定义client property中。这样避免了数据同步的问题,又可灵活地存储复杂的描述数据。
为降低鼠标和键盘事件处理的复杂度,界面设计工具在运行时分为两种状态:普通编辑状态以及组件添加状态。在普通编辑状态中,用户可以选择、拖拽正在设计的 组件,还可以剪切、删除、复制、粘帖组件,另外还可以进行各种对齐操作。在普通编辑状态下,还可激活上下文菜单,从菜单中选择针对当前选中组件的操作。目 前只有实现了一个上下文菜单,就是切换容器的布局管理器。以后会随着其他功能的加入而逐渐加入其他菜单。
组件添加状态是种独占状态。在此状态中,鼠标能进行的操作只有放置添加组件,任何编辑状态下的操作都不能进行。在组件面板上选择组件能够进入添加状态。如 需要返回普通编辑状态,需要放置被添加组件,或显式地从工具栏里选择编辑模式。两种状态进行切换时会触发DesignerState事件。
另外界面设计工具一般都设计成可扩展的插件式平台。由于界面设计使用的组件可能千差万变,用户还可能自定义组件和布局器,因此界面设计工具一般设计成以组件和布局器为基本扩展元素的平台。
为实现这种可扩展性,界面设计工具往往扩展java beans工具实现这一目的。缺省java平台提供的java beans元数据描述对象BeanInfo往往有限,不足以描述或者提供界面设计时需要数据,并且对布局管理器就根本没有提供描述方法,因此界面设计工具一般都会对java beans工具进行扩展,实现独立JavaBeans元数据描述机制。管理这种元数据描述对象的模块类似于插件管理平台,设计工具所需的组件元数据信息都从这儿获取。
swing_designer采用类似方法,为组件和布局管理器分别提供了描述接口:ComponentAdapter和LayoutAdapter。它 们相当于插件接口,实质上是对BeanInfo进行的扩展,目的是为界面设计工具提供特殊的行为和信息支持。其管理工具类是AdapterBus,这是个 简单的插件管理平台,采用配置文件注册的方式,将各种组件和布局管理器对应的Adapter实现注册进来,供界面设计工具使用。
综上所述,此界面设计工具的总体结构如下:
其中热键的处理采用代理模式:在键盘处理器代理上注册各种热键及其热键处理器,由代理负责键盘事件的捕捉和分发。绿色部分是界面设计工具的扩展点,新的自定义组件或布局管理器如需要特殊的设计行为,可实现Component Adapter及LayoutAdapter,并在配置文件中进行注册。项目已经在dyno.swing. designer.beans.adapters实现了常见组件及布局管理器的Adapter。
除上面所提的核心包,其他包dyno.swing.designer.beans.context实现上下文菜单;dyno.swing.designer.beans.keys是实现各种热键处理器;dyno.swing. designer.beans.location是鼠标在拖拽时的位置枚举实现,用来控制拖拽行为;dyno.swing.designer.beans.painters实现组件添加时的位置提示;dyno.swing. designer.beans.toolkit实现组件选择面板;dyno.swing.beans和dyno.swing. beans.resource是把以前的FolderPane拿过来作为组件面板使用;dyno.swing. beans.layouts是自定义的一个AbsoluteLayout,目的是实验工具对于自定义布局管理器的扩展功能。
我对于这个工具的系统结构设计并不是十分满意,以后会随着项目的前进,可能对这部分做一些重构,但大的结构调整不会再有了。
最新代码下载。
增加了从组件面板拖拽组件到界面设计工具上的功能。以前的做法是要先选中组件,然后才能进行设计。还有一个改进是,在布局管理器中支持拖拽定 位。举例来说,假设在BorderLayout的容器中已经添加了组件A和B到North和Center,后来发现应该把B放到South,而A应该放在 Center,就可以直接通过拖拽A和B到新的位置,而不需要删除重新添加。下面是几个截图:
下一步工作是实现组件树的浏览和属性编辑。最终结果类似于下图所示的NetBeans中Inspector和Properties的窗口,功能包 括在组件浏览树进行组件选择和移动,和属性编辑表的编辑,事件方法的添加和删除等功能。这部分是界面设计工具的第二个难点和重点。
周末将swing_designer项目开发向前推进了一大步,主要是添加了组件导航树和属性编辑表,使其具备了界面设计工具的基本功能。这些 功能需要BeanInfo机制的支持,每种类型组件基本上都需要相应的beaninfo类,属性编辑表中TableCellEditor及 TableCellRenderer的实现也需要大量的类,因此整个项目规模急剧膨胀,现在已经扩张到了22个包190个类。
本周剩下的时间不打算再添加新的功能,只对现有项目进行一次清理、重构,添加必要的注释和文档,并将这部分实现的基本思想和技术写出来。
下面是几个截图。
设计工具运行窗口
双击鼠标编辑组件文字
双击鼠标编辑TabbedPane页签
组件导航树,支持选择并拖拽组件
组件属性编辑器,以后陆续会添加布局限定等属性
最新源代码下载。
最近两周工作一直很忙,开发swing_designer空闲时间几乎没有,只在两个周末继续完善了一下属性编辑功能,并对以前功能进行重构。到目前位置主要的改进有:
1.将属性进行分类显示,分为常用属性、其他属性、布局限定属性以及布局管理器属性几个部分。
2.扩充常用组件他常见编辑属性。由于每个组件需要的属性编辑器很多,因此相关的PropertyEditor、TableCellEditor及 TableCellRenderer等类继续膨胀。BeanInfo类继续丰富,BeanInfo配置文件由单个配置文件变成多个可以任意添加的配置文 件。目前为止项目的规模是281个类,139资源和配置文件。
3.重构了AdapterBus扩展点结构。添加ContainerAdapter和BorderAdapter来简化容器类操作和边框类型的扩展。Adapter类由全局单个实例变成每个组件对应一个实例,支持有有状态类型的操作。
总之,这一阶段的工作还是上一阶段的继续扩展和完善。下一步工作是继续添加扩充常用组件属性,添加编辑管理事件处理器。
几个截图:
常用属性和其他属性分类
布局位置限定属性,根据容器组件的类型、布局管理器类型不同而不同
布局管理器的属性编辑,属性值发生变化的用粗体标出
复杂属性类型编辑的支持,定义Border边框
最新源代码。
swing_designer:添加高级容器组件的支持
最近一直比较忙,不仅仅是工作上,私人事情也很多,所以swing_designer一直处于缓慢进展中,一直也没有抽出时间来写文章,只把现有的成果继续完善。其中最重要的几个进展是:
1.完成所有常见组件的基本属性编辑功能。这些属性中包括组件一些复杂属性如model的编辑。
2.添加复杂容器组件JLayeredPane、JDesktopPane、JInternalFrame。到目前为止,Matisse的组件面板上的swing container和swing components已经基本涵盖。
3.一些小的改动,比如对于JTree、JTable、JTextArea、JTextPane、JEditorPane等组件的添加自动添加JScrollPane等。
目前项目的规模达到了346个类,142资源文件。有些难以控制了,需要时间重新梳理梳理一下。下一阶段的计划除了树立项目,主要开发集中在事件编辑这一块。
现在越来越觉得要控制项目的规模。本来该项目是要做成教程性质的,现在规模没有控制好,做成了具有一定实用性质的工具了,反而把文章给丢了。而现在精力有限,如果想给大家讲讲这些思想的话,就必须限制一下规模。因此打算今后做如下功能限定:
1.不打算支持菜单编辑。该部分比较独立,可以单独做一个项目来完成。
2.不打算支持除JPanel外的其他窗口组件类型。其他顶层窗口容器的思想和JPanel相似,但代码要做相当的改动。
3.不支持AWT组件。
4.不支持非可见组件,比如ButtonGroup等,需要手工编码补齐。
下面是近期所作修改的一些截图:
Overall View
Desktop Pane with 2 Internal Frames
3 Newly Added Components
More Properties Added
JTable & JTree Added with JScrollPane
源代码
swing_designer:添加事件代码编辑
这两天swing_designer的进度还算可以,到昨晚为止,已把事件代码编辑功能添加上去了。事件编辑功能是作为属性编辑页的模式添加上去的。由于 不是某个JavaIDE的插件,所以代码编辑器只是个简单的文本编辑框,不具有任何代码辅助功能,也不打算加这些功能,因为这些功能偏离当初设计这个项目 的初衷。
下一步计划是添加界面生成与代码解析功能。这部分是个重头戏。说实在的,以前写的界面设计工具这部分都没有解决好,至今我也没有找出能比较完善的解决这个 问题的方法。现在的初步思路是不使用界面描述文件,而使用约定俗称的代码格式来辅助界面解析和代码生成,即在代码中使用约定俗成的方法和某些特定注释来辅 助界面解析。目前思路还不是太清晰,需要进一步探索。
下面是事件代码编辑属性表的截图:
swing_designer进度:界面代码解析
最近两天swing_designer的进度是:
1.重新布局的程序界面,加入了source code编辑器。
2.重构了菜单、工具栏的代码使用Action接口实现。
3.添加一些应用程序菜单和上下文菜单。
4.界面代码解析生成设计界面。
下面是截图:
整个项目就剩下最后一步了,即由设计界面生成代码。
代码下载。
swing_designer项目终于结束了
将最后的一项功能即由设计界面生成代码加上了。采用的特殊注释加编码规范的方法解析和生成代码,不依赖外部描述文件。你可以修改java代码,只要不打破 一些代码规则,就能解析成设计界面。关于实现细节我以后还会撰文描述。总的来说是一个介于netbeans matisse和eclipse ve策略之间的方法,生成的代码风格上很像Visual Age for Java的界面设计工具生成的代码。
这个项目一开始,我就发现要消耗太多的时间和精力,毕竟界面设计工具不是一个小项目。再加上进一阶段个人事情太多,以至于没有以前写文章的生产率了。
swing_designer的最新代码下载。