Восстановление состояния расширения / выбора в JTree с отложенной загрузкой

У меня есть приложение, которое содержит JTree с поддержкой DefaultTreeModel, которое используется для отображения иерархии файлов, которая отражает раздел файловой системы на моем сервере (я буду называть это моим клиентским приложением). У меня также есть серверное приложение, которое предоставляет данные, которые должны отображаться моим клиентским приложением (я буду называть это моим серверным приложением). Я использую подход "лениво загружать детей", так что мне нужно загружать файлы в мое дерево, только если пользователь заинтересован в них. Подход с отложенной загрузкой:

  1. Я переопределяю treeWillExpand(TreeExpansionEvent evt)
  2. Я установил путь выбора для расширяющегося узла.
  3. Затем я отправляю сообщение на сервер с просьбой о дочерних узлах этого узла.
  4. Когда сервер отвечает, я получаю последний выбранный компонент пути.
  5. Тогда я использую DefaultTreeModel.insertNodeInto() для каждого возвращенного файла данных.
  6. Наконец звоню DefaultTreeModel.nodeStructureChanged(),

Вышеописанное работает нормально, и у меня нет проблем с ленивой загрузкой детей. Моя проблема возникает, когда новые данные загружаются на сервер, и я хочу обновить дерево, чтобы оно не только включало новые данные, но и устанавливало состояние расширения и выбранный узел на то, что было до обновления дерева (так, чтобы пользователь не дергается на дереве только потому, что есть новые данные для просмотра). Поток выглядит следующим образом:

  1. Новые данные загружаются на сервер
  2. Серверное приложение архивирует эти данные и заполняет базу данных информацией о загруженных файлах.
  3. Серверное приложение уведомляет клиентское приложение о загрузке новых данных.
  4. Клиентское приложение сохраняет состояние расширения дерева, используя JTree.getExpandedDescendants()
  5. Клиентское приложение сохраняет путь выбора дерева, используя JTree.getSelectionPath()
  6. Клиентское приложение удаляет все узлы из DefaultTreeModel.
  7. Клиентское приложение запрашивает данные с сервера, начиная с корневого узла.
  8. Клиентское приложение перебирает перечисление пути к дереву, возвращенное из JTree.getExpandedDescendants() призвание JTree.expandPath() на каждом TreePath в перечислении.
  9. Клиентское приложение устанавливает выбранный путь к дереву.

Моя проблема в том, что независимо от того, что я пробую, графический интерфейс дерева не обновляется, чтобы отражать состояние расширения. Я знаю, что мой вызов expandPath работает, потому что я вижу запрос данных, отправленных клиентом, и ответ с данными с сервера для каждого вызова expandPath. Я также отображаю информацию о текущем выбранном узле в другом окне, и он показывает правильно выбранный узел. Но, к сожалению, к моему разочарованию, графический интерфейс отображает только корневой узел (расширенный) и его дочерние элементы (не развернутый) вместо предыдущего расширенного состояния.

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

Вот несколько вещей, которые я пробовал:

  • Я нашел поток с настройкой, аналогичной моей, и его проблема была решена путем переопределения equals() а также hashCode() но это не сработало для меня.
  • Различные методы для вызова расширения, такие как: setExpandsSelectedPaths(true), nodeStructureChanged(), JTree.invalidate()
  • Существует множество различных вариантов сохранения состояния расширения, однако я не считаю, что состояние расширения является неправильным, поскольку вижу правильные данные, передаваемые назад и вперед между клиентским приложением и приложением сервера.

Вот мой SSCCE:

package tree.sscce;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.JButton;
import java.util.Enumeration;
import javax.swing.BoxLayout;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreePath;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import javax.swing.JTextPane;

public class TreeSSCCE extends JFrame implements TreeWillExpandListener {

private static final long serialVersionUID = -1930472429779070045L;

public static void main(String[] args) 
{
    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            TreeSSCCE inst = new TreeSSCCE();
            inst.setLocationRelativeTo(null);
            inst.setVisible(true);
            inst.setDefaultCloseOperation(EXIT_ON_CLOSE);
        }           
    });
}

private DefaultMutableTreeNode rootNode;
private JTree tree;
private DefaultTreeModel treeModel;
private TreePath selectionPathPriorToNewData;
private Enumeration<TreePath> expandedPathsPriorToNewData;
private int treeSize = 5;

public TreeSSCCE() {

    this.setBounds(0, 0, 500, 400);
    JPanel mainPanel = new JPanel();
    getContentPane().add(mainPanel, BorderLayout.CENTER);
    mainPanel.setBounds(0, 0, 500, 400);
    mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));

    JPanel descriptionPanel = new JPanel();
    descriptionPanel.setBounds(0, 0, 500, 200);
    mainPanel.add(descriptionPanel);

    JTextPane textPane = new JTextPane();
    String newLine = System.getProperty("line.separator");
    descriptionPanel.setLayout(new BorderLayout(0, 0));
    textPane.setText("Start by expanding some nodes then click 'Add New Data' and you will notice that the tree state is not retained.");
    descriptionPanel.add(textPane);

    // Initialize The Tree
    tree = new JTree();
    rootNode = new DefaultMutableTreeNode("Root");
    treeModel = new DefaultTreeModel(rootNode);
    tree.addTreeWillExpandListener(this);
    tree.setModel(treeModel);
    tree.setShowsRootHandles(true);
    populateTree(false);

    JScrollPane scrollPane = new JScrollPane(tree);
    mainPanel.add(scrollPane);

    JPanel buttonPanel = new JPanel();
    mainPanel.add(buttonPanel);

    JButton btnAddNewData = new JButton("Add New Data");
    btnAddNewData.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent arg0) {
            addNewDataToTree();
        }
    });
    buttonPanel.add(btnAddNewData);


}

