Восстановление развернутых / свернутых состояний узлов дерева

Я работал с сохранением состояния дерева (расширенные / выбранные узлы) и создал служебный класс, который может сохранять и восстанавливать состояния узлов. Работает нормально.

Но все же есть одна проблема с самим JTree - когда пользователь работает с некоторым экземпляром JTree (разворачивающиеся / сворачивающиеся узлы), может возникнуть ситуация, когда какой-то узел (скрытый под другим свернутым узлом) раскрывается. Ничего особенного в этом нет - это просто прекрасно.

JTree хранит записи о развернутых / свернутых узлах в отдельном expandedState Хеш-таблица с использованием пути к узлу в качестве ключа и логического значения в качестве расширенного значения состояния. Поэтому, когда этот развернутый узел под свернутым родительским узлом станет видимым, он все равно будет раскрыт, поскольку для него есть запись в expandedState Hashtable с true значение.

Ситуация объяснена на скриншотах...
1. Разверните root и разверните какой-нибудь узел (папка "glassfish4") под root:

2. Свернуть корень:

3. Снова разверните root, и мы все еще увидим расширенный дочерний узел (папка "glassfish4"):

Представьте, что я сохранил состояние дерева в момент скриншота #2, когда рут свернут - проблема в том, что если я хочу восстановить все состояния узлов дерева (даже для скрытых), я не могу развернуть узел под другим свернутым узлом, потому что это заставит все родительские узлы расширяться. Также я не могу получить доступ expandedState Hashtable для изменения расширенных состояний непосредственно внутри него, так как он объявлен как закрытый в JTree, и нет хороших способов получить к нему доступ. Поэтому я не могу полностью воспроизвести начальное состояние дерева.

Так что я могу сделать это:

  1. Принудительно получить доступ к этому Hashtable через отражение - действительно плохая идея
  2. Переписать узлы JTree расширяют логику - это тоже плохая идея
  3. Сначала восстановите все развернутые состояния, а затем восстановите все свернутые состояния - это заставит дерево делать дополнительные бессмысленные перерисовки и много дополнительного рендеринга, так что это действительно плохой обходной путь, который я не хочу использовать

Может я что-то упускаю?

Так что в основном вопрос заключается в следующем:
Есть ли другой способ расширить дочерние узлы, не вызывая расширение родительских узлов?

Ниже вы можете найти несколько классов, которые я использую для сохранения / восстановления состояния дерева.

Просто позвони TreeUtils.getTreeState(tree) чтобы получить состояние JTree и TreeUtils.setTreeState(tree,treeState) восстановить состояние JTree. Обратите внимание, что дерево должно использовать UniqueNode, в противном случае эти методы будут вызывать ClassCastException - вы можете просто заменить DefaultMutableTreeNode на UniqueNode, если у вас есть собственные узлы, расширяющие DefaultMutableTreeNode.

UniqueNode.java - простой узел с собственным уникальным идентификатором

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

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 ответ

Решение

Почему бы не восстановить состояние, выполнив те же действия, как описано, сначала установить расширенные подузлы, а затем при необходимости свернуть их родительский узел?

Единственное отличие от вашего текущего кода - использовать две итерации вместо одной. Сначала выполните итерацию и разверните, где необходимо, затем выполните итерацию и сверните, где необходимо.

В любом случае дерево должно рисовать один раз из-за логики перекраски.

Другие вопросы по тегам