JTree - home-Verzeichnis des Benutzers aufklappen

Ich habe Schwierigkeiten mit einem JTree, der ein Dateisystem abbildet. Ich hätte gerne, dass das darin befindliche home-Verzeichnis des Benutzers beim Start des Programms aufgeklappt dargestellt wird.

Mein Code zum Aufklappen:

    File homePath = FileSystemView.getFileSystemView().getHomeDirectory();
    DefaultMutableTreeNode homeDir = new DefaultMutableTreeNode(homePath);
    TreeNode[] path = homeDir.getPath();
    TreePath treePath = new TreePath(path);
    fileTree.expandPath(treePath);

Die Wurzel des JTree bleibt aber weiterhin geschlossen. Wie kann ich das Problem (anders) lösen?

Möglicherweise liegt es ja daran dass Root nicht sichtbar ist…

<JTree>.setRootVisible(true)

sollte dann helfen.

Ich hatte tatsächlich den root-node unsichtbar geschaltet. Aber das Sichtbarmachen half nichts.

Ich erzeuge tiefer liegende Verzeichnisse erst, wenn sie selektiert (aufgeklappt) wurden (lazy). Daher denke ich, dass deshalb keiner der Knoten bis zum Zielverzeichnis aufgeklappt werden kann…

Woraus besteht denn das eigentliche TreeModel? Ist das aus (Default)MutableTreeNode-Instanzen aufgebaut, oder ist das was “eigenes”?

Die entscheidenen Punkte hier sind wohl…

DefaultMutableTreeNode homeDir = new DefaultMutableTreeNode(homePath);
TreeNode[] path = homeDir.getPath();

Damit wird (wie man sich mit einem Syso auch ansehen kann) ein einzelner Knoten erstellt, und der Path enthält ein einzelnes Element. Der kennt ja den JTree und das TreeModel nicht. Und er erstellt auch nicht “automatisch” irgendeinen Pfad, der aus Knoten besteht, die bis C:\ hochgehen…

Außerdem… ich glaube (müßte das aber erst nochmal verifizieren) dass bei expandPath nur ein path übergeben werden kann, der echt zu diesem Baum gehört. Also, selbst wenn du händisch einen TreePath erstellen würdest, der den gewünschten Pfad beschreibt

