如何调整 TreeCellEditor 周围的 JTree 行的大小

How do I resize the JTree row around the TreeCellEditor

我使用多行 JTextArea 来编辑我的 JTree 中的值。

通过一些技巧,我能够调整 JTextArea 的大小以适应其中的文本,但编辑器周围的 JTree nodes/rows 不会移开。 (SCCEE 附下方截图)

如何让 JTree "reflow" 编辑器组件周围的所有节点?

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;

public final class TextAreaEditorForJTree {

    public static final String INITIAL_TEXT = "Line 1\nLine 2\nLine 3";

    public static void main(String args[]) {

        JTree tree = createSimpleTree();

        addTextAreaEditor(tree);

        JScrollPane scrollPane = new JScrollPane(tree);

        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(scrollPane, BorderLayout.CENTER);
        frame.setSize(300, 300);
        frame.setVisible(true);
    }

    private static JTree createSimpleTree() {
        DefaultMutableTreeNode root = new DefaultMutableTreeNode(INITIAL_TEXT);

        for (int i = 0; i < 10; i++) {
            MutableTreeNode child = new DefaultMutableTreeNode(INITIAL_TEXT);
            root.add(child);
        }

        JTree tree = new JTree(root);
        tree.setEditable(true);
        tree.setShowsRootHandles(true);

        return tree;
    }

    private static void addTextAreaEditor(JTree tree) {
        TreeCellEditor editor = new TextAreaTableCellEditor();
        tree.setCellEditor(editor);
    }

    private static final class TextAreaTableCellEditor extends AbstractCellEditor implements TreeCellEditor {

        private final JPanel panel;
        private final JLabel label;
        private final JTextArea textArea;
        private DefaultMutableTreeNode currentNode;

        public TextAreaTableCellEditor() {

            label = new JLabel("Editor:");

            textArea = new JTextArea();
            textArea.setColumns(10);

            panel = new JPanel();
            BoxLayout boxLayout = new BoxLayout(panel, BoxLayout.X_AXIS);
            panel.setLayout(boxLayout);
            panel.add(label);
            panel.add(textArea);

            textArea.addComponentListener(new ComponentListener() {

                @Override
                public void componentResized(ComponentEvent e) {
                    setSizeToPreferredSizeLater();
                }

                @Override
                public void componentShown(ComponentEvent e) {
                    setSizeToPreferredSizeLater();
                }

                @Override
                public void componentMoved(ComponentEvent e) {
                }

                @Override
                public void componentHidden(ComponentEvent e) {
                }
            });

            textArea.getDocument().addDocumentListener(new DocumentListener() {
                public void insertUpdate(DocumentEvent e) {
                    setSizeToPreferredSizeLater();
                }

                public void removeUpdate(DocumentEvent e) {
                    setSizeToPreferredSizeLater();
                }

                public void changedUpdate(DocumentEvent e) {
                    setSizeToPreferredSizeLater();
                }
            });
        }

        private void setSizeToPreferredSizeLater() {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    panel.setSize(panel.getPreferredSize());
                }
            });

        }

        public Object getCellEditorValue() {
            return textArea.getText();
        }

        public boolean isCellEditable(EventObject anEvent) {
            return true;
        }

        public boolean shouldSelectCell(EventObject anEvent) {
            return true;
        }

        public boolean stopCellEditing() {
            currentNode.setUserObject(textArea.getText());
            return true;
        }

        public void cancelCellEditing() {
            currentNode.setUserObject(textArea.getText());
        }

        public Component getTreeCellEditorComponent(final JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row) {

            this.currentNode = ((DefaultMutableTreeNode) value);

            textArea.setText((String) currentNode.getUserObject());

            return panel;
        }
    }
}

SCCEE 的屏幕截图 - 编辑器(灰色框和右侧的文本)出现在所有其他树节点的顶部。

我找到了我的问题的解决方案,它与 the answer to a related question. If you're just dealing with renderers, then you may be better served by reviewing the other question and answer 中的实现非常相似,但在我的例子中,我正在处理一个在我键入时调整大小的编辑器。

