简单的强大 TableModel(带反射)





3.00/5 (3投票s)
TableModel 复杂性不再是问题。
源代码更新日期: 2009年5月28日
本文的葡萄牙语版本,请访问 此链接。
章节目录
1. 动机
“不要使用 DefaultTableModel”,我经常遇到一些人在使用 DefaultTableModel
实现时遇到问题,我的建议是,不要使用它。但是实现一个能让所有工作都变得简单的类,也并非易事。所以我实现了一个,现在在这里与大家分享。
我的目标是编写一个单一的 TableModel
,简单、可扩展、易读且功能强大。这一切都通过反射和注解来实现。
有了这个模型,您可以
- 添加并获取每一行的当前对象
- 无需处理
String
数组 - 在单元格更新时保持对象同步更新
- 通过注解进行配置,简化代码的可读性
List
接口的方法,如:add
、addAll
、remove
和indexOf
- 如果您不喜欢注解,仍然可以使用它(参见第 2.2.1 章)
2. 开始编码
2.1 基础
2.1.1 简介
首先:下载包含项目源代码的 objecttablemodel.zip 压缩包。
这个项目中值得关注的类有
ObjectTableModel
,这是实现的表格模型FieldResolver
,后台工作在这里进行,访问表格列的字段@Resolvable
,标记字段的注解,用于表格的默认值。如格式化器(如果需要)、Column
名称以及实际访问字段的FieldAccessHandler
。- 实现的
FieldAccessHandlers
有FieldHandler
(默认),它直接使用类中的Field
;以及MethodHandler
,它使用类中的get
(或 is)/set
方法。 AnnotationResolver
类只包含创建FieldResolvers
的通用方法。
这是创建类 JTable
所需的唯一代码。
首先:类,这里以 Person
为例。
import mark.utils.el.annotation.Resolvable;
public class Person {
@Resolvable(colName = "Name")
private String name;
@Resolvable(colName = "Age", formatter = IntFormatter.class)
private int age;
private Person parent;
public Person(String name, int age) {
this(name, age, null);
}
public Person(String name, int age, Person parent) {
this.name = name;
this.age = age;
this.parent = parent;
}
//Getters and setters omitted
}
创建表格所需的代码就是这些
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import mark.utils.el.annotation.AnnotationResolver;
import mark.utils.swing.table.ObjectTableModel;
import test.Person;
public class ObjectTableModelDemo {
public void show() {
//Here we create the resolver for annotated classes.
AnnotationResolver resolver = new AnnotationResolver(Person.class);
//We use the resolver as parameter to the ObjectTableModel
//and the String represent the cols.
ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
resolver, "name,age");
//Here we use the list to be the data of the table.
tableModel.setData(getData());
JTable table = new JTable(tableModel);
JFrame frame = new JFrame("ObjectTableModel");
JScrollPane pane = new JScrollPane();
pane.setViewportView(table);
pane.setPreferredSize(new Dimension(400,200));
frame.add(pane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
//Just for create a default List to show.
private List<Person> getData() {
List<Person> list = new ArrayList<Person>();
list.add(new Person("Marky", 17, new Person("Marcos", 40)));
list.add(new Person("Jhonny", 21));
list.add(new Person("Douglas", 50, new Person("Adams", 20)));
return list;
}
public static void main(String[] args) {
new ObjectTableModelDemo().show();
}
}
ObjectTableModel
类的第二个参数可以更强大。
您不限于该类的属性。您可以使用其中字段的属性。
AnnotationResolver resolver = new AnnotationResolver(Person.class);
ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
resolver, "name,age,parent.name,parent.age");
如果您使用“parent.name
”,您将看到此 Person
的父级的名称。
您也可以指定列名。只需在字段名后加上冒号(:),然后写上列名。
AnnotationResolver resolver = new AnnotationResolver(Person.class);
ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
resolver, "name:Person Name,age:Person Age,parent.name:Parent Name,parent.age:Parent Age");
以下所有文本,请勿写入 objecttablemodel_demo.zip 压缩包中。
2.1.2 自定义格式化器
在大多数情况下,仅 String
就足以提供正确的可视化。
但是如果我们想在表格中放置一个 Calendar
字段呢?
java.util.GregorianCalendar[time=-367016400000,areFieldsSet=true,areAllFieldsSet=
true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Sao_Paulo",offset=
-10800000,dstSavings=3600000,useDaylight=true,transitions=129,lastRule=java.util.
SimpleTimeZone[id=America/Sao_Paulo,offset=-10800000,dstSavings=3600000,useDaylight=
true,startYear=0,startMode=3,startMonth=9,startDay=15,startDayOfWeek=1,startTime=0,
startTimeMode=0,endMode=3,endMonth=1,endDay=15,endDayOfWeek=1,endTime=0,endTimeMode=
0]],firstDayOfWeek=2,minimalDaysInFirstWeek=1,ERA=1,YEAR=1958,MONTH=4,WEEK_OF_YEAR=
20,WEEK_OF_MONTH=3,DAY_OF_MONTH=16,DAY_OF_YEAR=136,DAY_OF_WEEK=6,DAY_OF_WEEK_IN_MONTH
=3,AM_PM=0,HOUR=0,HOUR_OF_DAY=0,MINUTE=0,SECOND=0,MILLISECOND=0,ZONE_OFFSET=
-10800000,DST_OFFSET=0]
这不适合显示。
为此,我们创建一个 mark.utils.bean.Formatter
的新实例。
它的方法签名是
package mark.utils.bean;
/**
*@author Marcos Vasconcelos
*/
public interface Formatter {
/**
* Convert a object to String.
*/
public abstract String format(Object obj);
/**
* Convert the String to the Object.
*/
public abstract Object parse(String s);
/**
* Naming proposes only
*/
public abstract String getName();
}
我们可以在 @Resolvable
注解中设置格式化器,这是我的日历实现。
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import mark.utils.bean.Formatter;
public class CalendarFormatter implements Formatter {
private final static SimpleDateFormat formatter = new SimpleDateFormat(
"dd/MM/yyyy");
@Override
public String format(Object obj) {
Calendar cal = (Calendar) obj;
return formatter.format(cal.getTime());
}
@Override
public String getName() {
return "calendar";
}
@Override
public Object parse(String s) {
Calendar cal = new GregorianCalendar();
try {
cal.setTime(formatter.parse(s));
} catch (ParseException e) {
e.printStackTrace();
}
return cal;
}
}
回到 Person
类,我们现在可以创建另一个字段并将其放入我们的 Table
中。
@Resolvable(formatter = CalendarFormatter.class)
private Calendar birth;
对于我们的表格,我们可以使用 String: "name,age,birth"
。
第三列将显示类似“26/06/1991
”的值。
比标准的 Calendar.toString()
更适合显示。
2.1.3 List 接口的方法
我在模型中添加了 List
接口的方法,这样就可以像处理列表一样轻松地添加和删除对象。
这里有一个使用这些方法的示例。
ObjectTableModel<Person> model = new ObjectTableModel<Person>(
new AnnotationResolver(Person.class).resolve("name,age"));
Person person1 = new Person("Marky", 17);
Person person2 = new Person("MarkyAmeba", 18);
model.add(person1);
model.add(person2);
List<Person> list = new ArrayList<Person>();
list.add(new Person("Marcos", 40));
list.add(new Person("Rita", 40));
model.addAll(list);
int index = model.indexOf(person2);// Should return 2
model.remove(index);
model.remove(person1);
model.clean();
2.1.4 更新和从模型中获取对象
当然。表格不仅仅用于显示数据。在模型中,有一个名为 setEditDefault
的方法,而 isEditable(int x, int y)
方法返回此值。(这意味着如果设置为 true
,则整个表格都可以编辑。如果设置为 false
,则整个表格都不可编辑)。
如果设置为 true
,您可以编辑单元格。焦点丢失后,表格会在 Model
中调用 setValueAt
,并将其设置到相应的对象中。
值以 String
的形式传递,FieldResolver
使用其 Formatter 实例将值转换为对象以进行设置。这意味着您不限于处理 String
,而是可以处理任何 Object
。实现正确的 Formatter 即可实现这一点。
这是一个关于它是如何工作的示例。
首先。我们的模型和格式化器。
import mark.utils.el.annotation.Resolvable;
import mark.utils.el.handler.MethodHandler;
public class Person {
@Resolvable(colName = "Name")
private String name;
@Resolvable(colName = "Age", formatter = IntFormatter.class)
private int age;
private Person parent;
public Person(String name, int age, Person parent) {
this.name = name;
this.age = age;
this.parent = parent;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public static class IntFormatter implements Formatter {
@Override
public String format(Object obj) {
return Integer.toString((Integer) obj);
}
@Override
public String getName() {
return "int";
}
@Override
public Object parse(String s) {
return Integer.parseInt(s);
}
}
}
这是一个例子。
package test.el.annotation;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import mark.utils.el.annotation.AnnotationResolver;
import mark.utils.swing.table.ObjectTableModel;
import org.junit.Test;
import test.Person;
public class AnnotationResolverTest {
@Test
public void testAnnotationResolverInit() {
AnnotationResolver resolver = new AnnotationResolver(Person.class);
ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
resolver,
"name,age,parent.name:Parent,parent.age:Parent age");
tableModel.setData(getData());
tableModel.setEditableDefault(true);
JTable table = new JTable(tableModel);
JFrame frame = new JFrame("ObjectTableModel");
JScrollPane pane = new JScrollPane();
pane.setViewportView(table);
pane.setPreferredSize(new Dimension(400, 200));
frame.add(pane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
private List<Person> getData() {
List<Person> list = new ArrayList<Person>();
list.add(new Person("Marky", 17, new Person("Marcos", 40)));
list.add(new Person("Jhonny", 21, new Person("",0)));
list.add(new Person("Douglas", 50, new Person("Adams",20)));
return list;
}
public static void main(String[] args) {
new AnnotationResolverTest().testAnnotationResolverInit();
}
}
单元格中的任何更改都会更新对象。
获取行的对象
处理 JTables
最糟糕的部分是获取其值。几乎所有时候,我们需要使用 getValutAt
来获取值,并将其设置为对象中的相应值。但本项目旨在实现这一点。
ObjectTableModel
的 getValue(int row)
方法返回指定行的 Object
。
ObjectTableModel
是类型化的,并且 getValue
方法返回该类型的一个对象,避免了类型转换。
以下代码返回第二行的 Person
。
AnnotationResolver resolver = new AnnotationResolver(Person.class);
ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
resolver,
"name,age,parent.name:Parent,parent.age:Parent age");
tableModel.setData(getData());
tableModel.setEditableDefault(true);
Person person = tableModel.getValue(2);//The row
System.out.println(person.getName());
2.2 高级
2.2.1 FieldResolver
这个项目的所有后台都在这个类中。而 @Resolvable
和 AnnotationResolver
只是为 ObjectTableModel
创建 FieldResolver
实例。
但您仍然可以代替注解使用它。
以下代码
FieldResolver nameResolver = new FieldResolver(Person.class, "name");
FieldResolver ageResolver = new FieldResolver(Person.class, "age");
ageResolver.setFormatter(new IntFormatter());
FieldResolver parentNameResolver = new FieldResolver(Person.class,
"paren.name", "Parent");
FieldResolver parentAgeResolver = new FieldResolver(Person.class,
"parent.age", "Parent age");
FieldResolver birthResolver = new FieldResolver(Person.class, "birth",
"Birth day");
birthResolver.setFormatter(new CalendarFormatter());
ObjectTableModel<Person> model = new ObjectTableModel<Person>(
new FieldResolver[] { nameResolver, ageResolver,
parentNameResolver, parentAgeResolver, birthResolver });
这等同于以下代码
AnnotationResolver resolver = new AnnotationResolver(Person.class);
ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
resolver,
"name,age,parent.name:Parent,parent.age:Parent age,birth: Birth day");
tableModel.setData(getData());
但是在第一种情况下,我们不需要 Person
类字段中的 @Resolvable
注解。
Field Resolver 工厂
FieldResolverFactory
仅为提高代码可读性而存在,使其易于创建新的 FieldResolvers
。它的构造函数需要一个 Class<?>
对象,该对象代表我们正在为其创建字段解析器的类(与在 FieldResolver
构造函数中传递的相同)。
主要方法是
createResolver(String fieldName)
createResolver(String fieldName, String colName)
createResolver(String fieldName, Formatter formatter)
createResolver(String fieldName, String colName, Formatter formatter)
使用工厂的第一个 FieldResolver
示例应该是
FieldResolverFactory fac = new FieldResolverFactory(Person.class);
FieldResolver nameRslvr = fac.createResolver("name");
FieldResolver ageRslvr = fac.createResolver("age", new IntFormatter());
FieldResolver parentNameRslvr = fac.createResolver("paren.name",
"Parent");
FieldResolver parentAgeRslvr = fac.createResolver("parent.age",
"Parent age", new IntFormatter());
FieldResolver birthRslvr = fac.createResolver("birth", "Birth day",
new CalendarFormatter());
ObjectTableModel<Person> model = new ObjectTableModel<Person>(
new FieldResolver[] { nameRslvr, ageRslvr, parentNameRslvr,
parentAgeRslvr, birthRslvr });
2.2.2 FieldHandler 和 MethodHandler
到目前为止,我们使用的是默认的 FieldAccessHandler
,即 FieldHandler
。
使用它,我们不需要类中的任何 getter/setter 方法。所有内容都直接通过反射访问。
使用 MethodHandler
时,类会在给定的类中搜索 getter 或 is/setter 方法。
一个简单的例子。
import java.util.ArrayList;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
import mark.utils.el.annotation.Resolvable;
import mark.utils.el.handler.MethodHandler;
public class Person {
@Resolvable(colName = "Name", accessMethod = MethodHandler.class)
private String name;
@Resolvable(colName = "Age", formatter = IntFormatter.class)
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return "The name is: " + name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return 150;
}
public void setAge(int age) {
this.age = age;
}
}
以及 TableModel
的 init
。
AnnotationResolver resolver = new AnnotationResolver(Person.class);
ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
resolver,
"name,age");
tableModel.setData(getData());
运行此代码,我们会注意到所有姓名列都以“The name is:
”开头,因为它在 getName
方法中,我们为此字段使用了 MethodHandler
。但对于始终返回 150
的 getAge
,我们可以注意到实际设置的年龄属性,因为它仍然使用 FieldHandler
。
3. 兴趣点
反射非常强大。请查看 mark.utils.el
包及其子包,了解此项目中所有基于反射的后台。
历史
- 2009年6月5日:初始版本