[ "C:\", "C:\Files"; "C:\Files\User" ]

dann wären die Elemente darin ja neu erzeugte Knoten, und nicht identisch mit denen, die im Baum liegen.

(Für einen ähnlichen Fall hatte ich mal https://github.com/javagl/CommonUI/blob/master/src/main/java/de/javagl/common/ui/JTrees.java#L374 erstellt: Wenn man einen TreePath hat, und “den gleichen” TreePath in einem anderen tree ausklappen will, muss man (offenbar, und falls ich damals nicht ein Brett vorm Kopf hatte) den Pfad “konvertieren”, damit er aus Knoten-Instanzen besteht, die identisch zu denen im anderen Baum sind …)

Vermutlich gibt’s eine recht einfache Lösung, aber dazu müßte man das TreeModel genauer kennen.

(Ich hatte zum Testen eben schnell gewebsucht, und dabei http://www.java2s.com/Tutorial/Java/0240__Swing/JTreeDisplayingtheFileSystemHierarchyUsingtheCustomTreeModel.htm gefunden, woran man sieht, dass so ein TreeModel ggf. nicht mal TreeNode-Instanzen enthalten muss. Das TreeModel an sich ist eine sehr abstrakte Beschreibung einer “Baumdatenstruktur”, wo alles mögliche dahinter stehen kann. An sich ist das cool, aber … an manchen Stellen kann das “unbequem” sein…)

Ich hatte von Anfang an Schwierigkeiten, den JTree zu verdauen, habe viel im Netz gelesen, einige Beispiele ausprobiert und bin bei diesem hängen geblieben. Das erschien mir durchschaubar:

private TreeModel createFileTreeModel() {
    DefaultMutableTreeNode root = new DefaultMutableTreeNode();

    File[] fileSystemRoots = FILE_SYSTEM_VIEW.getRoots();
    for (File rootPath : fileSystemRoots) {
        DefaultMutableTreeNode node = new DefaultMutableTreeNode(rootPath);
        root.add(node);
        File[] files = FILE_SYSTEM_VIEW.getFiles(rootPath, true);
        for (File file : files) {
            if (file.isDirectory()) {
                node.add(new DefaultMutableTreeNode(file));
            }
        }
    }
    return new DefaultTreeModel(root);
}

Das Model besteht also aus DefaultMutableTreeNodes.
Am JTree hängt dann noch ein TreeSelectionListener, der die Kindknoten besorgt und darstellt, wenn ein Konten ausgewählt wurde.

Hm. Falls ich das richtig verstanden habe, existiert der Teil des JTrees ja noch gar nicht, in dem Moment, wo es ausgeklappt werden soll. (Also, dieser Teil wird ja erst durch den SelectionListener erstellt …).

(Ich würde da eher einen TreeWillExpandListener verwenden, aber … vielleicht gibt es für den SelectionListener ja einen Grund…)

Zugegeben, mit dem FileSystemView hatte ich bisher noch nicht wirklich was gemacht. Ich war dann etwas irritiert, zu sehen, dass er bei mir „Desktop“ als Root zurückgegeben hat, aber auch als Home-Directory - das macht irgendwie keinen Sinn… :confused:

Wie auch immer, es gibt bei der genauen Umsetztung sicher einige Freiheitsgrade, aber ich hab’ mal was gebastelt, was „bei mir funktioniert“ :wink: - vielleicht ist doch der eine oder andere hilfreiche Schnipsel drin…

package bytewelt;

import java.awt.BorderLayout;
import java.io.File;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.filechooser.FileSystemView;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

public class FileTreeExpandTest
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(() -> createAndShowGui());
    }

    private static void createAndShowGui()
    {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        DefaultTreeModel fileTreeModel = createFileTreeModel();
        JTree tree = new JTree(fileTreeModel);
        JScrollPane scrollPane = new JScrollPane(tree);
        
        tree.addTreeWillExpandListener(new TreeWillExpandListener()
        {
            @Override
            public void treeWillExpand(TreeExpansionEvent event)
                throws ExpandVetoException
            {
                TreePath path = event.getPath();
                DefaultMutableTreeNode lastNode = 
                    (DefaultMutableTreeNode)path.getLastPathComponent();
                Object userObject = lastNode.getUserObject();
                File file = (File)userObject;
                List<File> subdirectories = getSubdirectories(file);
                System.out.println("treeWillExpand at " + lastNode
                    + ", validating children for " + subdirectories);
                validateChildren(lastNode, subdirectories);
            }
            @Override
            public void treeWillCollapse(TreeExpansionEvent event)
                throws ExpandVetoException
            {
                // Not used
            }
        });

        frame.getContentPane().setLayout(new BorderLayout());
        frame.getContentPane().add(scrollPane, BorderLayout.CENTER);
        
        JButton expandButton = new JButton("Expand Home");
        expandButton.addActionListener(e -> expandHome(tree));
        frame.getContentPane().add(expandButton, BorderLayout.SOUTH);
        
        frame.setSize(600, 600);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);    
    }

    private static void expandHome(JTree fileTree)
    {
        File homePath = new File("C:/Users/User/");
        List<File> pathObjects = createPathFromRoot(homePath);
        
        System.out.println("Expanding "+pathObjects);
        
        TreeModel treeModel = fileTree.getModel();
        DefaultTreeModel defaultTreeModel = (DefaultTreeModel) treeModel;
        TreePath treePath = validateTreePath(defaultTreeModel, pathObjects);
        
        fileTree.expandPath(treePath);
        fileTree.setSelectionPath(treePath);
    }
    
    
    
    // Create a list containing the Files from the root of the
    // file system, up to the given file
    private static List<File> createPathFromRoot(File file)
    {
        Deque<File> list = new LinkedList<File>();
        File current = file;
        while (current != null)
        {
            list.addFirst(current);
            current = FILE_SYSTEM_VIEW.getParentDirectory(current);
        }
        return new ArrayList<File>(list);
    }
    
    // Make sure that the given tree model contains a path with
    // the given user objects, creating the nodes if necessary,
    // and return this path
    private static TreePath validateTreePath(
        DefaultTreeModel treeModel, List<?> objects)
    {
        DefaultMutableTreeNode rootNode = 
            (DefaultMutableTreeNode)treeModel.getRoot();
        DefaultMutableTreeNode currentNode = rootNode;
        TreePath treePath = new TreePath(currentNode);
        for (int i=0; i<objects.size(); i++)
        {
            Object object = objects.get(i);
            DefaultMutableTreeNode nextNode = 
                validateChild(currentNode, object);
            currentNode = nextNode;
            treePath = treePath.pathByAddingChild(currentNode);
        }
        return treePath;
    }
    
    // Make sure that the given node has a child with the given
    // user object, creating it if necessary, and return the child
    private static DefaultMutableTreeNode validateChild(
        DefaultMutableTreeNode node, Object userObject)
    {
        int n = node.getChildCount();
        for (int i = 0; i < n; i++)
        {
            DefaultMutableTreeNode child = 
                (DefaultMutableTreeNode)node.getChildAt(i);
            Object childUserObject = child.getUserObject();
            if (Objects.equals(childUserObject, userObject))
            {
                return child;
            }
        }
        DefaultMutableTreeNode newChild = 
            new DefaultMutableTreeNode(userObject);
        node.add(newChild);
        return newChild;
    }
    
    
    
    private static FileSystemView FILE_SYSTEM_VIEW =
        FileSystemView.getFileSystemView();

    private static DefaultTreeModel createFileTreeModel()
    {
        DefaultMutableTreeNode root = new DefaultMutableTreeNode();

        File[] fileSystemRoots = FILE_SYSTEM_VIEW.getRoots();
        for (File rootPath : fileSystemRoots)
        {
            DefaultMutableTreeNode node = new DefaultMutableTreeNode(rootPath);
            root.add(node);
            List<File> subdirectories = getSubdirectories(rootPath);
            validateChildren(node, subdirectories);
        }
        return new DefaultTreeModel(root, true);
    }
    
    
    // If the given file is a directory, return a list of all subdirectories
    private static List<File> getSubdirectories(File file)
    {
        List<File> subdirectories = new ArrayList<File>();
        if (file.isDirectory())
        {
            File[] subDirs = FILE_SYSTEM_VIEW.getFiles(file, true);
            for (File subDir : subDirs)
            {
                if (subDir.isDirectory())
                {
                    subdirectories.add(subDir);
                }
            }
        }
        return subdirectories;
    }
    
    // Make sure that the given node has children 
    // with all the given user objects
    private static boolean validateChildren(
        DefaultMutableTreeNode node, Iterable<?> childUserObjects)
    {
        boolean changed = false;
        Set<Object> existing = getChildUserObjects(node);
        for (Object childUserObject : childUserObjects)
        {
            if (!existing.contains(childUserObject))
            {
                node.add(new DefaultMutableTreeNode(childUserObject));
                changed = true;
            }
        }
        return changed;
    }
    
    // Returns a set of the user objects of all children of the given node
    private static Set<Object> getChildUserObjects(Object nodeObject)
    {
        Set<Object> childUserObjects = new LinkedHashSet<Object>();
        if (!(nodeObject instanceof DefaultMutableTreeNode))
        {
            // Warning here
            return childUserObjects;
        }
        DefaultMutableTreeNode node = (DefaultMutableTreeNode)nodeObject;
        int n = node.getChildCount();
        for (int i = 0; i < n; i++)
        {
            DefaultMutableTreeNode child = 
                (DefaultMutableTreeNode)node.getChildAt(i);
            Object childUserObject = child.getUserObject();
            childUserObjects.add(childUserObject);
        }
        return childUserObjects;
    }
    
}
1 „Gefällt mir“