两种情况下的关键都是调用AbstractLayoutCache.invalidateSizes()。此缓存在 BasicTreeUI 的受保护成员中,您可以从 JTree.getUI() 获取(此 假设 您的树的 L&F 扩展 BasicTreeUI

您应该注意,虽然该方法被调用 "invalidateSizes",但它确实使所有节点 边界 无效。节点 bounds 还包括组件的位置。我真的很想扩展 UI 对象或插入 forwarding/proxy 对象,但我无法在分配的时间内找到解决方案。

在 size/bounds 无效后,调用 tree.repaint() 将更新 UI 以显示新编辑器的大小。

好的,所以我找到了两种调用此方法的方法,我也不喜欢...但它们确实有效:

private static class MyJTree extends JTree {
    ...
    public void invalidateNodeBoundsViaSideEffect() {
        if (ui instanceof BasicTreeUI) {
            BasicTreeUI basicTreeUI = (BasicTreeUI) ui;
            basicTreeUI.setLeftChildIndent(basicTreeUI.getLeftChildIndent());
        }}

    public void invalidateNodeBoundsViaRefection() {
        if (ui instanceof BasicTreeUI) {
            try {
                Field field = BasicTreeUI.class.getDeclaredField("treeState");
                field.setAccessible(true);

                AbstractLayoutCache treeState = (AbstractLayoutCache) field.get(ui);

                if (treeState != null) {
                    treeState.invalidateSizes();
                }
            } catch (Exception e) {
            }
        }}}

包含此解决方案的修订后的 SCCEE:

import java.awt.*;
import java.awt.event.*;
import java.lang.reflect.Field;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.basic.*;
import javax.swing.tree.*;

public final class TextAreaEditorForJTree2 {

    public static final String INITIAL_TEXT = "Line 1\nLine 2\nLine 3";

    public static void main(String args[]) {

        JTree tree = createSimpleTree();

        addTextAreaEditor(tree);

        JScrollPane scrollPane = new JScrollPane(tree);

        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(scrollPane, BorderLayout.CENTER);
        frame.setSize(300, 300);
        frame.setVisible(true);
    }

    private static JTree createSimpleTree() {

        DefaultMutableTreeNode root = new DefaultMutableTreeNode(INITIAL_TEXT);

        for (int i = 0; i < 10; i++) {
            MutableTreeNode child = new DefaultMutableTreeNode(INITIAL_TEXT);
            root.add(child);
        }

        JTree tree = new MyJTree(root);
        tree.setRowHeight(0);  // CRITICAL - Setting to '0' means the row heights are variable and the renderer's **bounds** should be recomputed more often!
        tree.setEditable(true);
        tree.setShowsRootHandles(true);

        return tree;
    }

    private static void addTextAreaEditor(JTree tree) {
        TreeCellEditor editor = new TextAreaTableCellEditor(tree);
        tree.setCellEditor(editor);
    }

    private static final class TextAreaTableCellEditor extends AbstractCellEditor implements TreeCellEditor {

        private final JPanel editorPanel;
        private final JLabel editorLabel;
        private final JTextArea textArea;
        private DefaultMutableTreeNode currentNode;
        private final JTree tree;

        public TextAreaTableCellEditor(final JTree target) {
            this.tree = target;

            editorLabel = new JLabel("Editor:");

            textArea = new JTextArea();
            textArea.setColumns(10);
            textArea.setLineWrap(true);
            textArea.setWrapStyleWord(true);

            editorPanel = new JPanel();
            BoxLayout boxLayout = new BoxLayout(editorPanel, BoxLayout.X_AXIS);
            editorPanel.setLayout(boxLayout);
            editorPanel.add(editorLabel);
            editorPanel.add(textArea);

            editorPanel.setSize(editorPanel.getPreferredSize());

            textArea.addComponentListener(new ComponentListener() {
                public void componentResized(ComponentEvent e) {somethingChanged();}
                public void componentShown(ComponentEvent e) {somethingChanged();}
                public void componentMoved(ComponentEvent e) {}
                public void componentHidden(ComponentEvent e) {}
            });

            textArea.getDocument().addDocumentListener(new DocumentListener() {
                public void insertUpdate(DocumentEvent e) {somethingChanged();}
                public void removeUpdate(DocumentEvent e) {somethingChanged();}
                public void changedUpdate(DocumentEvent e) {somethingChanged();}
            });
        }

        private void somethingChanged() {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    // TODO: skip if size is not changing
                    editorPanel.setSize(editorPanel.getPreferredSize());
                    ((MyJTree) tree).invalidateNodeBounds();
                    tree.repaint();
                }
            });
        }

        public Object getCellEditorValue() {
            return textArea.getText();
        }

        public boolean isCellEditable(EventObject anEvent) {
            return true;
        }

        public boolean shouldSelectCell(EventObject anEvent) {
            return true;
        }

        public boolean stopCellEditing() {
            currentNode.setUserObject(textArea.getText());
            return true;
        }

        public void cancelCellEditing() {
            currentNode.setUserObject(textArea.getText());
        }

        public Component getTreeCellEditorComponent(final JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row) {
            this.currentNode = ((DefaultMutableTreeNode) value);
            textArea.setText((String) currentNode.getUserObject());
            return editorPanel;
        }
    }

    private static class MyJTree extends JTree {

        public MyJTree(TreeNode root) {
            super(root);
        }

        public void invalidateNodeBounds() {
            invalidateNodeBoundsViaSideEffect();
            //invalidateNodeBoundsViaRefection();
        }

        public void invalidateNodeBoundsViaSideEffect() {
            if (ui instanceof BasicTreeUI) {
                BasicTreeUI basicTreeUI = (BasicTreeUI) ui;
                basicTreeUI.setLeftChildIndent(basicTreeUI.getLeftChildIndent());
            }
        }

        public void invalidateNodeBoundsViaRefection() {

            if (ui instanceof BasicTreeUI) {

                try {
                    Field field = BasicTreeUI.class.getDeclaredField("treeState");
                    field.setAccessible(true);

                    AbstractLayoutCache treeState = (AbstractLayoutCache) field.get(ui);

                    if (treeState != null) {
                        treeState.invalidateSizes();
                    }
                } catch (Exception e) {
                }
            }
        }
    }
}