渲染时更改JTree行高resize的行为

我想只在节点是SELECTED时使用包含三个文本字段的自定义TreeCellRenderer,而在节点未选择时使用默认渲染器。 问题是虽然我为面板设置了合适的首选大小和最小大小,但JTree不会更新编辑的行高。 相反,当我使用相同的面板作为编辑器时,它被正确呈现。

有人可以解释为什么会这样吗?
是否有推荐的方法来实现类似于编辑的渲染大小调整行为?
JTree是否提供了直接设置它的方法,还是有必要扩展JTree或(更糟)L&F?

注意:在深入研究BasicTreeUI.startEditing(TreePath path, MouseEvent event)方法后,我注意到以下几行代码。 他们似乎负责编辑大小调整:

 if(editorSize.width != nodeBounds.width || editorSize.height != nodeBounds.height) { // Editor wants different width or height, invalidate // treeState and relayout. editorHasDifferentSize = true; treeState.invalidatePathBounds(path); updateSize(); // To make sure x/y are updated correctly, fetch // the bounds again. nodeBounds = getPathBounds(tree, path); } else editorHasDifferentSize = false; tree.add(editingComponent); editingComponent.setBounds(nodeBounds.x, nodeBounds.y, nodeBounds.width, nodeBounds.height); 

这是一个SSCCE,显示了不同的编辑和渲染行为。

  • 未选择节点时,将使用默认渲染器。
  • 通过在节点上单击一次,选择节点并使用面板渲染器。
  • 通过在节点上单击两次,开始编辑并使用面板编辑器。

如您所见,仅在编辑期间才能正确呈现面板。

 import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.MouseEvent; import java.util.EventObject; import javax.swing.AbstractCellEditor; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.TreeCellEditor; import javax.swing.tree.TreeCellRenderer; public class TestResizeTreeRowsFrame { public static void main(String[] args){ SwingUtilities.invokeLater(new Runnable(){ @Override public void run() { MyTreeNode root = createRoot(); TestFrame f = new TestFrame(root); f.setVisible(true); } }); } private static MyTreeNode createRoot(){ MyTreeNode root = new MyTreeNode("RootColumnName", "RootTableName", "Root"); MyTreeNode aNode = new MyTreeNode("AcolumnName", "AtableName", "A"); MyTreeNode bNode = new MyTreeNode("BcolumnName", "BtableName", "B"); MyTreeNode cNode = new MyTreeNode("CcolumnName", "CtableName", "C"); root.add(aNode); root.add(bNode); root.add(cNode); return root; } public static class MyTreeNode extends DefaultMutableTreeNode{ /** * */ private static final long serialVersionUID = 1L; String columnName, tableName, value; public MyTreeNode(String columnName, String tableName, String value){ this.columnName = columnName; this.tableName = tableName; this.value = value; } public String getColumnName() { return columnName; } public String getTableName() { return tableName; } public String getValue() { return value; } public String toString(){ return value; } } public static class TestFrame extends JFrame { /** * */ private static final long serialVersionUID = 1L; private JPanel contentPane; private JTree tree; /** * Create the frame. */ public TestFrame(MyTreeNode root) { this.setTitle("RECORD Frame"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setBounds(100, 100, 450, 300); contentPane = new JPanel(); contentPane.setBorder(new EmptyBorder(5, 5, 5, 5)); contentPane.setLayout(new BorderLayout(0, 0)); setContentPane(contentPane); JScrollPane scrollPane = new JScrollPane(); contentPane.add(scrollPane, BorderLayout.CENTER); tree = new JTree(root); scrollPane.setViewportView(tree); tree.setEditable(true); tree.setInvokesStopCellEditing(true); tree.setCellRenderer(new NodeRenderer()); tree.setCellEditor(new PanelRenderer()); } private static class Renderer_Panel extends JPanel{ /** * */ private static final long serialVersionUID = 1L; private JTextField propertyTextField; private JTextField prototypeTextField; private JTextField valueTextField; /** * Create the panel. */ public Renderer_Panel() { setPreferredSize(new Dimension(480, 97)); setMinimumSize(new Dimension(480, 97)); setLayout(new BorderLayout(0, 0)); JPanel panel = new JPanel(); panel.setMinimumSize(new Dimension(480, 97)); panel.setPreferredSize(new Dimension(480, 97)); add(panel, BorderLayout.CENTER); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); Component verticalGlue_1 = Box.createVerticalGlue(); panel.add(verticalGlue_1); JScrollPane scrollPane = new JScrollPane(); scrollPane.setBorder(null); scrollPane.setPreferredSize(new Dimension(20, 60)); JPanel nodePropertiesPanel = new JPanel(); nodePropertiesPanel.setBorder(new EmptyBorder(0, 3, 0, 0)); nodePropertiesPanel.setPreferredSize(new Dimension(200, 30)); nodePropertiesPanel.setMinimumSize(new Dimension(0, 0)); scrollPane.setViewportView(nodePropertiesPanel); GridBagLayout gbl_panel = new GridBagLayout(); gbl_panel.columnWidths = new int[]{0, 0, 0}; gbl_panel.rowHeights = new int[]{0, 0, 0, 0}; gbl_panel.columnWeights = new double[]{0.0, 1.0, Double.MIN_VALUE}; gbl_panel.rowWeights = new double[]{0.0, 0.0, 0.0, Double.MIN_VALUE}; nodePropertiesPanel.setLayout(gbl_panel); JLabel lblProperty = new JLabel("Column:"); GridBagConstraints gbc_lblProperty = new GridBagConstraints(); gbc_lblProperty.insets = new Insets(0, 0, 5, 5); gbc_lblProperty.anchor = GridBagConstraints.WEST; gbc_lblProperty.gridx = 0; gbc_lblProperty.gridy = 0; nodePropertiesPanel.add(lblProperty, gbc_lblProperty); propertyTextField = new JTextField(); GridBagConstraints gbc_propertyTextField = new GridBagConstraints(); gbc_propertyTextField.insets = new Insets(0, 0, 5, 0); gbc_propertyTextField.fill = GridBagConstraints.HORIZONTAL; gbc_propertyTextField.gridx = 1; gbc_propertyTextField.gridy = 0; nodePropertiesPanel.add(propertyTextField, gbc_propertyTextField); propertyTextField.setColumns(10); JLabel lblPrototype = new JLabel("Table:"); GridBagConstraints gbc_lblPrototype = new GridBagConstraints(); gbc_lblPrototype.anchor = GridBagConstraints.WEST; gbc_lblPrototype.insets = new Insets(0, 0, 5, 5); gbc_lblPrototype.gridx = 0; gbc_lblPrototype.gridy = 1; nodePropertiesPanel.add(lblPrototype, gbc_lblPrototype); prototypeTextField = new JTextField(); GridBagConstraints gbc_prototypeTextField = new GridBagConstraints(); gbc_prototypeTextField.insets = new Insets(0, 0, 5, 0); gbc_prototypeTextField.fill = GridBagConstraints.HORIZONTAL; gbc_prototypeTextField.gridx = 1; gbc_prototypeTextField.gridy = 1; nodePropertiesPanel.add(prototypeTextField, gbc_prototypeTextField); prototypeTextField.setColumns(10); JLabel lblNewLabel = new JLabel("Value:"); GridBagConstraints gbc_lblNewLabel = new GridBagConstraints(); gbc_lblNewLabel.anchor = GridBagConstraints.WEST; gbc_lblNewLabel.insets = new Insets(0, 0, 0, 5); gbc_lblNewLabel.gridx = 0; gbc_lblNewLabel.gridy = 2; nodePropertiesPanel.add(lblNewLabel, gbc_lblNewLabel); valueTextField = new JTextField(); GridBagConstraints gbc_valueTextField = new GridBagConstraints(); gbc_valueTextField.fill = GridBagConstraints.HORIZONTAL; gbc_valueTextField.gridx = 1; gbc_valueTextField.gridy = 2; nodePropertiesPanel.add(valueTextField, gbc_valueTextField); valueTextField.setColumns(10); panel.add(scrollPane); Component verticalGlue = Box.createVerticalGlue(); panel.add(verticalGlue); } public void setProperty(String property){ this.propertyTextField.setText(property); } public void setPrototype(String prototype){ this.prototypeTextField.setText(prototype); } public void setValue(String value){ this.valueTextField.setText(value); } @Override public Dimension getPreferredSize() { return new Dimension(480, 97); } @Override public Dimension getMinimumSize() { return new Dimension(480, 97); } } private class PanelRenderer extends AbstractCellEditor implements TreeCellEditor, TreeCellRenderer{ /** * */ private static final long serialVersionUID = 1L; Renderer_Panel component = new Renderer_Panel(); MyTreeNode value; @Override public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row) { MyTreeNode myNode = ((MyTreeNode)value); String nodeValue = null; String prototype = null; String property = null; nodeValue = myNode.getValue(); prototype = myNode.getTableName(); property = myNode.getColumnName(); component.setProperty(property); component.setPrototype(prototype); component.setValue(nodeValue); this.value = myNode; return component; } @Override public Object getCellEditorValue() { return this.value.getValue(); } @Override public boolean isCellEditable(EventObject anEvent) { if(anEvent instanceof MouseEvent){ MouseEvent mouseEvent = (MouseEvent)anEvent; if(mouseEvent.getClickCount() == 2){ return true; }else{ return false; } }else{ return false; } } @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { return getTreeCellEditorComponent(tree, value, selected, expanded, leaf, row); } } private class LabelNodeRenderer extends DefaultTreeCellRenderer { /** * */ private static final long serialVersionUID = 1L; @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); MyTreeNode myNode = ((MyTreeNode)value); this.setText(myNode.getValue()); return this; } } private class NodeRenderer implements TreeCellRenderer{ /** * */ private static final long serialVersionUID = 1L; private LabelNodeRenderer labelRenderer = new LabelNodeRenderer(); private PanelRenderer panelRenderer = new PanelRenderer(); @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { Component returnedComponent = null; if(selected){ returnedComponent = panelRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); }else{ returnedComponent = labelRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); } returnedComponent.setSize(returnedComponent.getPreferredSize()); return returnedComponent; } } } } 

