【问题标题】:Restoring expanded/collapsed tree node states恢复展开/折叠的树节点状态
【发布时间】:2013-08-17 12:29:04
【问题描述】:

我正在处理树状态(扩展/选择的节点)保存并制作了一个可以保存和恢复节点状态的实用程序类。它工作正常。

但是 JTree 本身仍然存在一个问题 - 当用户使用一些 JTree 实例(展开/折叠节点)时,可能会出现一些节点(隐藏在另一个折叠节点下)被展开的情况。没什么特别的 - 很好。

JTree 将有关展开/折叠节点的记录保存在单独的expandedState Hashtable 中,使用节点路径作为键,布尔值作为展开状态值。因此,当折叠的父节点下的展开节点变得可见时,它仍然会展开,因为在 expandedState Hashtable 中有一条记录,其值为 true

屏幕截图说明的情况...
1.展开根目录,展开根目录下的一些节点(“glassfish4”文件夹):

2.折叠根:

3. 再次展开root,我们仍然看到子节点(“glassfish4”文件夹)展开:

想象一下,当 root 折叠时,我在屏幕截图 #2 时刻保存了树状态 - 问题是如果我想恢复所有树节点状态(即使是隐藏的),我无法在另一个折叠下展开节点节点,因为这将强制所有父节点展开。此外,我无法访问 expandedState Hashtable 以直接在其中更改扩展状态,因为它在 JTree 中被声明为私有并且没有访问它的好方法。所以我不能完全重现初始树状态。

所以我能做的是:

  1. 通过反射强制访问该 Hashtable - 真是个坏主意
  2. 重写 JTree 节点扩展逻辑 - 这也是个坏主意
  3. 首先恢复所有展开状态,然后恢复所有折叠状态 - 这将迫使树进行额外的无意义重绘和大量额外渲染,所以这是一个非常糟糕的解决方法,我不想使用

也许我错过了其他东西?

所以基本上问题是:
有没有其他方法可以扩展子节点而不导致父节点扩展?

您可以在下面找到一些我用来保存/恢复树状态的类。

只需调用TreeUtils.getTreeState(tree) 即可检索JTree 状态,调用TreeUtils.setTreeState(tree,treeState) 即可恢复JTree 状态。请注意,树必须使用 UniqueNode,否则这些方法将抛出 ClassCastException - 如果您有自己的扩展 DefaultMutableTreeNode 的节点,您可以简单地将 DefaultMutableTreeNode 替换为 UniqueNode。

UniqueNode.java - 具有自己唯一 ID 的简单节点

public class UniqueNode extends DefaultMutableTreeNode implements Serializable
{
    /**
     * Prefix for node ID.
     */
    private static final String ID_PREFIX = "UN";

    /**
     * Unique node ID.
     */
    protected String id;

    /**
     * Costructs a simple node.
     */
    public UniqueNode ()
    {
        super ();
        setId ();
    }

    /**
     * Costructs a node with a specified user object.
     *
     * @param userObject custom user object
     */
    public UniqueNode ( Object userObject )
    {
        super ( userObject );
        setId ();
    }

    /**
     * Returns node ID and creates it if it doesn't exist.
     *
     * @return node ID
     */
    public String getId ()
    {
        if ( id == null )
        {
            setId ();
        }
        return id;
    }

    /**
     * Changes node ID.
     *
     * @param id new node ID
     */
    public void setId ( String id )
    {
        this.id = id;
    }

    /**
     * Changes node ID to new random ID.
     */
    private void setId ()
    {
        this.id = TextUtils.generateId ( ID_PREFIX );
    }

    /**
     * {@inheritDoc}
     */
    public UniqueNode getParent ()
    {
        return ( UniqueNode ) super.getParent ();
    }

    /**
     * Returns TreePath for this node.
     *
     * @return TreePath for this node
     */
    public TreePath getTreePath ()
    {
        return new TreePath ( getPath () );
    }
}

TreeUtils.java - 将 TreeState 从 JTree 保存/加载到 JTree 的实用程序类

public class TreeUtils
{
    /**
     * Returns tree expansion and selection states.
     * Tree nodes must be instances of UniqueNode class.
     *
     * @param tree tree to process
     * @return tree expansion and selection states
     */
    public static TreeState getTreeState ( JTree tree )
    {
        return getTreeState ( tree, true );
    }

    /**
     * Returns tree expansion and selection states.
     * Tree nodes must be instances of UniqueNode class.
     *
     * @param tree          tree to process
     * @param saveSelection whether to save selection states or not
     * @return tree expansion and selection states
     */
    public static TreeState getTreeState ( JTree tree, boolean saveSelection )
    {
        TreeState treeState = new TreeState ();

        List<UniqueNode> elements = new ArrayList<UniqueNode> ();
        elements.add ( ( UniqueNode ) tree.getModel ().getRoot () );
        while ( elements.size () > 0 )
        {
            UniqueNode element = elements.get ( 0 );

            TreePath path = new TreePath ( element.getPath () );
            treeState.addState ( element.getId (), tree.isExpanded ( path ), saveSelection && tree.isPathSelected ( path ) );

            for ( int i = 0; i < element.getChildCount (); i++ )
            {
                elements.add ( ( UniqueNode ) element.getChildAt ( i ) );
            }

            elements.remove ( element );
        }

        return treeState;
    }

