JTable自定义单元格渲染器:根据对象属性设置样式的更优实现方式咨询
JTable自定义单元格渲染器:根据对象属性设置样式的更优实现方式咨询
我现在遇到这么个需求:用Swing的JTable展示Person对象列表,每个Person有个mIsDead的布尔属性,想要给已死亡的Person的“Name”列设置不同的文本颜色。目前我是把PersonTableModel直接传递给TableCellRenderer,在渲染时通过模型获取对应行的Person对象来判断样式,但总觉得这种方式耦合度太高,想问问有没有更简洁优雅的实现思路?
(注:下面的代码是简化的演示版本,把所有类都放在一个文件里方便测试,实际项目中这些都是拆分到不同文件的独立类)
package stackoverflow; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.util.ArrayList; import java.util.function.Function; import javax.swing.JFrame; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.UIManager; import javax.swing.event.TableModelListener; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableModel; public class ItemBasedTableCellRenderer extends JFrame { private static final long serialVersionUID = 1538589452450196943L; public static void main(final String... args) throws Exception { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); new ItemBasedTableCellRenderer().setVisible(true); } /* 基础泛型TableModel类,实际项目中是独立的工具类 */ public abstract class JcTableModel<T> implements TableModel { protected final ArrayList<TableModelListener> mListeners = new ArrayList<>(); protected final ArrayList<JcJTableColumnWrapper<T>> mColumns = new ArrayList<>(); protected final ArrayList<T> mItems = new ArrayList<>(); public JcJTableColumnWrapper<T> addColumn(final String pTitle, final Function<T, Object> pGetValueLambda, final Class<?> pColType) { final JcJTableColumnWrapper<T> wrapper = new JcJTableColumnWrapper<>(pTitle, pGetValueLambda, pColType); mColumns.add(wrapper); return wrapper; } public ArrayList<T> getBackingList() { return mItems; } @Override public int getColumnCount() { return mColumns.size(); } @Override public int getRowCount() { return getBackingList().size(); } @Override public String getColumnName(final int pColumn) { return mColumns.get(pColumn).mTitle; } @Override public Class<?> getColumnClass(final int pColumnIndex) { return mColumns.get(pColumnIndex).mColType; } @Override public Object getValueAt(final int pRow, final int pColumn) { final T item = getBackingList().get(pRow); return mColumns.get(pColumn).getValue(item); } @Override public void setValueAt(final Object pArg0, final int pArg1, final int pArg2) { /* 空实现,实际项目中会处理编辑逻辑 */ } @Override public boolean isCellEditable(final int pRow, final int pColumn) { return false; } @Override public void addTableModelListener(final TableModelListener pArg0) { mListeners.add(pArg0); } @Override public void removeTableModelListener(final TableModelListener pArg0) { mListeners.remove(pArg0); } public T getItemInRow(final int pRowIndex) { if (pRowIndex < 0 || mItems.size() <= pRowIndex) throw new IllegalArgumentException("Invalid index [" + pRowIndex + "]. Possible: [" + (mItems.size() < 1 ? "NONE" : ("0-" + (mItems.size() - 1)) + "]")); return mItems.get(pRowIndex); } } /* 列包装类,实际项目中是独立工具类 */ public class JcJTableColumnWrapper<T> { public final String mTitle; private final Function<T, Object> mLambda; public final Class<?> mColType; public JcJTableColumnWrapper(final String pTitle, final Function<T, Object> pGetValueLambda, final Class<?> pColType) { mTitle = pTitle; mLambda = pGetValueLambda; mColType = pColType; } public Object getValue(final T pT) { return mLambda.apply(pT); } } static class Person { public final String mName; public final int mAge; public final int mHeight; public final boolean mIsDead; public Person(final String pName, final int pAge, final int pHeight, final boolean pIsDead) { mName = pName; mAge = pAge; mHeight = pHeight; mIsDead = pIsDead; } } private class PersonTableModel extends JcTableModel<Person> { public PersonTableModel() { addColumn("Name", i -> i.mName, String.class); addColumn("Age", i -> Integer.valueOf(i.mAge), Integer.class); addColumn("Height", i -> Integer.valueOf(i.mHeight), Integer.class); // 模拟测试数据 getBackingList().add(new Person("Alice", 25, 165, false)); getBackingList().add(new Person("Bob", 30, 180, true)); getBackingList().add(new Person("Charlie", 22, 175, false)); } } private class PersonTablecellRenderer extends DefaultTableCellRenderer { private static final long serialVersionUID = -2926459004469528514L; private final PersonTableModel mModel; public PersonTablecellRenderer(final PersonTableModel pModel) { mModel = pModel; } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); // 注意:这里直接用row作为模型索引是有问题的,如果表格启用排序/过滤,row是视图索引,不是模型索引! final Person person = mModel.getItemInRow(row); if (person.mIsDead) { setForeground(Color.RED); } else { setForeground(isSelected ? table.getSelectionForeground() : UIManager.getColor("Table.foreground")); } return this; } } public ItemBasedTableCellRenderer() { super("Item Based Table Cell Renderer"); setDefaultCloseOperation(EXIT_ON_CLOSE); setSize(400, 300); setLocationRelativeTo(null); PersonTableModel model = new PersonTableModel(); JTable table = new JTable(model); // 给Name列设置自定义渲染器 table.getColumnModel().getColumn(0).setCellRenderer(new PersonTablecellRenderer(model)); add(new JScrollPane(table), BorderLayout.CENTER); } }
现有方案的不足
- 耦合度过高:
PersonTablecellRenderer直接依赖PersonTableModel,导致渲染器无法复用在其他模型上,而且一旦模型结构变动,渲染器也得跟着修改。 - 存在潜在bug:渲染器中直接使用
row参数作为模型索引,如果表格启用了排序或过滤,row是视图索引,和模型中的索引不一致,会导致获取到错误的Person对象。
更优的实现方式
方案1:通过函数式接口解耦渲染器与模型
让渲染器不直接依赖具体的TableModel,而是接收一个Function<Integer, Person>类型的回调函数,用来根据模型索引获取对应的Person对象。这样渲染器只关注“如何根据Person设置样式”,完全不关心数据从哪里来。
修改后的渲染器代码:
private class PersonTablecellRenderer extends DefaultTableCellRenderer { private static final long serialVersionUID = -2926459004469528514L; private final Function<Integer, Person> rowToPerson; public PersonTablecellRenderer(Function<Integer, Person> rowToPerson) { this.rowToPerson = rowToPerson; } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); // 转换为模型索引,解决排序/过滤后的索引不一致问题 int modelRow = table.convertRowIndexToModel(row); Person person = rowToPerson.apply(modelRow); if (person != null && person.mIsDead) { setForeground(Color.RED); } else { // 适配选中状态的默认颜色 setForeground(isSelected ? table.getSelectionForeground() : UIManager.getColor("Table.foreground")); } return this; } }
使用时,只需要传入模型的getItemInRow方法引用即可:
PersonTableModel model = new PersonTableModel(); JTable table = new JTable(model); // 给Name列设置渲染器,通过方法引用传递行对象获取逻辑 table.getColumnModel().getColumn(0).setCellRenderer(new PersonTablecellRenderer(model::getItemInRow));
方案2:基于通用模型接口解耦
如果项目中有多个类似的泛型模型,可以定义一个通用的RowItemModel接口,让所有自定义模型实现这个接口,渲染器依赖这个接口而不是具体的模型类。
定义通用接口:
public interface RowItemModel<T> { T getItemInRow(int rowIndex); }
修改JcTableModel实现这个接口:
public abstract class JcTableModel<T> implements TableModel, RowItemModel<T> { // 原有代码不变,getItemInRow方法已经实现了接口要求 }
然后修改渲染器依赖这个通用接口:
private class PersonTablecellRenderer extends DefaultTableCellRenderer { private static final long serialVersionUID = -2926459004469528514L; private final RowItemModel<Person> itemModel; public PersonTablecellRenderer(RowItemModel<Person> itemModel) { this.itemModel = itemModel; } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); int modelRow = table.convertRowIndexToModel(row); Person person = itemModel.getItemInRow(modelRow); if (person != null && person.mIsDead) { setForeground(Color.RED); } else { setForeground(isSelected ? table.getSelectionForeground() : UIManager.getColor("Table.foreground")); } return this; } }
使用方式和之前类似,但渲染器现在可以支持任何实现了RowItemModel<Person>的模型,复用性更强。
方案总结
这两种方案都解决了原有实现的问题:
- 降低了耦合度:渲染器不再依赖具体的模型类,只依赖抽象接口或函数式接口
- 修复了索引问题:通过
convertRowIndexToModel处理视图和模型索引不一致的情况 - 提升了复用性:同一个渲染器可以配合不同的模型使用,只要能提供获取Person对象的逻辑
内容来源于stack exchange
相关产品推荐
相关产品推荐