另外,请原谅我,如果这不是一个合适的地方,但我抓住机会询问是否有一本推荐Swing最佳实践的好书?
Swing的架构师是否在某处提供了他们基于Swing设计的推荐解决方案?(我知道我问的太多了)
是否至少有一本食谱包含像在JTree TreeCellRenderer中发现的Kleopatra评论的建议, 提出了显示选择颜色的问题 :

 a) extending a component is dirty design b) mixing calls to super and this is calling for pain (fi the infamous color memory in the default table cell renderer) 

或解释设计决策,例如让CellEditorListener只监听editCanceled和editingStopped,而不是editStarted(如果我想调整JTable的单元格大小而不必覆盖JTable.editCellAt,这将是有用的)。

提前致谢!

一些事实:

  • BasicTreeUI保留节点大小的缓存
  • 没有公共API强制它重新validation该缓存
  • 假设节点大小要求完全取决于数据,而不取决于可视状态,即选择状态的改变不会触发任何内部更新
  • 旁白:在渲染器/编辑器中设置大小没有任何效果:无论你做什么,ui都会在它认为合适时改变它

总的来说,没有办法实现你的要求而不会变脏。 基本上,您必须听取选择更改 – 因为渲染器在选定内容与未选择内容中具有不同的大小要求 – 然后尽力使ui的内部缓存无效。 基本上有两种选择:

  • 使用reflection来访问ui的受保护方法
  • 假模型事件会导致内部重新计算缓存

下面是第一个片段(无法通过快速测试进行第二次工作,但依旧记得我做过了……)

 protected TreeSelectionListener createReflectiveSelectionListener() { TreeSelectionListener l = new TreeSelectionListener() { @Override public void valueChanged(TreeSelectionEvent e) { invalidateLayoutCache(); } protected void invalidateLayoutCache() { BasicTreeUI ui = (BasicTreeUI) tree.getUI(); try { Method method = BasicTreeUI.class.getDeclaredMethod("configureLayoutCache"); method.setAccessible(true); method.invoke(ui); } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e1) { e1.printStackTrace(); } } }; return l; } 

刚刚发现了第二个 – 与第一个类似的脏级别 – 选项:

 protected TreeSelectionListener createFakeDataEventSelectionListener() { TreeSelectionListener l = new TreeSelectionListener() { @Override public void valueChanged(final TreeSelectionEvent e) { fireDataChanged(e.getOldLeadSelectionPath()); fireDataChanged(e.getNewLeadSelectionPath()); } private void fireDataChanged(TreePath lead) { if (lead == null) return; DefaultTreeModel model = (DefaultTreeModel) tree.getModel(); TreeNode last = (TreeNode) lead.getLastPathComponent(); model.nodeChanged(last); } }; return l; }