簡體   English   中英

如何有效地充分擴展大型JTree

[英]How to fully expand a large JTree efficiently

有幾個相關的問題,與設置新TreeModel時自動擴展JTree通常擴展JTree有關,其中一些還旨在提高JTree中許多路徑的性能。

但是,所提出的解決方案似乎都沒有涵蓋可以視為“簡單”應用程序案例的解決方案:我有一棵大樹(即一棵非常深,非常寬或兩者兼有的樹),並且我想完全以編程方式進行擴展。

以下是顯示問題的MCVE :它創建具有100k節點的樹模型。 按下按鈕將觸發對expandAll的調用,該調用將嘗試使用從對相關問題的答案中得出的方法來擴展所有節點。

問題在於,擴展這10萬個節點大約需要13秒鍾 (在使用最新JVM的普通計算機上)。

import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.util.function.Consumer;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;


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

    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().setLayout(new GridLayout(1,0));

        f.getContentPane().add(createTestPanel(
            TreeExpansionPerformanceProblem::expandAll));

        f.setSize(800,600);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    private static JPanel createTestPanel(Consumer<JTree> expansionMethod)
    {
        JPanel panel = new JPanel(new BorderLayout());
        JTree tree = new JTree(createTestTreeModel());
        panel.add(new JScrollPane(tree), BorderLayout.CENTER);

        JPanel buttonPanel = new JPanel();
        JButton expandAllButton = new JButton("Expand all");
        expandAllButton.addActionListener( e -> 
        {
            System.out.println("Expanding...");
            long before = System.nanoTime();
            expansionMethod.accept(tree);
            long after = System.nanoTime();
            System.out.println("Expanding took "+(after-before)/1e6);

        });
        buttonPanel.add(expandAllButton);
        panel.add(buttonPanel, BorderLayout.SOUTH);
        return panel;
    }

    private static void expandAll(JTree tree)
    {
        int r = 0;
        while (r < tree.getRowCount())
        {
            tree.expandRow(r);
            r++;
        }
    }

    private static TreeModel createTestTreeModel() 
    {
        DefaultMutableTreeNode root = new DefaultMutableTreeNode("JTree");
        addNodes(root, 0, 6, 6, 10);
        return new DefaultTreeModel(root);
    }

    private static void addNodes(DefaultMutableTreeNode node, 
        int depth, int maxDepth, int count, int leafCount)
    {
        if (depth == maxDepth)
        {
            return;
        }
        for (int i=0; i<leafCount; i++)
        {
            DefaultMutableTreeNode leaf = 
                new DefaultMutableTreeNode("depth_"+depth+"_leaf_"+i);
            node.add(leaf);
        }
        if (depth < maxDepth - 1)
        {
            for (int i=0; i<count; i++)
            {
                DefaultMutableTreeNode child = 
                    new DefaultMutableTreeNode("depth_"+depth+"_node_"+i);
                node.add(child);
                addNodes(child, depth+1, maxDepth, count, leafCount);
            }
        }

    }
}

是否有任何選項可以使更多節點更有效地擴展?

完全擴展一棵大樹時會遇到各種瓶頸,並且有不同的方法來規避這些問題。

有趣的是,收集TreePath對象以進行擴展和遍歷樹通常不是最昂貴的部分。 根據在VisualVMJava Flight Recorder中運行的探查 ,在計算模型狀態( TreeModel )和視圖( JTree )之間的“映射”時,大部分時間都花在了上面。 這主要是指

  • 計算JTree的行高
  • 計算TreeCellRenderer標簽的邊界

並非所有這些計算都可以避免。 但是,通過以下方法可以大大加快樹的擴展速度

  • 使用JTree#setRowHeight設置固定的行高
  • 臨時禁用樹的TreeExpansionListeners