private void removeAllTreeNodes()
{

    while(!treeModel.isLeaf(treeModel.getRoot()))
    {
        treeModel.removeNodeFromParent((MutableTreeNode)treeModel.getChild(treeModel.getRoot(),0));
    }
    treeModel = null;
    treeModel = new DefaultTreeModel(rootNode);
    tree.setModel(treeModel);
}

public void restoreExpansionState(Enumeration enumeration)
{
    if (enumeration != null) 
    {
        while (enumeration.hasMoreElements()) 
        {
            TreePath treePath = (TreePath) enumeration.nextElement();
            tree.expandPath(treePath);
            tree.setSelectionPath(treePath);
        }
        tree.setSelectionPath(selectionPathPriorToNewData);
    }
}

protected void addNewDataToTree() 
{
    // save the tree state
    selectionPathPriorToNewData = tree.getSelectionPath();
    expandedPathsPriorToNewData = tree.getExpandedDescendants(new TreePath(tree.getModel().getRoot()));
    removeAllTreeNodes();
    populateTree(true);
    restoreExpansionState(expandedPathsPriorToNewData);
}

private void populateTree(boolean newData)
{
    if(newData)
        treeSize++;
    MyParentNode[] parents = new MyParentNode[treeSize];
    for(int i = 0; i < treeSize; i++)
    {
        parents[i] = new MyParentNode("Parent [" + i + "]");
        treeModel.insertNodeInto(parents[i], rootNode, i);
    }
}

@Override
public void treeWillCollapse(TreeExpansionEvent evt) throws ExpandVetoException {
    // Not used.
}

@Override
public void treeWillExpand(TreeExpansionEvent evt) throws ExpandVetoException 
{
    System.out.println("Tree expanding: " + evt.getPath());
    tree.setExpandsSelectedPaths(true);
    tree.setSelectionPath(evt.getPath());
    // we have already loaded the top-level items below root when we
    // connected so lets just return...
    if(evt.getPath().getLastPathComponent().equals(treeModel.getRoot()))
        return;

    // if this is not root lets figure out what we need to do.
    DefaultMutableTreeNode expandingNode = (DefaultMutableTreeNode) evt.getPath().getLastPathComponent();

    // if this node already has children then we don't want to reload so lets return;
    if(expandingNode.getChildCount() > 0)
        return;     
    // if this node has no children then lets add some
    MyParentNode mpn = new MyParentNode("Parent Under " + expandingNode.toString());
    treeModel.insertNodeInto(mpn, expandingNode, expandingNode.getChildCount());
    for(int i = 0; i < 3; i++)
    {
        treeModel.insertNodeInto(new DefaultMutableTreeNode("Node [" + i + "]"), mpn, i);
    }
}

private class MyParentNode extends DefaultMutableTreeNode
{
    private static final long serialVersionUID = 433317389888990065L;
    private String name = "";

    public MyParentNode(String _name)
    {
        super(_name);
        name = _name;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + getOuterType().hashCode();
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        MyParentNode other = (MyParentNode) obj;
        if (!getOuterType().equals(other.getOuterType()))
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }

    @Override
    public boolean isLeaf()
    {
        return false;
    }

    private TreeSSCCE getOuterType() {
        return TreeSSCCE.this;
    }       
}

}

Заранее благодарим за любую помощь, которую вы можете предоставить.

PS Это мой первый вопрос, поэтому, пожалуйста, дайте мне знать, если я правильно спрашиваю (и не беспокойтесь обо мне;)).

1 ответ

Я использую пользовательскую модель дерева (расширяет DefaultTreeModel) и реагирую на событие DBTreeEvent.STRUCTURE_CHANGED, чтобы справиться с этим. Это то, что я делаю, чтобы сохранить старое государство. Не уверен, поможет ли это вам или нет..

//NOTE: node is the tree node that caused the tree event
TreePath nodesPath = new TreePath(node.getPath());
TreePath currentSel = myTree.getLeadSelectionPath();
List<TreePath> currOpen  = getCurrExpandedPaths(nodesPath);
super.nodeStructureChanged(node);
reExpandPaths(currOpen);
myTree.setSelectionPath(currentSel);

private List<TreePath> getCurrExpandedPaths(TreePath currPath)
{
    List<TreePath> paths = new ArrayList<TreePath>();
    Enumeration<TreePath> expandEnum = myTree.getExpandedDescendants(currPath);
    if (expandEnum == null)
        return null;

    while (expandEnum.hasMoreElements())
        paths.add(expandEnum.nextElement());

    return paths;
}

private void reExpandPaths(List<TreePath> expPaths)
{
    if(expPaths == null)
        return;
    for(TreePath tp : expPaths)
        myTree.expandPath(tp);
}
Другие вопросы по тегам