(Nebenbei: Dass man beim Hantieren mit JTrees so oft genötigt ist, auf DefaultMutableTreeNode (bzw. allgemein) zu casten, finde ich etwas schade. Das nervte mich auch überall in https://github.com/javagl/CommonUI/blob/master/src/main/java/de/javagl/common/ui/JTrees.java#L412 . Aber die Flexibilität, die durch das TreeModel erreicht wird (bzw. damit, dass es eben nicht diesen konkreten Typ annimmt) ist schon was feines…)

Danke für die Mühe, Zeit und Arbeit, die du investiert hast. Ich arbeite dein Beispiel mal durch, da steckt eine Menge Zeug drin, dass ich verstehen und nachvollziehen möchte. Ich denke, daraus kann ich Einiges lernen. :+1:t2:

Hier wäre noch der passende DefaultTreeCellRenderer für deine Testklasse:

static class FileTreeCellRenderer extends DefaultTreeCellRenderer {
    private FileSystemView fsv = FileSystemView.getFileSystemView();

    @Override
    public Component getTreeCellRendererComponent(
            JTree tree,
            Object value,
            boolean selected,
            boolean expanded,
            boolean leaf,
            int row,
            boolean hasFocus) {

        super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);

        DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
        File file = (File) node.getUserObject();

        setIcon(fsv.getSystemIcon(file));
        setText(fsv.getSystemDisplayName(file));

        return this;
    }
}

Da ich unter Linux arbeite habe ich deine Zeile 84 angepasst:

File homePath = new File(System.getProperty("user.home"));

Jetzt funktioniert das auch unter Linux ganz wunderbar!

Zum TreeSelectionListener: Ich habe mal gelesen, dass die Berechnung größerer Bäume viel Rechenzeit beanspruchen kann. Und der Baum, der dargestellt werden soll, ist einer von einem Server mit mehreren großen Platten.
Daher war ich auf der Suche nach einer entsprechenden Lösung und fand im Netz eine in dieser lazy Erstellung der tieferen Kindknoten bei Mausklick. Somit wird der Rechenaufwand minimiert.

