You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
    }
}

现有方案的不足

  1. 耦合度过高:PersonTablecellRenderer直接依赖PersonTableModel,导致渲染器无法复用在其他模型上,而且一旦模型结构变动,渲染器也得跟着修改。
  2. 存在潜在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 03:13:11