使用多个自定义表模型,避免重复代码

时间:2014-10-21 21:50:53

标签: java swing jtable tablemodel abstracttablemodel

我正在一个项目中,我们有几个域类来建模业务数据。这些类是简单的POJO,我必须使用它们显示几个表。例如,考虑这个类:

public class Customer {

    private Long id;
    private Date entryDate;
    private String name;
    private String address;
    private String phoneNumber;

    public Customer(Long id, Date entryDate, String name, String address, String phoneNumber) {
        this.id = id;
        this.entryDate = entryDate;
        this.nombre = name;
        this.domicilio = address;
        this.telefono = phoneNumber;
    }

    // Getters and setters here
}

我已创建了自己的AbstractTableModel扩展表模型,以便直接使用Customer类:

public class CustomerTableModel extends AbstractTableModel {

    private final List<String> columnNames;
    private final List<Customer> customers;

    public CustomerTableModel() {
        String[] header = new String[] {
            "Entry date",
            "Name",
            "Address",
            "Phone number"
        };
        this.columnNames = Arrays.asList(header);
        this.customers = new ArrayList<>();
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
        switch (columnIndex) {
            case 0: return Date.class;
            case 1: return String.class;
            case 2: return String.class;
            case 3: return String.class;
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        Customer customer = getCustomer(rowIndex);
        switch (columnIndex) {
            case 0: return customer.getEntryDate();
            case 1: return customer.getName();
            case 2: return customer.getAddress();
            case 3: return customer.getPhoneNumber();
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return true;
    }

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        if (columnIndex < 0 || columnIndex >= getColumnCount()) {
            throw new ArrayIndexOutOfBoundsException(columnIndex);
        } else {
            Customer customer = getCustomer(rowIndex);
            switch (columnIndex) {
                case 0: customer.setEntryDate((Date)aValue); break;
                case 1: customer.setName((String)aValue); break;
                case 2: customer.setAddress((String)aValue); break;
                case 3: customer.setPhoneNumber((String)aValue); break;
            }
            fireTableCellUpdated(rowIndex, columnIndex);
        }
    }

    @Override
    public int getRowCount() {
        return this.customers.size();
    }

    @Override
    public int getColumnCount() {
        return this.columnNames.size();
    }

    @Override
    public String getColumnName(int columnIndex) {
        return this.columnNames.get(columnIndex);
    }

    public void setColumnNames(List<String> columnNames) {
        if (columnNames != null) {
            this.columnNames.clear();
            this.columnNames.addAll(columnNames);
            fireTableStructureChanged();
        }
    }

    public List<String> getColumnNames() {
        return Collections.unmodifiableList(this.columnNames);
    }

    public void addCustomer(Customer customer) {
        int rowIndex = this.customers.size();
        this.customers.add(customer);
        fireTableRowsInserted(rowIndex, rowIndex);
    }

    public void addCustomers(List<Customer> customerList) {
        if (!customerList.isEmpty()) {
            int firstRow = this.customers.size();
            this.customers.addAll(customerList);
            int lastRow = this.customers.size() - 1;
            fireTableRowsInserted(firstRow, lastRow);
        }
    }

    public void insertCustomer(Customer customer, int rowIndex) {
        this.customers.add(rowIndex, customer);
        fireTableRowsInserted(rowIndex, rowIndex);
    }

    public void deleteCustomer(int rowIndex) {
        if (this.customers.remove(this.customers.get(rowIndex))) {
            fireTableRowsDeleted(rowIndex, rowIndex);
        }
    }

    public Customer getCustomer(int rowIndex) {
        return this.customers.get(rowIndex);
    }

    public List<Customer> getCustomers() {
        return Collections.unmodifiableList(this.customers);
    }

    public void clearTableModelData() {
        if (!this.customers.isEmpty()) {
            int lastRow = customers.size() - 1;
            this.customers.clear();
            fireTableRowsDeleted(0, lastRow);
        }
    }
}

到现在为止一切都很好。然而,这种方法至少有两个问题:

  1. 由于我必须为每个类实现一个表模型,因此我将生成大量重复代码以基本上做三件事:定义适当的表头,向/从底层结构添加/删除对象(列表),覆盖setValueAt()getValueAt()方法以使用用户定义的对象。

