65.9K
CodeProject 正在变化。 阅读更多。
Home

简单的强大 TableModel(带反射)

starIconstarIconstarIconemptyStarIconemptyStarIcon

3.00/5 (3投票s)

2009年5月6日

CPOL

6分钟阅读

viewsIcon

31744

downloadIcon

600

TableModel 复杂性不再是问题。

源代码更新日期: 2009年5月28日

本文的葡萄牙语版本,请访问 此链接

章节目录

  1. 动机
  2. 开始编码
    1. Basic
      1. 引言
      2. 自定义格式化器
      3. List 接口的方法
      4. 更新和从模型中获取对象
    2. 高级
      1. FieldResolver
      2. FieldHandler 和 MethodHandler
  3. 关注点

1. 动机

“不要使用 DefaultTableModel”,我经常遇到一些人在使用 DefaultTableModel 实现时遇到问题,我的建议是,不要使用它。但是实现一个能让所有工作都变得简单的类,也并非易事。所以我实现了一个,现在在这里与大家分享。

我的目标是编写一个单一的 TableModel,简单、可扩展、易读且功能强大。这一切都通过反射和注解来实现。

有了这个模型,您可以

  • 添加并获取每一行的当前对象
  • 无需处理 String 数组
  • 在单元格更新时保持对象同步更新
  • 通过注解进行配置,简化代码的可读性
  • List 接口的方法,如:addaddAllremoveindexOf
  • 如果您不喜欢注解,仍然可以使用它(参见第 2.2.1 章)

2. 开始编码

2.1 基础

2.1.1 简介

首先:下载包含项目源代码的 objecttablemodel.zip 压缩包。

这个项目中值得关注的类有

  • ObjectTableModel,这是实现的表格模型
  • FieldResolver,后台工作在这里进行,访问表格列的字段
  • @Resolvable,标记字段的注解,用于表格的默认值。如格式化器(如果需要)、Column 名称以及实际访问字段的 FieldAccessHandler
  • 实现的 FieldAccessHandlersFieldHandler(默认),它直接使用类中的 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 来获取值,并将其设置为对象中的相应值。但本项目旨在实现这一点。

ObjectTableModelgetValue(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

这个项目的所有后台都在这个类中。而 @ResolvableAnnotationResolver 只是为 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;
	}
}

以及 TableModelinit

AnnotationResolver resolver = new AnnotationResolver(Person.class);
		ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
				resolver,
				"name,age");
		tableModel.setData(getData());

运行此代码,我们会注意到所有姓名列都以“The name is: ”开头,因为它在 getName 方法中,我们为此字段使用了 MethodHandler。但对于始终返回 150getAge,我们可以注意到实际设置的年龄属性,因为它仍然使用 FieldHandler

3. 兴趣点

反射非常强大。请查看 mark.utils.el 包及其子包,了解此项目中所有基于反射的后台。

历史

  • 2009年6月5日:初始版本
© . All rights reserved.