Java; Desktop 的再介绍强调了今年的 JavaOne 大会对于那些抱怨 Swing 太慢太难使用界面太难看的开发人员来说Swing 和 GUI 开发所做的更新努力并没有带来什么受人欢迎的好消息如果您最近没有用过 Swing那么您会很高兴听到其中的许多问题已经得到解决Swing 被重新设计它能执行得更好并能更好地利用 Java D APISwing 的开发者在 版甚至最新发布的 版中提高了外观支持Swing 从没像现在这么好过 如果以前曾经用过 JTable那么您可能也同时被迫使用了 TableModel您可能还注意到每个 TableModel 中的所有代码与其他 TableModel 中的代码几乎是一样的在编译的 Java 类中有差异的代码实际上是不存在的本文将分析 TableModel/JTable 目前的设计方法说明这种设计的不足展示为什么它没有实现模型视图控制器(MVC)模式的真正目标您将看到框架和构成 TMF 框架的代码 —— 我以前编写的代码与最常用的开放源代码项目的组合使用该框架开发人员可以把 TableModel 的大小从数百行代码减少到只有区区一行并把重要的表信息放在外部 XML 文件中在读完本文之后只使用如下所示的一行代码您就可以管理您的 JTable 数据 TableUtilitiessetViewToModel(tableconfigxml My Table myJTable CollectionUtilitiesobservableList(myData));
JTable 和 TableModel 存在的 MVC 问题 MVC 已经成为非常流行的 UI 设计模式因为它把业务逻辑清晰地从数据的视图中分离了出来Struts 是 MVC 在 Web 上应用的一个非常好的例子最初Swing 最大的一个卖点是它采用了 MVC将视图从模型中分离了出来代码背后的想法是代码的模块化程度足够高所以不用修改模型中的任何代码就可以分离出视图我想任何用过 JTables 和 TableModels 的人都会发笑告诉您这是绝对不可能的使用 MVC 设计模式的理想情况是在开发人员用 JList 或 JComboBox 替换 JTable 时可以不用修改表示数据的模式中的代码但是在 Swing 中做不到这点Swing 使得把 JTable JList 和 JComboBox 热交换到应用程序中成为不可能即使所有这三个组件都是用来为相同的数据模型提供视图对于 Swing 中的 MVC 设计这是一个严重的不足如果您想为 JTable 交换 JList就必须重写视图背后的全部代码才能实现该交换 JTable/TableModel 的另一个 MVC 缺陷是模型变化的时候视图不会更新自身开发人员必须保持对模型的引用并调用一个函数这样模型才会告诉视图对自身进行更新;但是理想的情况应当是不需要任何额外的代码就能实现自动更新 最后JTable 和 TableModel 组件设计的问题是它们彼此之间缠杂得过于密切如果您修改了 JTable 中的代码那么您需要确保您没有破坏负责处理的 TableModel反之亦然对于一个被认为是在模块化基础上建立的设计模式来说目前的实现显然是一种存在过多依赖关系的设计 TMF 框架更好地遵循了 MVC 的目标它把 JTable 中视图和模型的工作更加清晰地分离开来虽然它还没有达到让组件能够热切换的更高目标但是它已经在正确方向上迈出了一步 让我们来检视 TMF 框架看看它是如何让传统 TableModel 过时的设计该框架的第一部分是学习 JTable 的使用 —— 开发人员如何使用它它显示了什么内容以便了理解哪些东西可以内化通用化哪些应当保留可配置状态以便开发人员配置对于 TableModel也要进行同样的思考我必须确定哪些东西可以从代码中移出哪些必须留在代码中一旦找出这些问题接下来要做的就是确定能够让代码足够通用的最佳技术以便所有人都能使用它但是还要让代码具备足够的可配置性这也是为了让每个人都能使用它 该框架分成三个基本部分一个能够处理任何类型数据的通用 TableModel一个外部 XML 文件(负责对不同表中不同的表内容进行配置)以及模型与视图之间的桥 在本文中您可以在 src 文件夹中找到文中介绍的所有源代码特定于 TMF 的代码位于 comibmjxswingtable 包中 comibmjxswingtableBeanTableModel BeanTableModel 是框架的第一部分它充当的是通用 TableModel 您可以用它来处理任何类型的数据我知道您可能会说您怎么这么肯定它适用于所有的数据呢?确实很明显我不能这么肯定而且实际上我确信有一些它不适用的例子但是从我使用 JTables 的经验来说我愿意打赌(即使看起来我有点抬槓)实际使用中的 JTables% 都是用来显示数据对象列表(也就是说JavaBeans 组件的 ArrayList)基于这个假设我建立了一个通用表模型它可以显示任何数据对象列表它就是 BeanTableModel BeanTableModel 大量使用了 Java 的内省机制来检查 bean 中的字段显示正确的数据它还使用了来自 Jakarta Commons Collections 框架的两个类来辅助设计 在我深入研究代码之前请让我解释来自类的几个概念因为我可以在 bean 上使用内省机制所以我需要了解 bean 本身的信息主要是了解字段的名称是什么我可以通过普通的内省机制来完成这项工作我可以检查 bean 找出其字段但是对于表来说这还不够好因为多数开发人员想让他们的表按照指定顺序显示字段除此之外还有一项表需要的信息我无法通过内省机制从 bean 中获得即列名消息所以为了获得正确显示对于表中的每个列您需要两条信息列名和将要显示的 bean 中的字段我用键值对的格式表示该信息其中将列名用作键字段作为值 正因为如此我在这里使用了来自 Collections 框架的适合这项工作的两个类 BeanMap 用作实用工具类负责处理内省机制它接手了内省机制的所有繁琐工作普通的内省机制开发需要大量的 try / catch 块对于表来说这是没有必要的 BeanMap 把 bean 作为输入像处理 HashMap 那样来处理它在这里键是 bean 中的字段(例如 firstName )值是 get 方法(例如 getFirstName() )的结果BeanTableModel 广泛地运用 BeanMap 消除了操作内省机制的麻烦也使得访问 bean 中的信息更加容易 LinkedMap 是另外一个在 BeanTableModel 中全面应用的类我们还是回到为列名字段映射所进行的键值数据设置对于数据对象来说很明显应当选择 HashMap但是HashPap 没有保留插入的顺序对于表来说这是非常重要的一部分开发人员希望在每次显示表的时候都能以指定的顺序显示列这样插入的顺序就必须保留解决方案是 LinkedMap 它是 LinkedList 与 HashMap 的组合它既保留了列也保留了列的顺序信息参见清单 可以查看我是如何用 LinkedMap 和 BeanMap 来设置表的信息的 清单 用 LinkedMap 和 BeanMap 设置表信息 protected List mapValues = new ArrayList(); protected LinkedMap columnInfo = new LinkedMap(); protected void initializeValues(Collection values) { List listValues = new ArrayList(values); mapValuesclear(); for (Iterator i=erator(); ihasNext();) { mapValuesadd(new BeanMap(inext())); } } 在 BeanTableModel 中比较有趣的检查代码实际上是通用 TableModel 的那一部分这部分代码扩展了 AbstractTableModel 将清单 中的代码与您通常用来建立传统 TableModel 的代码进行比较您可以看到一些类似之处 清单 BeanTableModel 中的通用 TableModel 代码 /** * Returns the number of BeanMaps therefore the number of JavaBeans */ public int getRowCount() { return mapValuessize(); } /** * Returns the number of keyvalue pairings in the column LinkedMap */ public int getColumnCount() { return columnInfosize(); } /** * Gets the key from the LinkedMap at the specified index (and a * good example of why a LinkedMap is needed instead of a HashMap) */ public String getColumnName(int col) { return columnInfoget(col)toString(); } /** * Gets the class of the columnA lot of developers wonder what * this is even used forIt is used by the JTable to use custom * cell renderers some of which are built into JTables already * (Boolean Integer String for example)If youwrite a custom cell * renderer it would get loaded by the JTable for use in displayif that * specified class were returned here * The function uses the BeanMap to get the actual value out of the * JavaBean and determine its classHowever because the BeanMap * autoboxes things it converts the primitives to Objects for you * (eg ints to Integers) the code needs to unautobox it since the * function must return a Class ObjectThus it recognizes any primitives * and converts them to their respective Object class */ public Class getColumnClass(int col) { BeanMap map = (BeanMap)mapValuesget(); Class c = mapgetType(columnInfogetValue(col)toString()); if (c == null) return Objectclass; else if (cisPrimitive()) return nvertPrimitiveToObject(c); else return c; } /** * The BeanTableModel automatically returns false and if you * need to make an editable table youll have to subclass * BeanTableModel and override this function */ public boolean isCellEditable(int row int col) { return false; } /** * The function that returns the value that you see in the JTableIt gets * the BeanMap wrapping the JavaBean based on the row it uses the * column number to get the field from the column information LinkedMap * and then uses the field to retrieve the value out of the BeanMap */ public Object getValueAt(int row int col) { BeanMap map = (BeanMap)mapValuesget(row); return mapget(columnInfogetValue(col)); } /** * The opposite function of the getValueAt it duplicates the work of the * getValueAt but instead puts the Object value into the BeanMap instead * of retrieving its value */ public void setValueAt(Object value int row int col) { BeanMap map = (BeanMap)mapValuesget(row); mapput(columnInfogetValue(col) value); superfireTableRowsUpdated(row row); } /** * The BeanTableModel implements the CollectionListener interface * ( of the parts of the framework) and thus listens for changes in the * data it is modeling and automatically updates the JTable and the * model when a change occurs to the data */ public void collectionChanged(CollectionEvent e) { initializeValues((Collection)egetSource()); superfireTableDataChanged(); } 正如您所看到的BeanTableModel 的整个 TableModel 足够通用化可以在任何表中使用它充分利用了内省机制省去了所有特定于 bean 的编码工作在传统的 TableModel 中这类编码工作绝对是必需的 —— 同时也是完全冗余的BeanTableModel 还可以在 TMF 框架之外使用虽然在外面使用会丧失一些威力和灵活性 看过这段代码之后您会提出两个问题首先BeanTableModel 从哪里获得列名字段与键值配对的信息?第二到底什么是 ObservableCollection ?这些问题会将我们引入框架的接下来的两个部分这些问题的答案以及更多的内容将在本文后面接下来的章节中出现 Castor XML 解析器 保存必需的列名字段信息的最合理的位置位于 Java 类之外这样不需要再重新编译 Java 代码就可以修改这个信息因为关于列名和字段的信息是 TMF 框架中惟一明确与表有关的信息这意味着整个表格都可以在外部进行配置 显然该解决方案会自然而然把 XML 作为配置文件的语言选择配置文件必须为多种表模型保存信息;您还需要能够用这个文件指定每个列中的数据配置文件还应当尽可能地易于阅读因为开发人员之外的人员有可能要修改它 这些问题的最佳解决方案是 Castor XML 解析器查看 Castor 实际使用的最佳方法就是查看如何在框架中使用它 让我们来考虑一下配置文件的目的保存表模型和表中列的信息 XML 文件应当尽可能简单地显示这些信息TMF 框架中的 XML 文件用清单 所示的格式来保存表模型信息 清单 TMF 配置文件示例 <model> <className>demohrTableModelFreeExample</className> <name>Hire</name> <column> <name>First Name</name> <field>firstName</field> </column> <column> <name>Last Name</name> <field>lastName</field> </column> </model>
与这个目的相反的目标是开发人员必须处理的 Java 对象应当像 XML 文件一样容易理解通过 Castor XML 解析器用来存储 target=_blank>存储列信息的三个 Java 对象就可以看到这一点这三个对象是 TableData (存储文件中的所有表模型) TableModelData (存储特定于表模型的信息)和 TableModelColumnData (存储列信息)这三个类提供了 Java 开发人员所需的所有包装器以便得到有关 TableModel 的所有必要信息 将所有这些包装在一起所缺少的一个环节就是 映射文件它是一个 XML 文件Castor 用它把简单的 XML 映射到简单的 Java 对象中在完美的世界中映射文件也应当很简单但事实要比这复杂得多良好的映射文件要使别的一切东西都保持简单;所以一般来说映射文件越复杂配置文件和 Java 对象就越容易处理映射文件所做的工作顾名思义就是把 XML 对象映射到 Java 对象清单 显示了 TMF 框架使用的映射文件 清单 TMF 框架使用的 Castor 映射文件
Code highlighting produced by Actipro CodeHighlighter (freeware)
> <?xml version=?>
<mapping> <description>A mapping file for externalized table models</description> <class name=comibmjxswingtableTableData> <mapto xml=data/> <field name=tableModelData collection=arraylist type= comibmjxswingtableTableModelData> <bindxml name=tableModelData/> </field> </class> <class name=comibmjxswingtableTableModelData> <mapto xml=model/> <field name=className type=string> <bindxml name=className/> </field> <field name=name type=string> <bindxml name=name/> </field> <field name=columns collection=arraylist type= comibmjxswingtableTableModelColumnData> <bindxml name=columns/> </field> </class> <class name=comibmjxswingtableTableModelColumnData> <mapto xml=column/> <field name=name type=string> <bindxml name=name/> </field> <field name=field type=string> <bindxml name=field/> </field> </class> </mapping>
仅仅通过观察这段代码您就可以看出映射文件清晰地勾划出了每个用来存储表模型信息的类定义了类的类型并将 XML 文件中的名称连接到了 Java 对象中的字段请保持相同的名称这样会让事情简单更好管理一些但是没必要保持名称相同 到现在为止列名和字段信息都已外部化可以读入包含列信息的 Java 对象中并且可以很容易地把信息发送给 BeanTableModel并用它来设置列 ObservableCollection TMF 框架的最后一个关键部分就是 ObservableCollection 您们当中的某些人可能熟悉 ObservableCollection 的概念它是 Java Collections 框架的一个成员在被修改的时候它会抛出事件从而允许其侦听器根据这些事件执行操作虽然从来没有将它引入 Java 语言的正式发行版中但在 Internet 上这个概念已经有了一些第三方实现就本文而言我使用了自己的 ObservableCollection 实现因为框架只需要一些最基本的功能我的实现使用了一个称为 collectionChanged() 的方法每次发生修改时 ObservableCollection 都会在自己的侦听器上调用该方法也可以将该用法称为 Collection 类的 Decorator(有关 Collections 的 Decorator 更多信息请参阅 Collections 框架的站点)只需要增加几行代码您就可以在普通的 Collection 类中创建 Collection 类的 Observable 实例 清单 显示了 ObservableCollection 用法的示例(这只是一个示例没有包含在 jxzip 中) 清单 ObservableCollection 用法示例
Code highlighting produced by Actipro CodeHighlighter (freeware)
> // convert a normal list to an ObservableList
ObservableList oList = CollectionUtilitiesobservableList(list); // A listener could then register for events from this list by calling oListaddCollectionListener(this); // trigger event oListadd(new Integer()); // listener receives event public void collectionChanged(CollectionEvent e) { // event received here }
ObservableCollection 有许多 TMF 框架之外的应用程序如果您决定采用 TMF 框架您会发现在开发代码期间 ObservableCollection 框架有许多实际的用途 但是它在 TMF 框架中的用途重点在于它能更好地定义视图和模型之间的关系当数据发生变化时可以自动更新视图您可以回想一下这正是传统 TableModel 的最大限制因为每当数据发生变化时都必须用表模型的引用来更新视图而在 TMF 框架中使用 ObservableCollection 时当数据发生变化时视图会自动更新不需要维护一个到模型的引用在 BeanTableModel 的 collectionChanged() 方法的实现中您可以看到这一点 TableUtilities 在该框架中执行的最后一步操作是将所有内容集成到一些实用方法中让 TMF 框架使用起来简单明了这些实用方法可以在 comibmjxswingtableTableUtilities 类中找到该类提供了您将需要的所有辅助函数 getColumnInfo() 该实用方法用 Castor XML 文件解析指定的文件并返回指定表模型的所有列信息返回的形式是 BeanTableModel 所需的 LinkedMap 当开发人员选择从 BeanTableModel 中派生子类时这个方法很重要 getTableModel() 该实用方法是建立在上面的 getColumnInfo() 方法之上它获得列的信息然后把信息传递给 BeanTableModel返回已经设置好所有信息的 BeanTableModel setViewToModel() 该实用方法是最重要的函数也是 TMF 框架的主要吸引人的地方它也是建立在 getTableModel() 方法之上也有一个到 JTable 的引用(JTable 中有这个表的模型)以及一个到数据(要在表中显示)的引用它对 JTable 上的 TableModel 进行设置并把数据传递给 TableModel结果是只需一行代码就为 JTable 完成了 TableModel 的设置TMF 框架在该方法上得到了最佳印证TableModel 将永远地被下面这个简单的方法所代替
Code highlighting produced by Actipro CodeHighlighter (freeware)
> TableUtilitiessetViewToModel(table_configxml Table myJTable myList);
每篇关于 GUI 编程的文章都需要一个示例本文当然也不例外该示例的目的是指出使用 TMF 框架代替传统 TableModel 设计的主要优势所在示例中的应用程序将在屏幕上显示多个表并且可以添加或删除表表中可以包含不同类型的信息( String 类型 int 类型 Boolean 类型和 BigDecimal 类型)而且最重要的是其中还包含可配置的列信息必须定期更改它们 示例应用程序的代码从 JX 包中分离了出来您可以 HR 文件夹的 src 目录中找到源代码还可以双击 build/lib 文件中编译好的 JAR 文件通过 JRE 运行应用程序 在示例应用程序中有两个类可以相互交换一个叫作 TableModelFreeExample 另一个叫作 TableModelExample 这两个类在应用程序中做的是同样的事使应用程序产生的行为也相同但是它们的设计不同一个使用的是 TMF 框架另外一个则使用传统的 TableModel您从它们身上注意到的第一件事可能是 TMF 类 TableModelFreeExample 该类由 行代码构成而在传统 TableModel 版本 TableModelExample 中它长达 行 Evil HR Director 应用程序 我要使用的示例应用程序是 Evil HR Director 应用程序它允许人力资源总监(可能很可怕戴着眼镜)在 JTable 中查看潜在雇员的列表然后从表中选出雇佣的人新雇佣的员工的资料会转移到当前雇员使用的两个 JTable 中;其中一个表包含个人信息另外一个表包含财务信息在当前雇员表中总监可以随意选择解雇谁您可以在图 中看到该应用程序的 UI 图 Evil HR Director 应用程序 src=http://imgeducitycn/img_///jpg border= twffan=done> 为了进一步证明 TMF 框架的简单性请看清单 这个清单只包含三行必需的代码就可以创建 Evil HR Director 应用程序中包含的三个表的模型这些代码可以在 TableModelFreeExample 中找到 清单 在 Evil HR Director 应用程序中创建模型所需要的代码
Code highlighting produced by Actipro CodeHighlighter (freeware)
> TableUtilitiessetViewToModel(demo/hr/resources/evil_hr_tablexml
Hire hireTable candidates); TableUtilitiessetViewToModel(demo/hr/resources/evil_hr_tablexml Personal personalTable employees); TableUtilitiessetViewToModel(demo/hr/resources/evil_hr_tablexml Financial financialTable employees);
为了进行比较 TableModelExample 中包含用传统 TableModel 方法为三个表格创建模型所需要的代码请查看示例包中的代码不过我不想在这里列出所有代码因为它足足有 行! 演示 TMF 框架的灵活性 TMF 框架的巨大优势之一是它能更加容易地基于 JTable 的应用程序在其发布之后进行修改为了证实这一点让我们来看两个可能的场景这两个场景在使用 Evil HR Director 应用程序中每天都可能出现在每个场景中您都会看到框架是如何让应用程序更加容易地适应不断变化的用户需求 场景 公司的策略发生变化规定在公司的应用程序中查看私人的婚姻信息是非法的 TMF最终用户需要从 XML 配置文件中删除 Married?married 传统 TableModel开发人员必须深入研究 Java 代码修改 getColumnName() 让它无法返回列名Married?;修改 getColumnCount() 让它返回的结果比以前返回的结果少一列;修改 getValueAt() 不让它返回 isMarried() 然后开发人员必须重新编译 Java 代码并重新部署应用程序 场景 公司策略发生变化公司觉得有必要在潜在雇员表中包含居住地所在的州的信息 TMF: 最终用户需要将 Statestate 添加到 XML 配置文件中 传统 TableModel开发人员必须深入研究 Java 代码修改 getColumnName() 添加一个叫作 State 新列;修改 getColumnCount() 让它返回的列数加 ;修改 getValueAt() 让它返回 getState() 然后开发人员必须重新编译 Java 代码并重新部署应用程序 您可以看到当应用程序中的表发生变化时(尤其在碰到一个总是朝令夕改的老板时更改更加频繁)编辑 XML 文件要比重新部署整个应用程序容易得多 使用代码 在您飞奔过去删除所有 TableModel 代码之前我想我还得占用您一分钟解释一下 jxzip 文件的内容以及您怎样才能在您自己的项目中使用它(请记住特定于 TMF 的代码可以在 comibmjxswingtable 包中找到;您还会在 JX 包中找到我在以前的文章Go stateoftheart with IFrame中介绍的其他代码) jxzip 文件包含两上文件夹 src—— 包含本文中使用的源代码在 src 文件夹中还有两个文件夹一个是 HR包含构成 Evil HR Director 应用程序的源代码;另一个是 JX包含 JX 项目中使用的所有源代码 build—— 包含 Evil HR Director 应用程序和 JX 项目编译后的类文件该文件夹中的 lib 文件夹则包含 HR 应用程序和 JX 项目的 JAR 文件 libzip 文件包含以下文件夹 lib—— 包含所有的第三方 JAR 文件运行应用程序或者任何使用 JX 项目的项目需要使用这些文件在这个文件夹中您还会找到第三方项目的许可 docszip 文件包含下列文件夹 docs—— 包含 JX 项目的所有 JavaDoc 信息 要在应用程序中使用 JX 包则需要把 CLASSPATH 指向 build/lib 文件夹中的 jxjar 以及 lib 文件中包含的所有三个第三方 JAR 文件第三方包的许可条款允许您重新发布本文包含的所有包但是如果有兴趣对这些包做些修改请阅读许可条款 结束语 使用 TableModel Free 框架就不用再编写传统 TableModel 了TMF 框架改进了 JTable 和 TableModel 模型之间的 MVC 关系更清楚地分离了它们在日后的发布中您甚至可以在不修改任何模型代码的情况下对组件进行热交换框架还允许您在模型发生变化时自动更新视图从而消除传统 TableModel 设计中所必需的视图和模型之间的通信 TMF 框架还会极大地减少开发 GUI 所需的时间特别是在处理 JTable 时几年以前我处理的一个应用程序中有 多个 JTable每个表都来自同一个原始表模型该应用程序可以作为示例使用 TMF 框架我们只用 行代码就能解决问题;但是不幸的是当时还没有 TMF所以我们最后编写了 行额外的代码才生成必需的表模型这不但增加了开发时间还增加了测试和调试的时间 与使用传统 TableModel 相比使用 TMF 框架使您到了一个更加容易配置所有 JTable 的时代请想像这样一个 POS 应用程序该应用程序被销售给了 个不同的客户每个客户都有一套特定的信息所以每个用户都想有一组显示在 GUI 上的特定的列如果没有 TMF 框架您就必须为每个客户都生成一组特定的 TableModel —— 由此也就生成了一组特定的应用程序而使用可配置的 XML 文件每个客户都可以使用相同的应用程序客户所在地的业务分析师可以根据需要修改 XML 文件请想像一下这节约了多少开发和支持成本! TableModel Free 框架解决了 Swing 开发人员社区的特定需求减少了处理 JTable 时的开发时间和维护开销提高了它们对终端用户的易用性Swing 桌面正在回归使用像 TMF 框架这样的工具开发人员会发现可以更容易地使用 Swing 和开发 GUI 应用程序您要做的第一步就是用 TMF 框架的一行代码代替您所有的 TableModel把所有 TableModel 都永远地抛到虚拟空间的黑洞中去吧 代码下载jxzip libzip docszip |