  2. 假设我拥有完全相同的客户列表,但我必须在两个不同的表中提供这些表,其中包含不同的标题或数据。我必须子类化我的表模型并覆盖它需要被覆盖的任何东西才能满足这个要求。它根本不会很优雅。

  3. 问题:有没有办法摆脱样板代码,使我的表格模型灵活且可重复使用?

2 个答案:

答案 0 :(得分:6)

与其他Swing模型(例如:DefaultComboBoxModelDefaultListModel)一样,我们可以使用Generics来创建灵活且可重复使用的表模型,同时提供API以与用户一起使用 - 定义了POJO。

此表格模型具有以下特殊功能:

  • 它从AbstractTableModel扩展到利用表模型事件处理。
  • 与上面显示的CustomerTableModel不同,此表模型必须是抽象的,因为它不能覆盖getValueAt()方法:仅仅因为我们不知道此表模型将处理的类或数据类型,覆盖上述方法的任务留给了子类。
  • 它继承了setValueAt()的空AbstractTableModel实现。这是有道理的,因为isCellEditable()也是从该类继承而且始终返回false
  • getColumnClass()的默认实现也是继承的,并且始终返回Object.class

这些功能使得这个表模型非常易于实现,具体取决于我们的要求:

  • 如果我们需要显示只读表格,那么我们必须覆盖2个方法:getValueAt()getColumnClass()(建议使用最后一个但不是强制性的。)
  • 如果我们的表格需要可编辑,那么我们必须覆盖4种方法:上面提到的两种方法加上isCellEditable()setValueAt()

让我们来看看我们的表模型的代码:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.table.AbstractTableModel;

/**
 * Abstract base class which extends from {@code AbstractTableModel} and 
 * provides an API to work with user-defined POJO's as table rows. Subclasses 
 * extending from {@code DataObjectTableModel} must implement 
 * {@code getValueAt(row, column)} method. 
 * <p />
 * By default cells are not editable. If those have to be editable then 
 * subclasses must override both {@code isCellEditable(row, column)} and 
 * {@code setValueAt(row, column)} methods.
 * <p />
 * Finally, it is not mandatory but highly recommended to override 
 * {@code getColumnClass(column)} method, in order to return the appropriate 
 * column class: default implementation always returns {@code Object.class}.
 * 
 * @param <T> The class handled by this TableModel.
 * @author dic19
 */
public abstract class DataObjectTableModel<T> extends AbstractTableModel {

    private final List<String> columnNames;
    private final List<T> data;

    public DataObjectTableModel() {
        this.data = new ArrayList<>();
        this.columnNames = new ArrayList<>();
    }

    public DataObjectTableModel(List<String> columnIdentifiers) {
        this();
        if (columnIdentifiers != null) {
            this.columnNames.addAll(columnIdentifiers);
        }
    }

    @Override
    public int getRowCount() {
        return this.data.size();
    }

    @Override
    public int getColumnCount() {
        return this.columnNames.size();
    }

    @Override
    public String getColumnName(int columnIndex) {
        return this.columnNames.get(columnIndex);
    }

    public void setColumnNames(List<String> columnNames) {
        if (columnNames != null) {
            this.columnNames.clear();
            this.columnNames.addAll(columnNames);
            fireTableStructureChanged();
        }
    }

    public List<String> getColumnNames() {
        return Collections.unmodifiableList(this.columnNames);
    }

    public void addDataObject(T dataObject) {
        int rowIndex = this.data.size();
        this.data.add(dataObject);
        fireTableRowsInserted(rowIndex, rowIndex);
    }

    public void addDataObjects(List<T> dataObjects) {
        if (!dataObjects.isEmpty()) {
            int firstRow = data.size();
            this.data.addAll(dataObjects);
            int lastRow = data.size() - 1;
            fireTableRowsInserted(firstRow, lastRow);
        }
    }

    public void insertDataObject(T dataObject, int rowIndex) {
        this.data.add(rowIndex, dataObject);
        fireTableRowsInserted(rowIndex, rowIndex);
    }

    public void deleteDataObject(int rowIndex) {
        if (this.data.remove(this.data.get(rowIndex))) {
            fireTableRowsDeleted(rowIndex, rowIndex);
        }
    }

