JTable可扩展单元格编辑器:行高动态调整问题求助
解决方案:JTable可展开折叠单元格的行高动态调整
核心问题梳理
- 点击展开/折叠按钮时,需同步更新对应行高度,折叠时恢复默认行高
- 原方案用
ComponentListener.componentResized导致表格持续重绘,preferredSize监听器无法保证表格可靠重绘 - 需要实现切换单元格时保留展开状态(将展开状态绑定到数据模型)
关键修改方向
- 展开状态与数据模型绑定:用Map存储每行的展开状态(实际项目可直接封装到表格数据对象中),确保切换单元格或编辑结束后状态不丢失
- 直接在按钮点击事件中更新行高:避开易引发循环重绘的组件监听器,点击按钮时直接计算组件高度并更新表格行高
- 修复编辑器两次点击激活问题:调整表格选择设置,让单元格点击一次即可进入编辑状态
- 统一渲染与编辑的状态同步:渲染器和编辑器从同一状态源获取展开状态,保证显示一致性
修改后的完整代码
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); }); } }
关键细节说明
- 状态持久化:用
rowExpandedStateMap暂存每行展开状态,实际项目建议将该属性整合到表格数据模型的实体类中,更符合MVC设计模式 - 行高更新逻辑:直接在按钮点击事件中计算组件高度并设置行高,彻底避免组件监听器引发的循环重绘问题;折叠时直接恢复预设的默认行高
- 编辑器激活优化:通过配置表格的选择属性,实现点击单元格直接进入编辑状态,解决原方案中两次点击激活的问题
- 状态同步保障:渲染器和编辑器从同一状态源读取展开状态,确保单元格在渲染和编辑模式下显示一致
内容的提问来源于stack exchange,提问作者bric3
相关产品推荐
相关产品推荐

