2013-08-15 3 views
5

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

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

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

Ситуация объясняется на скриншотах ...
1. Раскройте корень и расширить некоторый узел ("glassfish4" папку) под корнем:
enter image description here
2. Collapse корень:
enter image description here
3. Раскройте корень снова и мы все еще видим дочерний узел («glassfish4» папка) расширенный:
enter image description here

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

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

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

Может быть, я пропустил что-то еще?

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

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

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

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

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); 
     } 

Метод, который собирает дочерние узлы называют только если родительский узел расширен. Таким образом, все дочерние узлы под свернутыми узлами игнорируются. Если вы измените это поведение, вы увидите проблему, описанную в начале этого вопроса, - родительские узлы будут расширены.

+0

+1 пожалуйста можно поймать эти события, проследить из TreeModelListener, используя TreeExpansionListener и TreeWillExpandListener (или ошибка в вашем L & F, стандартные вопросы с JList, JComboBox, JSpinner и JTree и его Model_To_View) – mKorbel

+0

хороший вопрос - затрудняюсь ответить : да, даже JTree - это только ведомое состояние расширения, реальный контроллер - это AbstractLayoutCache, созданный и используемый делегатом ui. Поэтому я подозреваю (никогда не пробовал), что реальное решение будет envolve пользовательского layoutCache, который требует пользовательских uis ... – kleopatra

+0

@mKorbel его действительно возможность прослушать расширение дерева и расширить скрытые узлы, когда они станут видимыми, но это Обходной путь - лучший, но все же обходной путь. Я хочу восстановить состояние дерева сразу, не оставляя следов (например, ленивых слушателей расширения) восстановления «инструмента». –

ответ

1

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

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

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

+0

Я сказал в вопросительном столбце «почему бы и нет?»: «Сначала восстановите все расширенные состояния, а затем восстановите все свернутые состояния - это заставит дерево делать дополнительные бессмысленные рецензии и много дополнительного рендеринга, так что это очень плохое обходное решение, которое я не хочу использовать ». Это может (и это будет на большом дереве с большим количеством расширений) перерисовать несколько раз, и это то, чего я не хочу. Это очень плохо обходное решение, если вы собираетесь использовать это приложение большого размера с огромной древовидной структурой. –

+0

Как я уже сказал, никакого влияния на рендеринг не оказывает. JTree будет ссылаться на перерисовку, которая ничего не будет делать, кроме как запланировать краску, но все запланированные краски принуждаются к одной операции с краской. При попытке JTree рассчитать затронутые области могут быть некоторые накладные расходы, которые можно избежать, вызвав 'RepaintManager.currentManager (дерево) .addDirtyRegion (дерево, 0, 0, tree.getWidth(), tree.getHeight())' to отметьте все дерево грязным, что упростит дальнейшие вычисления. – Holger

+0

Как только я разворачиваю или сворачиваюсь, любое дерево узлов планирует перерисовать. Менеджер перерисовки действительно объединит эти рецензии, но только в том случае, если все действия по расширению и сворачиванию выполняются в течение 1 вызова EDT. В моем случае у меня есть асинхронные дочерние нагрузки, а вызовы expand/collapse будут выполняться в течение нескольких вызовов invokeLater (как только будут загружены требуемые дочерние узлы), и между этими вызовами произойдет перерисовка. Надеюсь, это упростит ситуацию - я просто не хотел, чтобы этот пример осложнялся добавлением ненужной загрузки дочерних асинхронных узлов, поскольку это не влияет на базовую логику для этого случая. –