    public void notifyDataObjectUpdated(T domainObject) {
        T[] elements = (T[])data.toArray();
        for (int i = 0; i < elements.length; i++) {
            if(elements[i] == domainObject) {
                fireTableRowsUpdated(i, i);
            }
        }
    }

    public T getDataObject(int rowIndex) {
        return this.data.get(rowIndex);
    }

    public List<T> getDataObjects(int firstRow, int lastRow) {
        List<T> subList = this.data.subList(firstRow, lastRow);
        return Collections.unmodifiableList(subList);
    }

    public List<T> getDataObjects() {
        return Collections.unmodifiableList(this.data);
    }

    public void clearTableModelData() {
        if (!this.data.isEmpty()) {
            int lastRow = data.size() - 1;
            this.data.clear();
            fireTableRowsDeleted(0, lastRow);
        }
    }
}

因此,采用此表模型和Customer类,完整的实现将如下所示:

String[] header = new String[] {"Entry date", "Name", "Address", "Phone number"};
DataObjectTableModel<Customer> model = new DataObjectTableModel<>(Arrays.asList(header)) {
    @Override
    public Class<?> getColumnClass(int columnIndex) {
        switch (columnIndex) {
            case 0: return Date.class;
            case 1: return String.class;
            case 2: return String.class;
            case 3: return String.class;
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        Customer customer = getDataObject(rowIndex);
        switch (columnIndex) {
            case 0: return customer.getEntryDate();
            case 1: return customer.getName();
            case 2: return customer.getAddress();
            case 3: return customer.getPhoneNumber();
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return true;
    }

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        if (columnIndex < 0 || columnIndex >= getColumnCount()) {
            throw new ArrayIndexOutOfBoundsException(columnIndex);
        } else {
            Customer customer = getDataObject(rowIndex);
            switch (columnIndex) {
                case 0: customer.setEntryDate((Date)aValue); break;
                case 1: customer.setName((String)aValue); break;
                case 2: customer.setAddress((String)aValue); break;
                case 3: customer.setPhoneNumber((String)aValue); break;
            }
            fireTableCellUpdated(rowIndex, columnIndex);
        }
    }
};

正如我们所看到的,在几行代码(LOC&lt; 50)中,我们有一个完整的实现。


它适用于JPA实体吗?

只要实体拥有公共getter和setter,它就会这样做。与JPA实现不同,此表模型不适用于反射,因此我们必须使用类的公共接口访问对象属性以实现getValueAt()setValueAt()方法。

它适用于JDBC吗?

不,不。我们必须将结果集包装到域类中,并使用上面提到的类提供的接口。

它是否适用于Java默认类?

是的。再一次,使用类'提供的接口。例如,让我们使用java.io.File类,我们可以使用以下表模型实现:

String[] header = new String[] {
    "Name",
    "Full path",
    "Last modified",
    "Read",
    "Write",
    "Execute",
    "Hidden",
    "Directory"
};

DataObjectTableModel<File> model = new DataObjectTableModel<File>(Arrays.asList(header)) {
    @Override
    public Class<?> getColumnClass(int columnIndex) {
        switch (columnIndex) {
            case 0: return String.class;
            case 1: return String.class;
            case 2: return Date.class;
            case 3: return Boolean.class;
            case 4: return Boolean.class;
            case 5: return Boolean.class;
            case 6: return Boolean.class;
            case 7: return Boolean.class;
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        File file = getDataObject(rowIndex);
        switch (columnIndex) {
            case 0: return file.getName();
            case 1: return file.getAbsolutePath();
            case 2: return new Date(file.lastModified());
            case 3: return file.canRead();
            case 4: return file.canWrite();
            case 5: return file.canExecute();
            case 6: return file.isHidden();
            case 7: return file.isDirectory();
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }
};

答案 1 :(得分:3)

与dic19的答案一样,您可以使用Row Table Model,它也使用泛型并提供许多常用方法,允许您动态更新TableModel。

您还需要实现一些方法,因为该模型也是抽象的。 JButtonTableModel.java代码显示了如何执行此操作。

此外,如果你想真正想要的话,可以查看Bean Table Model(上面博客中的链接),它扩展了RowTableModel。此模型使用反射来构建TableModel,因此您不必实现自定义模型。