„Mühe“ und „Zeug“ sagt erstmal nichts über das Ergebnis aus. Das ganze ist nicht wirklich „engineered“. Ich bin sicher, mit etwas Überlegung und Planung könnte man eine sauberere und bessere Lösung hinbekommen.

(Und, wie so oft, habe ich mir beim Schreiben gedacht: „Ach, das ist eigentlich ganz interessant, und das brauchen bestimmt viele in der einen oder anderen Form - ich muss mir mal die Zeit nehmen, das richtig zu machen, und als Lib auf GitHub zu stellen“. (Vielleicht als kleine, versteckte Unter-Funktionalität neben den JTrees-Utilities. Vielleicht auch standalone)).

Es gibt da schon einige interessante Fragen.


Das „lazy loading“ ist sehr wichtig. Erstens, weil Festplatten nunmal sehr groß und langsam (!) sind. Und zweitens, weil ich befürchte, dass, wenn man da diese (mir noch etwas unbekannte) FileSystemView verwendet, dort Zyklen vorkommen könnten. (Sowas wie „Desktop → My Computer → User → Desktop (!!!)“).


Der Fall, dass man gezielt einen Pfad ausklappen will, der (wegen der laziness) zu diesem Zeitpunkt noch gar nicht im Baum enthalten ist, ist auch interessant. Da könnte man überlegen: Angenommen, man hat diese Verzeichnisstruktur:

C:\
    Files\
        A\
            \A0
            \A1
            \A2
        B\
            \B0
            \B1
            \B2
        C\
            \C0
            \C1
            \C2

Anfangs sieht man nur das hier im JTree:

C:\
    Files\

Nun sagt man, das C:\Files\B\B1 ausgeklappt werden soll. Soll der Tree dann das hier anzeigen:

C:\
    Files\
        B\
            \B1

oder den kompletten Baum? Oder irgendein Mittelding wie

C:\
    Files\
        A\
        B\
            \B0
            \B1
            \B2
        C\

???
(Das Problem dabei ist, dass der JTree erstmal keine Metapher hat für "einen Baum, bei dem mehr Kindknoten vorhanden sind, als gerade sichtbar sind…)

Neuere Windows-Versionen versuchen da ja, „schlau“ zu sein: Wenn ein Verzeichnis „viele“ Unterverzeichnisse hat, zeigt der Explorer manchmal nur das an, durch das der gewählte Pfad führt. Die Geschwister muss man händisch aufklappen. (Das finde ich eigentlich nicht so schön, aber bei wirklich vielen Verzeichnissen vielleicht sinnvoll…)


BTW: Das File homePath = new File(System.getProperty("user.home")); ist im wesentlichen das, was die FileSystemView intern macht. Aber falls ich mich nicht täusche, hat das bei mir den Desktop zurückgegeben, und nicht das Benutzerverzeichnis. Aber ich muss mir die FileSystemView nochmal genauer ansehen…


Zum TreeSelectionListener: Ich habe mal gelesen, dass die Berechnung größerer Bäume viel Rechenzeit beanspruchen kann.

Ja, aus verschiedenen Gründen (langsame Platte etc). Man braucht ja nicht den kompletten Baum zu berechnen. Der Unterschied war wirklich nur bezogen auf die Frage: Wann müssen die Kindknoten erstellt werden? Und das muss eigentlich noch nicht passieren, wenn ein Knoten ausgewählt* wird (mit einem TreeSelectionListener), sondern erst, wenn der Knoten aufgeklappt wird (mit einem TreeWillExpandListener). Aber der Unterschied könnte nicht soooo wichtig sein.

Das alles ist schon viel besser, als das, was ich in mehreren Tagen zusammengestümpert habe. :wink::unamused:

Das, was ich hier bauen möchte, kommt in Teilen einem Fileexplorer schon nahe. In einer JTable, werden nach Auswahl (Selektion) eines Knotens im JTree die Inhalte des Knotens angezeigt. Per Doppelklick ist es nun (mit deiner Hilfe) möglich, im JTree in tiefere Verzeichnisse zu wechseln. Also alles so, wie ich mir das am Reißbrett vorgestellt habe.
Dank deiner Arbeit ist es nun auch recht einfach möglich, in der JTable Verzeichnisse anzuklicken, die im JTree geöffnet werden. :+1:t2:

Nur aus Neugier: Ist das eine TreeTable, oder getrennt?

Getrennt. Habe das in einer JSplitPane.