    /**
     * Restores tree expansion and selection states.
     * Tree nodes must be instances of UniqueNode class.
     *
     * @param tree      tree to process
     * @param treeState tree expansion and selection states
     */
    public static void setTreeState ( JTree tree, TreeState treeState )
    {
        setTreeState ( tree, treeState, true );
    }

    /**
     * Restores tree expansion and selection states.
     * Tree nodes must be instances of UniqueNode class.
     *
     * @param tree             tree to process
     * @param treeState        tree expansion and selection states
     * @param restoreSelection whether to restore selection states or not
     */
    public static void setTreeState ( JTree tree, TreeState treeState, boolean restoreSelection )
    {
        if ( treeState == null )
        {
            return;
        }

        tree.clearSelection ();

        List<UniqueNode> elements = new ArrayList<UniqueNode> ();
        elements.add ( ( UniqueNode ) tree.getModel ().getRoot () );
        while ( elements.size () > 0 )
        {
            UniqueNode element = elements.get ( 0 );
            TreePath path = new TreePath ( element.getPath () );

            // Restoring expansion states
            if ( treeState.isExpanded ( element.getId () ) )
            {
                tree.expandPath ( path );

                // We are going futher only into expanded nodes, otherwise this will expand even collapsed ones
                for ( int i = 0; i < element.getChildCount (); i++ )
                {
                    elements.add ( ( UniqueNode ) tree.getModel ().getChild ( element, i ) );
                }
            }
            else
            {
                tree.collapsePath ( path );
            }

            // Restoring selection states
            if ( restoreSelection )
            {
                if ( treeState.isSelected ( element.getId () ) )
                {
                    tree.addSelectionPath ( path );
                }
                else
                {
                    tree.removeSelectionPath ( path );
                }
            }

            elements.remove ( element );
        }
    }
}

TreeState.java - 保存节点状态的地图的容器类

public class TreeState implements Serializable
{
    /**
     * Tree node states.
     */
    protected Map<String, NodeState> states = new LinkedHashMap<String, NodeState> ();

    /**
     * Constructs new object instance with empty states.
     */
    public TreeState ()
    {
        super ();
    }

    /**
     * Constructs new object instance with specified states.
     *
     * @param states node states
     */
    public TreeState ( Map<String, NodeState> states )
    {
        super ();
        if ( states != null )
        {
            setStates ( states );
        }
    }

    /**
     * Returns all node states.
     *
     * @return all node states
     */
    public Map<String, NodeState> getStates ()
    {
        return states;
    }

    /**
     * Sets all node states.
     *
     * @param states all node states
     */
    public void setStates ( Map<String, NodeState> states )
    {
        this.states = states;
    }

    /**
     * Adds node state.
     *
     * @param nodeId   node ID
     * @param expanded expansion state
     * @param selected selection state
     */
    public void addState ( String nodeId, boolean expanded, boolean selected )
    {
        states.put ( nodeId, new NodeState ( expanded, selected ) );
    }

    /**
     * Returns whether node with the specified ID is expanded or not.
     *
     * @param nodeId node ID
     * @return true if node with the specified ID is expanded, false otherwise
     */
    public boolean isExpanded ( String nodeId )
    {
        final NodeState state = states.get ( nodeId );
        return state != null && state.isExpanded ();
    }

    /**
     * Returns whether node with the specified ID is selected or not.
     *
     * @param nodeId node ID
     * @return true if node with the specified ID is expanded, false otherwise
     */
    public boolean isSelected ( String nodeId )
    {
        final NodeState state = states.get ( nodeId );
        return state != null && state.isSelected ();
    }
}

NodeState.java - 单节点扩展/选择状态

public class NodeState implements Serializable
{
    /**
     * Whether node is expanded or not.
     */
    protected boolean expanded;

    /**
     * Whether node is selected or not.
     */
    protected boolean selected;

    /**
     * Constructs empty node state.
     */
    public NodeState ()
    {
        super ();
        this.expanded = false;
        this.selected = false;
    }

    /**
     * Constructs node state with the specified expansion and selection states.
     *
     * @param expanded expansion state
     * @param selected selection state
     */
    public NodeState ( boolean expanded, boolean selected )
    {
        super ();
        this.expanded = expanded;
        this.selected = selected;
    }

    /**
     * Returns whether node is expanded or not.
     *
     * @return true if node is expanded, false otherwise
     */
    public boolean isExpanded ()
    {
        return expanded;
    }

    /**
     * Sets whether node is expanded or not.
     *
     * @param expanded whether node is expanded or not
     */
    public void setExpanded ( boolean expanded )
    {
        this.expanded = expanded;
    }