下面是一個MCVE ,它比較了問題的“簡單”方法(將具有10萬個節點的樹擴展需要13秒)和更快的方法(僅需1秒即可擴展同一棵樹)。

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.GridLayout;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.TreeExpansionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;


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

    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().setLayout(new GridLayout(1,0));

        f.getContentPane().add(createTestPanel(
            TreeExpansionPerformanceSolution::expandAll));

        f.getContentPane().add(createTestPanel(
            TreeExpansionPerformanceSolution::expandAllFaster));

        f.setSize(800,600);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    private static JPanel createTestPanel(Consumer<JTree> expansionMethod)
    {
        JPanel panel = new JPanel(new BorderLayout());
        JTree tree = new JTree(createTestTreeModel());
        panel.add(new JScrollPane(tree), BorderLayout.CENTER);

        JPanel buttonPanel = new JPanel();
        JButton expandAllButton = new JButton("Expand all");
        expandAllButton.addActionListener( e -> 
        {
            System.out.println("Expanding...");
            long before = System.nanoTime();
            expansionMethod.accept(tree);
            long after = System.nanoTime();
            System.out.println("Expanding took "+(after-before)/1e6);

        });
        buttonPanel.add(expandAllButton);
        panel.add(buttonPanel, BorderLayout.SOUTH);
        return panel;
    }

    private static void expandAll(JTree tree)
    {
        int r = 0;
        while (r < tree.getRowCount())
        {
            tree.expandRow(r);
            r++;
        }
    }

    private static void expandAllFaster(JTree tree)
    {
        // Determine a suitable row height for the tree, based on the 
        // size of the component that is used for rendering the root 
        TreeCellRenderer cellRenderer = tree.getCellRenderer();
        Component treeCellRendererComponent = 
            cellRenderer.getTreeCellRendererComponent(
                tree, tree.getModel().getRoot(), false, false, false, 1, false);
        int rowHeight = treeCellRendererComponent.getPreferredSize().height + 2;
        tree.setRowHeight(rowHeight);

        // Temporarily remove all listeners that would otherwise
        // be flooded with TreeExpansionEvents
        List<TreeExpansionListener> expansionListeners =
            Arrays.asList(tree.getTreeExpansionListeners());
        for (TreeExpansionListener expansionListener : expansionListeners)
        {
            tree.removeTreeExpansionListener(expansionListener);
        }

        // Recursively expand all nodes of the tree
        TreePath rootPath = new TreePath(tree.getModel().getRoot());
        expandAllRecursively(tree, rootPath);

        // Restore the listeners that the tree originally had
        for (TreeExpansionListener expansionListener : expansionListeners)
        {
            tree.addTreeExpansionListener(expansionListener);
        }

        // Trigger an update for the TreeExpansionListeners
        tree.collapsePath(rootPath);
        tree.expandPath(rootPath);
    }

    // Recursively expand the given path and its child paths in the given tree
    private static void expandAllRecursively(JTree tree, TreePath treePath)
    {
        TreeModel model = tree.getModel();
        Object lastPathComponent = treePath.getLastPathComponent();
        int childCount = model.getChildCount(lastPathComponent);
        if (childCount == 0)
        {
            return;
        }
        tree.expandPath(treePath);
        for (int i=0; i<childCount; i++)
        {
            Object child = model.getChild(lastPathComponent, i);
            int grandChildCount = model.getChildCount(child);
            if (grandChildCount > 0)
            {
                class LocalTreePath extends TreePath
                {
                    private static final long serialVersionUID = 0;
                    public LocalTreePath(
                        TreePath parent, Object lastPathComponent)
                    {
                        super(parent, lastPathComponent);
                    }
                }
                TreePath nextTreePath = new LocalTreePath(treePath, child);
                expandAllRecursively(tree, nextTreePath);
            }
        }
    }


    private static TreeModel createTestTreeModel() 
    {
        DefaultMutableTreeNode root = new DefaultMutableTreeNode("JTree");
        addNodes(root, 0, 6, 6, 10);
        return new DefaultTreeModel(root);
    }

    private static void addNodes(DefaultMutableTreeNode node, 
        int depth, int maxDepth, int count, int leafCount)
    {
        if (depth == maxDepth)
        {
            return;
        }
        for (int i=0; i<leafCount; i++)
        {
            DefaultMutableTreeNode leaf = 
                new DefaultMutableTreeNode("depth_"+depth+"_leaf_"+i);
            node.add(leaf);
        }
        if (depth < maxDepth - 1)
        {
            for (int i=0; i<count; i++)
            {
                DefaultMutableTreeNode child = 
                    new DefaultMutableTreeNode("depth_"+depth+"_node_"+i);
                node.add(child);
                addNodes(child, depth+1, maxDepth, count, leafCount);
            }
        }

    }
}

筆記:

  • 這是一個自我回答的問題,我希望這個答案可能對其他人有所幫助。 但是, 1秒仍然很慢 我也嘗試了其他一些事情,例如設置tree.setLargeModel(true) ,但這並沒有產生積極的效果(實際上,它甚至更慢!)。 大部分時間仍用在樹的視覺狀態的最終更新中,我很高興在這里看到進一步的改進。

  • 可以用涉及DefaultMutableTreeNode#breadthFirstEnumerationDefaultTreeModel#getPathToRoot的幾行代碼來替換expandAllRecursively方法,而不會犧牲很多性能。 但是以當前形式,該代碼僅在TreeModel接口上運行,並且可以與任何類型的節點一起使用。

這里討論的, JTree已經使用flyweight模式來優化渲染。 我認為您在expandAllFaster()就足夠了。 擴展所有大於10 5的葉子充其量是笨拙的。 盡管適當的搜索控件可以緩解這種情況,但難以有意義地瀏覽生成的樹。

在Mac OS X TreeUI委托com.apple.laf.AquaTreeUI看到了一個有趣的折衷方案。 它遞歸擴展所選節點及其子節點時選擇鍵被按下時,由以下方法測定MouseEvent::isAltDown() 有關詳細信息,請參見名為"aquaFullyExpandNode"Action

最后,節省了用戶的擴張偏好可能是有價值的,因為例如

我正在研究…動態過濾> 100k節點的JTree

着眼於一個基於模型的方法,如建議在這里 ,將搜索到一個單獨的,也許是無模式,對話框。 概括來說,

  • 基於樹模型構造前綴樹以用作字典 ,也許使用此處建議的方法之一。

  • DocumentListener監視搜索字段並調整自定義TableModel條件以在用戶類型顯示匹配項。

  • 不顯示匹配項,直到輸入了最少的字符數; 三是大型機型的常見選擇。

  • TableModelListener展開與所選行相對應的樹節點; 或者,在“ 擴展”按鈕處理程序中擴展選定的行; 在無模式的上下文中,激發適當的PropertyChangeEvent ,樹應偵聽。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM