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

JTable可扩展单元格编辑器:行高动态调整问题求助

解决方案:JTable可展开折叠单元格的行高动态调整

核心问题梳理

  • 点击展开/折叠按钮时,需同步更新对应行高度,折叠时恢复默认行高
  • 原方案用ComponentListener.componentResized导致表格持续重绘,preferredSize监听器无法保证表格可靠重绘
  • 需要实现切换单元格时保留展开状态(将展开状态绑定到数据模型)

关键修改方向

  1. 展开状态与数据模型绑定:用Map存储每行的展开状态(实际项目可直接封装到表格数据对象中),确保切换单元格或编辑结束后状态不丢失
  2. 直接在按钮点击事件中更新行高:避开易引发循环重绘的组件监听器,点击按钮时直接计算组件高度并更新表格行高
  3. 修复编辑器两次点击激活问题:调整表格选择设置,让单元格点击一次即可进入编辑状态
  4. 统一渲染与编辑的状态同步:渲染器和编辑器从同一状态源获取展开状态,保证显示一致性

修改后的完整代码

package io.github.bric3.fireplace.ui;

import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import javax.swing.border.TitledBorder;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.util.HashMap;
import java.util.Map;

public class DynamicCellRow {
    // 存储每行展开状态,实际项目可将该属性整合到数据模型对象中
    private static final Map<Integer, Boolean> rowExpandedState = new HashMap<>();
    private static final int DEFAULT_ROW_HEIGHT = 25; // 默认行高

    @NotNull
    private static JTable makeTable() {
        var jTable = new JTable(
                new Object[][]{ {"a", "charly"}, {"b", "tango"} },
                new Object[]{"id", "control"}
        );
        jTable.setRowHeight(DEFAULT_ROW_HEIGHT); // 初始化默认行高

        {
            var richColumn = jTable.getColumnModel().getColumn(1);
            richColumn.setCellRenderer(new TableCellRenderer() {
                private final ExpandablePanel expandablePanelRenderComponent = new ExpandablePanel();

                @Override
                public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
                    boolean isExpanded = rowExpandedState.getOrDefault(row, false);
                    expandablePanelRenderComponent.setValue(value);
                    expandablePanelRenderComponent.setExpanded(isExpanded);
                    return updateRowHeight(table, expandablePanelRenderComponent, row, column);
                }
            });

            richColumn.setCellEditor(new DynamicCellEditor(jTable));
            // 配置表格点击直接进入编辑状态,解决两次点击激活问题
            jTable.setCellSelectionEnabled(true);
            jTable.setRowSelectionAllowed(true);
            jTable.setColumnSelectionAllowed(true);
        }
        return jTable;
    }

    public static <T extends JComponent> T updateRowHeight(JTable table, T cellComponent, int row, int column) {
        // 计算组件适配列宽后的首选高度
        cellComponent.setSize(table.getColumnModel().getColumn(column).getWidth(), Short.MAX_VALUE);
        int preferredHeight = cellComponent.getPreferredSize().height;
        // 根据展开状态设置行高:展开用组件高度,折叠用默认高度
        boolean isExpanded = rowExpandedState.getOrDefault(row, false);
        table.setRowHeight(row, isExpanded ? preferredHeight : DEFAULT_ROW_HEIGHT);
        return cellComponent;
    }

    static class ExpandablePanel extends JPanel {
        private final JLabel comp;
        private final JPanel advanced;
        private final JButton expandButton;

        ExpandablePanel() {
            setBorder(BorderFactory.createLineBorder(Color.black));
            setLayout(new GridBagLayout());

            GridBagConstraints gbc = new GridBagConstraints();
            gbc.gridx = 0;
            gbc.gridy = 0;
            gbc.weightx = 1;
            gbc.weighty = 1;
            gbc.insets = new Insets(2, 2, 2, 2);
            gbc.fill = GridBagConstraints.BOTH;

            advanced = new JPanel();
            {
                advanced.setLayout(new BoxLayout(advanced, BoxLayout.Y_AXIS));
                advanced.setBorder(new TitledBorder("Advance Settings"));
                advanced.add(new JCheckBox("Live"));
                advanced.add(new JCheckBox("Condition"));
                advanced.add(new JCheckBox("Disable"));
            }
            advanced.setVisible(false);

            var standard = new JPanel();
            {
                standard.setLayout(new BoxLayout(standard, BoxLayout.X_AXIS));
                comp = new JLabel("Label 1");
                standard.add(comp);
                standard.add(new JButton("Button 1"));
                expandButton = new JButton("+");
                standard.add(expandButton);
            }
            add(standard, gbc);

            gbc.gridy++;
            gbc.weighty = 0;
            add(advanced, gbc);
        }

        public void setValue(Object value) {
            comp.setText(value.toString());
        }

        public void setExpanded(boolean expanded) {
            advanced.setVisible(expanded);
            expandButton.setText(expanded ? "-" : "+");
        }

        // 绑定展开按钮点击事件,同步更新行高和状态
        public void bindExpandAction(JTable table, int row) {
            expandButton.addActionListener(e -> {
                boolean currentExpanded = rowExpandedState.getOrDefault(row, false);
                boolean newExpanded = !currentExpanded;
                rowExpandedState.put(row, newExpanded);
                setExpanded(newExpanded);
                // 直接更新行高并触发重绘
                updateRowHeight(table, this, row, table.getColumnModel().getColumnIndex("control"));
                table.repaint();
            });
        }
    }

    private static class DynamicCellEditor extends AbstractCellEditor implements TableCellEditor {
        private final JTable table;
        private Object value;
        private final ExpandablePanel expandablePanel = new ExpandablePanel();
        private int currentRow;

        public DynamicCellEditor(JTable table) {
            this.table = table;
        }

        @Override
        public Object getCellEditorValue() {
            return value;
        }

        @Override
        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
            this.value = value;
            this.currentRow = row;
            boolean isExpanded = rowExpandedState.getOrDefault(row, false);
            expandablePanel.setValue(value);
            expandablePanel.setExpanded(isExpanded);
            // 绑定当前行的展开按钮事件
            expandablePanel.bindExpandAction(table, row);
            return updateRowHeight(table, expandablePanel, row, column);
        }

        @Override
        public boolean stopCellEditing() {
            // 编辑结束后,保存当前行的展开状态
            rowExpandedState.put(currentRow, advancedPanelVisible());
            return super.stopCellEditing();
        }

        private boolean advancedPanelVisible() {
            return expandablePanel.advanced.isVisible();
        }
    }

    public static void main(String[] args) {
        var contentPane = new JPanel(new BorderLayout());
        contentPane.add(new JScrollPane(makeTable()));

        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("DynamicCellRow");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(contentPane);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

关键细节说明

  • 状态持久化:用rowExpandedState Map暂存每行展开状态,实际项目建议将该属性整合到表格数据模型的实体类中,更符合MVC设计模式
  • 行高更新逻辑:直接在按钮点击事件中计算组件高度并设置行高,彻底避免组件监听器引发的循环重绘问题;折叠时直接恢复预设的默认行高
  • 编辑器激活优化:通过配置表格的选择属性,实现点击单元格直接进入编辑状态,解决原方案中两次点击激活的问题
  • 状态同步保障:渲染器和编辑器从同一状态源读取展开状态,确保单元格在渲染和编辑模式下显示一致

内容的提问来源于stack exchange,提问作者bric3

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 01:30:23