    /**
     * Returns whether node is selected or not.
     *
     * @return true if node is selected, false otherwise
     */
    public boolean isSelected ()
    {
        return selected;
    }

    /**
     * Sets whether node is selected or not.
     *
     * @param selected whether node is selected or not
     */
    public void setSelected ( boolean selected )
    {
        this.selected = selected;
    }
}

顺便说一句,setTreeState 方法避免了此刻在折叠节点下恢复展开状态:

        // Restoring expansion states
        if ( treeState.isExpanded ( element.getId () ) )
        {
            tree.expandPath ( path );

            // We are going futher only into expanded nodes, otherwise this will expand even collapsed ones
            for ( int i = 0; i < element.getChildCount (); i++ )
            {
                elements.add ( ( UniqueNode ) tree.getModel ().getChild ( element, i ) );
            }
        }
        else
        {
            tree.collapsePath ( path );
        }

仅当父节点展开时才调用收集子节点的方法。所以折叠节点下的所有子节点都会被忽略。如果您更改该行为,您将看到我在此问题开头描述的问题 - 父节点将被扩展。

【问题讨论】:

  • +1 请使用 TreeExpansionListener 和 TreeWillExpandListener 捕获这些事件,从 TreeModelListener 跟踪(或 L&F 中的错误,JList、JComboBox、JSpinner 和 JTree 及其 Model_To_View 的标准问题)跨度>
  • 好问题 - 很难回答:公平地说,即使是 JTree 也只是扩展状态的奴隶,真正的控制器是 ui 委托创建和使用的 AbstractLayoutCache。所以我怀疑(从未尝试过)一个真正的解决方案将包含一个需要自定义 uis 的自定义 layoutCache ...
  • @mKorbel 它确实是一个选项来监听树扩展并在隐藏节点变得可见时展开它们,但这是一种解决方法 - 一个更好的解决方法,但仍然是一种解决方法。我想要的是立即恢复树状态,而不会留下任何恢复“工具”存在的痕迹(如惰性扩展侦听器)。
  • @kleopatra 是的,我刚刚注意到 BasicTreeUI 类中的 AbstractLayoutCache - 我想知道它是否保留了 JTree 保存的状态副本或......
  • 是的,它保留了一个包含所有已扩展节点的并行树模型

标签: java swing jtree expansion


【解决方案1】:

为什么不通过执行与描述相同的操作来恢复状态,首先将子节点设置为展开,然后根据需要将其父节点设置为折叠?

与您当前代码的唯一区别是使用两次迭代而不是一次。首先在需要的地方迭代和展开,然后在需要的地方迭代和折叠。

由于重绘逻辑,树无论如何都应该绘制一次。

【讨论】:

  • 我在问题帖子中说“为什么不呢?”:“首先恢复所有展开状态,然后恢复所有折叠状态 - 这将迫使树进行额外的无意义重绘和大量额外渲染,因此是我不想使用的非常糟糕的解决方法。”它可能(它会在一棵有很多扩展的大树上)重绘几次,这就是我不希望发生的事情。如果您要使用具有巨大树结构的大型应用程序,那真是一个糟糕的解决方法。
  • 正如我指出的,对渲染没有影响。 JTree 将调用 repaint,它只会安排一次绘制,但所有已调度的绘制都被强制转换为一个绘制操作。当 JTree 尝试计算受影响的区域时可能会有一些开销,这可以通过调用 RepaintManager.currentManager(tree).addDirtyRegion(tree, 0, 0, tree.getWidth(), tree.getHeight()) 将整个树标记为脏来避免,从而简化任何进一步的计算。
  • 只要我展开或折叠任何节点 - 树会安排重绘。重绘管理器确实会合并这些重绘,但前提是所有展开和折叠操作都在对 EDT 的 1 次调用中进行。在我的情况下,我有异步子加载和展开/折叠调用将在几个 invokeLater 调用中进行(一旦加载了所需的子节点),并且在这些调用之间会发生重绘。我希望这能说明问题——我只是不想通过添加不必要的异步节点子加载来使示例变得复杂,因为它不会影响这种情况的基本逻辑。
  • 所以你有一个异步树构建并多次恢复折叠/展开状态?如果这干扰了更改树的折叠/展开状态的用户操作,您会怎么做?据我了解,您必须在加载后立即更新某个子树的状态,因此如果您进行单次迭代或双次迭代,该子树没有区别。无论哪种情况,您发布的那段特定代码都会触发一次绘制操作。你安排的invokeLater()的数量不会改变,它只取决于异步操作。
  • 没想到。在这种特定情况下(扩展节点位于折叠节点下),将扩展加载到异步树中看起来并不好。我想我应该放弃该功能,因为它不是那么重要并且可能会产生很多垃圾代码...感谢您的讨论:)
猜你喜欢
  • 1970-01-01
  • 2011-05-10
  • 2011-05-29
  • 2013-07-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多