【问题标题】:Filtering on a JTree [closed]过滤 JTree [关闭]
【发布时间】:2012-03-03 07:22:03
【问题描述】:

问题

JTree 应用过滤以避免某些节点/叶子出现在JTree 的渲染版本中。理想情况下,我正在寻找一种允许使用动态过滤器的解决方案,但如果我可以使用静态过滤器,我已经很高兴了。

为了简单一点,我们假设JTree 只支持渲染,不支持编辑。移动、添加、删除节点应该是可能的。

例如,JTree 上方的搜索字段,输入 JTree 时只会显示匹配的子树。

一些限制:它用于可以访问 JDK 和 SwingX 的项目中。我想避免包含其他第三方库。

我已经想到了一些可能的解决方案,但似乎都不理想

方法

基于模型的过滤

  • 装饰TreeModel 以过滤掉一些值。快速和肮脏的版本很容易编写。过滤掉节点,并且在过滤器或委托TreeModel 的每次更改时,装饰器都可以触发整个树发生更改的事件(treeStructureChanged 以根节点为节点)。将此与恢复JTree 的选择状态和扩展状态的侦听器相结合,您将获得一个或多或少工作的版本,但源自TreeModel 的事件被搞砸了。这或多或少是this question 中使用的方法
  • 装饰TreeModel,但尝试触发正确的事件。我(还)没有设法想出这个的工作版本。似乎需要委托TreeModel 的副本,以便能够在从委托模型中删除节点时触发具有正确子索引的事件。我想再花一些时间我可以让它工作,但它只是感觉不对(过滤感觉像是视图应该做的事情,而不是模型)
  • 装饰用于创建初始TreeModel 的任何数据结构。但是,这是完全不可重用的,并且可能与为 TreeModel 编写装饰器一样难

基于视图的过滤

这似乎是要走的路。过滤不应该影响模型,而应该只影响视图。

  • 我看了RowFilter类。尽管 javadoc 似乎建议您可以将它与 JTree 结合使用:

    当与 JTree 关联时,条目对应于一个节点。

    我在RowFilter(或RowSorter)和JTree 类之间找不到任何链接。 RowFilter 和 Swing 教程的标准实现似乎表明 RowFilter 只能直接与 JTable 一起使用(请参阅 JTable#setRowSorter)。 JTree上没有类似的方法

  • 我还查看了JXTree javadoc。它有一个ComponentAdapter 可用,ComponentAdapter 的javadoc 表明RowFilter 可以与目标组件交互,但我看不到如何在RowFilterJTree 之间建立链接
  • 我还没有研究过JTable 如何使用RowFilters 处理过滤,也许同样可以在JTree 的修改版本上完成。

简而言之:我不知道解决这个问题的最佳方法是什么

注意:此问题可能与this question 重复,但该问题仍未得到解答,问题相当简短且答案似乎不完整,因此我想发布一个新问题。如果这没有完成(常见问题解答没有提供明确的答案)我将更新那个 3 岁的问题

【问题讨论】:

  • 仅供参考:SwingX 不支持对分层结构进行排序或过滤。实际上,我在几个月前开始进行排序,这看起来很有希望并且应该也适用于过滤(虽然还没有尝试过)寻找赞助商继续 - 提示,提示:-)
  • @kleopatra 有没有可能很快被包含在 SwingX 中?您是否可以在线尝试,以便我看看您的方法?
  • 当然,只要我能从某个地方挤出一些资金,让我可以继续工作。不,还没有什么像样的东西(是的,即使我在私下玩得很肮脏:-)
  • 我现在也有同样的问题。看到关于这个话题的答案如此之少,我感到非常沮丧……请问,您决定解决方案了吗?
  • @Robin 明白。但我不能放弃任务,应该尽快完成。像这样认为不好,(仅供参考,也许它可以帮助将来的人adrianwalker.org/2012/04/filtered-jtree.html。)

标签: java swing jtree swingx


【解决方案1】:

基于视图的过滤绝对是要走的路。您可以使用类似于我在下面编写的示例的内容。过滤树的另一个常见做法是在过滤树时切换到列表视图,因为列表不需要您显示需要显示其后代的隐藏节点。

这绝对是一个可怕的代码(我试图在刚刚编写它时尽可能地削减每个角落),但它应该足以让你开始。只需在搜索框中输入您的查询并按 Enter,它就会过滤 JTree 的默认模型。 (仅供参考,前 90 行只是生成的样板和布局代码。)

package com.example.tree;

import java.awt.BorderLayout;

public class FilteredJTreeExample extends JFrame {

    private JPanel contentPane;
    private JTextField textField;

    /**
     * Launch the application.
     */
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    FilteredJTreeExample frame = new FilteredJTreeExample();
                    frame.setVisible(true);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * Create the frame.
     */
    public FilteredJTreeExample() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setBounds(100, 100, 450, 300);
        contentPane = new JPanel();
        contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        contentPane.setLayout(new BorderLayout(0, 0));
        setContentPane(contentPane);

        JPanel panel = new JPanel();
        contentPane.add(panel, BorderLayout.NORTH);
        GridBagLayout gbl_panel = new GridBagLayout();
        gbl_panel.columnWidths = new int[]{34, 116, 0};
        gbl_panel.rowHeights = new int[]{22, 0};
        gbl_panel.columnWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
        gbl_panel.rowWeights = new double[]{0.0, Double.MIN_VALUE};
        panel.setLayout(gbl_panel);

        JLabel lblFilter = new JLabel("Filter:");
        GridBagConstraints gbc_lblFilter = new GridBagConstraints();
        gbc_lblFilter.anchor = GridBagConstraints.WEST;
        gbc_lblFilter.insets = new Insets(0, 0, 0, 5);
        gbc_lblFilter.gridx = 0;
        gbc_lblFilter.gridy = 0;
        panel.add(lblFilter, gbc_lblFilter);

        JScrollPane scrollPane = new JScrollPane();
        contentPane.add(scrollPane, BorderLayout.CENTER);
        final JTree tree = new JTree();
        scrollPane.setViewportView(tree);

        textField = new JTextField();
        GridBagConstraints gbc_textField = new GridBagConstraints();
        gbc_textField.fill = GridBagConstraints.HORIZONTAL;
        gbc_textField.anchor = GridBagConstraints.NORTH;
        gbc_textField.gridx = 1;
        gbc_textField.gridy = 0;
        panel.add(textField, gbc_textField);
        textField.setColumns(10);
        textField.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent evt) {
                TreeModel model = tree.getModel();
                tree.setModel(null);
                tree.setModel(model);
            }
        });

        tree.setCellRenderer(new DefaultTreeCellRenderer() {
            private JLabel lblNull = new JLabel();

            @Override
            public Component getTreeCellRendererComponent(JTree tree, Object value,
                    boolean arg2, boolean arg3, boolean arg4, int arg5, boolean arg6) {

                Component c = super.getTreeCellRendererComponent(tree, value, arg2, arg3, arg4, arg5, arg6);

                DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
                if (matchesFilter(node)) {
                    c.setForeground(Color.BLACK);
                    return c;
                }
                else if (containsMatchingChild(node)) {
                    c.setForeground(Color.GRAY);
                    return c;
                }
                else {
                    return lblNull;
                }
            }

            private boolean matchesFilter(DefaultMutableTreeNode node) {
                return node.toString().contains(textField.getText());
            }

            private boolean containsMatchingChild(DefaultMutableTreeNode node) {
                Enumeration<DefaultMutableTreeNode> e = node.breadthFirstEnumeration();
                while (e.hasMoreElements()) {
                    if (matchesFilter(e.nextElement())) {
                        return true;
                    }
                }

                return false;
            }
        });
    }

}

当您真正实现它时,您可能想要创建自己的 TreeNode 和 TreeCellRenderer 实现,使用不太愚蠢的方法来触发更新,并遵循 MVC 分离。请注意,“隐藏”节点仍会被渲染,但它们太小以至于您看不到它们。但是,如果您使用箭头键导航树,您会注意到它们仍然存在。如果您只需要一些有用的东西,这可能就足够了。

编辑

以下是 Mac OS 中树的未过滤和过滤版本的屏幕截图,显示空白在 Mac OS 中可见:

【讨论】:

  • 今晚我会检查你的代码,但我担心通过为过滤节点返回一个空标签,过滤节点仍会显示根句柄等内容。但我会更详细地检查它。
  • 一个不错的尝试,但视觉效果不够好。你最终会得到很多空白空间。我在你的回答中加入了一些截图来说明这一点
  • 抱歉没用。空白行没有出现在我面前;我想 JRE 的实现会有所作为。我添加了我的 Windows 屏幕截图。如果您返回一个大小为 0x0 的 Canvas 而不是空的 JLabel,它是否对您更有效?
  • 横向思维+1,即使效果不太好
  • 如果将不可见行的行高设置为0会怎样?
【解决方案2】:

这是一个仅使用标准 Swing 组件的可能解决方案:

我还没有使用过这个,但我更喜欢它的实现,而不是我在网上找到的其他快速的“ndirty”解决方案。

【讨论】:

  • 强调dirty :-) 基本上是在没有适当通知的情况下进行基于模型的过滤,根本无效......
  • 我必须同意@kleopatra。每次过滤器更改时用新模型替换我的模型会更好,这至少会触发正确的事件
  • 我同意你们两个。我打算自己实现它,但我决定使用完全不同的 UI 而不是可过滤树,否则我将报告我使用此实现的经验。
  • 我写了你所说的组件。无效怎么办?每次过滤时它都会替换模型。但是,如果您没有 1000 多个元素,这是一个完全可以接受的解决方案。
  • 投反对票,出于同样的原因@kleopatra 等人已经指出
【解决方案3】:

我使用的原则: 从 DB 中填充 ArrayList,然后填充树。当我必须过滤树节点时,我只是遍历 ArrayList,删除所有不符合条件的节点,然后使用修改后的 ArrayList 重建树...

【讨论】:

    【解决方案4】:

    我一直在研究过滤扩展JXTreeTable 的解决方法。为了简单起见,我采用了双模型方法。

    过滤模型

    public abstract class TellapicModelFilter extends DefaultTreeTableModel {
    
        protected Map<AbstractMutableTreeTableNode, 
                      AbstractMutableTreeTableNode>  family;
        protected Map<AbstractMutableTreeTableNode, 
                      AbstractMutableTreeTableNode>  filter;
        protected MyTreeTable                        treeTable;
        private   boolean                            withChildren;
        private   boolean                            withParents;
    
        /**
         * 
         * @param model
         */
        public TellapicModelFilter(MyTreeTable treeTable) {
            this(treeTable, false, false);
        }
    
        /**
        * 
        * @param treeTable
        * @param wp
        * @param wc
        */
        public TellapicModelFilter(MyTreeTable treeTable, boolean wp, boolean wc) {
            super(new DefaultMutableTreeTableNode("filteredRoot"));
            this.treeTable = treeTable;
            setIncludeChildren(wc);
            setIncludeParents(wp);
        }
    
        /**
         * 
         */
        public void filter() {
            filter = new HashMap<AbstractMutableTreeTableNode, AbstractMutableTreeTableNode>();
            family = new HashMap<AbstractMutableTreeTableNode, AbstractMutableTreeTableNode>();
            AbstractMutableTreeTableNode filteredRoot = (AbstractMutableTreeTableNode) getRoot();
            AbstractMutableTreeTableNode root = (AbstractMutableTreeTableNode) treeTable.getTreeTableModel().getRoot();
            filterChildren(root, filteredRoot);
            for(AbstractMutableTreeTableNode node : family.keySet())
                node.setParent(null);
            for(AbstractMutableTreeTableNode node : filter.keySet())
                node.setParent(filter.get(node));
        }
    
        /**
         * 
         * @param node
         * @param filteredNode
         */
        private void filterChildren(AbstractMutableTreeTableNode node, AbstractMutableTreeTableNode filteredNode) {
            int count = node.getChildCount();
            for(int i = 0; i < count; i++) {
                AbstractMutableTreeTableNode child = (AbstractMutableTreeTableNode) node.getChildAt(i);
                family.put(child, node);
                if (shouldBeFiltered(child)) {
                    filter.put(child, filteredNode);
                    if (includeChildren())
                        filterChildren(child, child);
                } else {
                    filterChildren(child, filteredNode);
                }
            }
        }
    
        /**
         * 
         */
        public void restoreFamily() {
            for(AbstractMutableTreeTableNode child : family.keySet()) {
                AbstractMutableTreeTableNode parent = family.get(child);
                child.setParent(parent);
            }  
        }
    
        /**
         * 
         * @param node
         * @return
         */
        public abstract boolean shouldBeFiltered(AbstractMutableTreeTableNode node); 
    
        /**
         * Determines if parents will be included in the filtered result. This DOES NOT means that parent will be filtered
         * with the filter criteria. Instead, if a node {@code}shouldBeFiltered{@code} no matter what the parent node is,
         * include it in the filter result. The use of this feature is to provide contextual data about the filtered node,
         * in the terms of: "where was this node that belongs to?"
         * 
         * @return True is parents should be included anyhow.
         */
        public boolean includeParents() {
            return withParents;
        }
    
        /**
         * Determines if children should be filtered. When a node {@code}shouldBeFiltered{@code} you can stop the filtering
         * process in that node by setting: {@code}setIncludeChildren(false){@code}. In other words, if you want to filter
         * all the tree, {@code}includeChildren{@code} should return true.
         * 
         * By letting this method return {@code}false{@code} all children of the node filtered will be automatically added
         * to the resulting filter. That is, children aren't filtered with the filter criteria and they will be shown with
         * their parent in the filter result.
         * 
         * @return True if you want to filter all the tree.
         */
        public boolean includeChildren() {
            return withChildren;
        }
    
        /**
         * 
         * @param include
         */
        public void setIncludeParents(boolean include) {
           withParents = include;
        }
    
       /**
        * 
        * @param include
        */
       public void setIncludeChildren(boolean include) {
           withChildren = include;
       }
    

    基本上,这个想法是将节点从原始模型连接/断开连接到过滤后的模型根,以跟踪当前 family 节点。

    过滤后的模型将在孩子和父母之间建立一个映射,并使用适当的方法来恢复这个家庭。方法‘restoreFamily’将重新连接那些丢失的孩子。

    过滤后的模型将在其filter() 方法中完成大部分工作,而将abstract 方法shouldBeFiltered(node) 留给实现:

    应该考虑到,在将过滤后的孩子连接到过滤后的根之前,无需断开与家庭的所有孩子的连接。如果性能是关键,则可以更深入地分析该行为。

    扩展 JXTreeTable

    最后,但最重要的是,需要通过实现一种方法并覆盖另一种方法来扩展底层树表:

    @Override
    public void setTreeTableModel(TreeTableModel treeModel) {
        if (!(treeModel instanceof TellapicModelFilter))
            model = treeModel;
    
        super.setTreeTableModel(treeModel);
    }
    
    public void setModelFilter(TellapicModelFilter mf) {
        if (modelFilter != null) {
            modelFilter.restoreFamily();
            setTreeTableModel(getUnfilteredModel());
        }
        // Is this necessary?
        if (mf == null) {
            setTreeTableModel(getUnfilteredModel());
        } else {
            modelFilter = mf;
            modelFilter.filter();
            setTreeTableModel(modelFilter);
        }
    }
    

    可以在link 中找到带有树表的完整且有效的示例。它包括一个Main.java 和一个准备构建的树。测试GUI 有一个按钮,用于在选定节点(如果有)中添加节点,并在框架顶部添加一个在写入时过滤的文本字段。

    【讨论】:

    • 您在 Main.java:58 行中缺少“字符串文本”。此外,从 UI 的角度来看,在键入查询时让分支折叠和节点重新排序是好的。
    • 感谢您指出缺少的变量。 Main.java 包含一个在写入时使用过滤的示例(在插入符号更新事件上)。重新排序问题可以简单地解决,维护一个有序的HashMap,反映节点的当前放置顺序。我会尝试更新代码。
    【解决方案5】:

    我终于设法找到了最适合我需要的东西,我想我会分享以防其他人使用它。

    我一直在尝试并排显示两个 JTree——一个包含另一个的过滤列表。

    基本上我已经制作了两个 TreeModel,并为两者使用相同的根节点。到目前为止,这似乎工作正常,只要我确保在我的代码中覆盖从 DefaultTreeModel 调用的每个方法,例如 nodeChanged( TreeNode node ) 否则会有痛苦。

    痛苦来自这样一个事实,即节点本身被查询以获取诸如 childcount 之类的信息的唯一时间是在 DefaultTreeModel 上调用节点结构类型方法时。除此之外,所有对树结构信息的调用都可以被截取过滤掉,如下所示。

    如果您像我一样使用 DefaultTreeModel 作为基础,那么您必须确保每次查询节点本身时都挖掘出来,这可能会令人讨厌。如果你直接实现 TreeModel 而不是像我一样偷懒,这个问题可能就不存在了。 NodesChanged 源代码直接来自 JDK 源代码。

    我很幸运,因为我想要的意味着总有一条从过滤列表中的每个项目返回根节点的路径。

    这就是我需要做的,即使我整天都在尝试疯狂而混乱的发明,比如重新创建树的浅层副本等,更不用说在 Stack 上阅读大量内容了!:

    public class FilteredSceneModel extends DefaultTreeModel {
    
    public static boolean isSceneItem(Object child) {
        return !(child instanceof DataItem);
    }
    
    public FilteredSceneModel(RootSceneNode root, SelectionModel sm) {
        super(root, sm);
    }
    
    private boolean isSceneFolder(Object node) {
        return node instanceof RootSceneNode || node instanceof Floor;
    }
    
    @Override
    public AbstractSceneItem getChild(Object parent, int index) {
        AbstractSceneItem asi = (AbstractSceneItem) parent;
        if (isSceneItem(parent)) {
            int dex = 0;
            for (AbstractSceneItem child : asi.children) {
                if (isSceneItem(child)) {
                    if (dex == index) {
                        return child;
                    }
                    dex++;
                }
            }
        }
        System.out.println("illegal state for: " + parent + " at index: " + index);
        return asi.getChildAt(index);
    }
    
    @Override
    public int getChildCount(Object parent) {
        if (isSceneItem(parent)) {
            AbstractSceneItem asi = (AbstractSceneItem) parent;
            int count = 0;
            for (AbstractSceneItem child : asi.children) {
                if (isSceneItem(child)) {
                    count++;
                }
            }
            return count;
        }
        return -1;
    }
    
    @Override
    public int getIndexOfChild(Object parent, Object childItem) {
        if (isSceneItem(parent)) {
            AbstractSceneItem asi = (AbstractSceneItem) parent;
            int count = 0;
            for (AbstractSceneItem child : asi.children) {
                if (isSceneItem(child)) {
                    if (child == childItem) {
                        return count;
                    }
                    count++;
                }
            }
        }
        return -1;
    }
    
    @Override
    public boolean isLeaf(Object node) {
        if (isSceneItem(node)) {
            if (isSceneFolder(node)) {
                return false;
            }
        }
        return true;
    }
    
    @Override
    public void activeFloorChanged(Floor floor) {
        for (AbstractSceneItem asi : floor) {
            if (isSceneItem(asi)) {
                nodeChanged(asi);
            }
        }
    }
    
    @Override
    protected void renamed(AbstractSceneItem asi) {
        if (isSceneItem(asi)) {
            nodeChanged(asi);
            System.out.println("scene only model renamed: " + asi.fullPathToString());
        }
    }
    
    @Override
    public void nodeChanged(TreeNode tn) {
        if (isSceneItem(tn)) {
            filteredNodeChanged(tn);
        }
    }
    
    @Override
    public void nodeStructureChanged(TreeNode tn) {
        if (isSceneItem(tn)) {
            super.nodeStructureChanged(tn);
        }
    }
    
    private void filteredNodeChanged(TreeNode node) {
        if (listenerList != null && node != null) {
            TreeNode parent = node.getParent();
    
            if (parent != null) {
                int anIndex = getIndexOfChild(parent, node);
                if (anIndex != -1) {
                    int[] cIndexs = new int[1];
    
                    cIndexs[0] = anIndex;
                    nodesChanged(parent, cIndexs);
                }
            } else if (node == getRoot()) {
                nodesChanged(node, null);
            }
        }
    }
    
    @Override
    public void nodesChanged(TreeNode node, int[] childIndices) {
        if (node != null) {
            if (childIndices != null) {
                int cCount = childIndices.length;
    
                if (cCount > 0) {
                    Object[] cChildren = new Object[cCount];
    
                    for (int counter = 0; counter < cCount; counter++) {
                        cChildren[counter] = getChild(node, childIndices[counter]);
                    }
                    fireTreeNodesChanged(this, getPathToRoot(node),
                            childIndices, cChildren);
                }
            } else if (node == getRoot()) {
                fireTreeNodesChanged(this, getPathToRoot(node), null, null);
            }
        }
    }
    }
    

    【讨论】:

      【解决方案6】:

      看看这个实现:http://www.java2s.com/Code/Java/Swing-Components/InvisibleNodeTreeExample.htm

      它创建 DefaultMutableNode 的子类,添加一个“isVisible”属性,而不是实际从 TreeModel 中删除/添加节点。我觉得很不错,它巧妙地解决了我的过滤问题。

      【讨论】:

      • 我认为这是一个很好的解决方案
      • 不错的基于模型的解决方案。感谢发帖!
      • 我认为,给定的示例(从 1999 年开始,哇!)在过滤树时不考虑添加新节点。因为这个,我得到了 ArrayIndexOutOfBoundsException。
      • 在使用 model.nodeChanged 或 model.nodeStructureChanged 时出现 ArrayIndexOutOfBoundsException。但是,model.refresh 似乎可以工作,所以我建议使用它来触发树中的更改。
      • @Amber 我不知道了。可能是我的意思是 reload() 而不是 refresh()。我已经有几年没有使用 Swing 了。
      【解决方案7】:

      ETableJTable 的子类和Outline 的父类,描述为here,包括“允许仅显示模型中的某些行的快速过滤功能(请参阅setQuickFilter())。”虽然这违反了没有“第三方库”的要求,但 Outline JAR 除了 JDK 之外没有其他依赖项。

      【讨论】:

      • 好奇:这真的在树方面有效吗?那是在折叠节点上正确过滤吗?过滤可见的扩展节点是更容易的部分。深入到隐藏节点(如果没有子树匹配,则隐藏父节点)是更棘手的部分。在我自己的(尚未发布的 SwingX 插件)中,它需要在内部遍历整个树(这可能很昂贵)
      • @kleopatra:我不确定我是否理解;更多here.
      • @kleopatra 你试过了吗,可能不是同时有过滤器和排序器
      • @mKorbel 我的错,没有检查,抱歉 :-)
      【解决方案8】:

      我对此有一个可能会感兴趣的建议。我已经在我自己的应用程序中将它付诸实践,它似乎工作得很好……下面是一个绝对最小的实现 SSCCE,展示了“insertNodeInto”。

      中心设计是 JTree-TreeModel 的多个耦合,它们都彼此保持完美同步...除了,显然,应用了一些过滤模式,因此某些节点(及其子树)不存在于一个中模型。同时,ON 树中的每个节点在 OFF 树中都有一个“对应”节点(尽管反过来不一定)。

      因此,最简单的设计涉及 2 个这样的耦合:一个带有过滤器“OFF”,另一个带有过滤器“ON”(顺便说一下,您可以有多个过滤器,因此您需要 n^2 个耦合器,其中 n是过滤器的数量......我已经完成了这个工作!)。

      要从一个耦合切换到另一个耦合(即从 ON 切换到 OFF,反之亦然),您只需在包含的 JViewport 中将一个 JTree 替换为另一个...这会在眨眼之间发生,完全不会被发现。所以这有点像视错觉。

      顺便说一下,这里使用的过滤器是“节点的 toString() 是否包含字符串 'nobble'”? (见方法FilterPair.is_filtered_out)

      有人可能会说这样的想法内存效率低得离谱……但实际上不同耦合中的节点,虽然不同的节点,使用相同的用户对象……所以我建议结构相当轻量级。

      要让两个联轴器(更不用说 4 个或 8 个)的机制相互同步要困难得多。下面我演示了一个相当全面的 insertNodeInto 实现......但是 DefaultTreeModel 的许多方法,JTree 的以及与选择有关的方法,都需要大量的思考。例如。如果(过滤器)OFF 树中的选择位于 ON 树中没有对应节点的节点上(因为它或其祖先之一已被过滤掉),那么 ON 树中的选择应该去哪里?我已经找到了所有这些问题的答案,但是这里没有空间来展示它们......

      import java.awt.*;
      import java.awt.event.*;
      import java.io.*;
      import javax.swing.*;
      import javax.swing.tree.*;
      
      public class FilterTreeDemo {
        public static void main(String[] args) throws FileNotFoundException {
          EventQueue.invokeLater(new ShowIt());
        }
      }
      
      class FiltNode extends DefaultMutableTreeNode { 
        FiltNode( Object user_obj ){
          super( user_obj );
        }
        FiltNode m_counterpart_node;
      
      //  public String toString(){
      //    // hash code demonstrates (as you toggle) that these are not the same nodes...
      //    return super.toString() + " (" + hashCode() + ")"; 
      //  }
      }
      
      class FilterPair {
      
        TreeCoupling m_on_coupling, m_off_coupling;
        boolean m_filter_on = true;
        JFrame m_main_frame;
        FiltNode m_on_root = new FiltNode( "root" );
        FiltNode m_off_root = new FiltNode( "root" );
      
        // needed to prevent infinite calling between models...  
        boolean m_is_propagated_call = false;
      
        FilterPair( JFrame main_frame ){
          m_on_root.m_counterpart_node = m_off_root;
          m_off_root.m_counterpart_node = m_on_root;
          m_on_coupling = new TreeCoupling( true ); 
          m_off_coupling = new TreeCoupling( false );
          m_main_frame = main_frame;
          // starts by toggling to OFF (i.e. before display)
          toggle_filter();
        }
      
        // this is the filter method for this particular FilterPair...
        boolean is_filtered_out( MutableTreeNode node ){
          return node.toString().contains( "nobble");
        }
      
      
        class TreeCoupling {
      
      
          class FilterTreeModel extends DefaultTreeModel {
            FilterTreeModel( TreeNode root ){
              super( root );
            }
      
            public void insertNodeInto(MutableTreeNode new_child, MutableTreeNode parent, int index){
              // aliases for convenience
              FiltNode new_filt_node = (FiltNode)new_child;
              FiltNode parent_filt_node = (FiltNode)parent;
      
              FiltNode new_counterpart_filt_node = null;
              FiltNode counterpart_parent_filt_node = null;
              // here and below the propagation depth test is used to skip code which is leading to another call to 
              // insertNodeInto on the counterpart TreeModel...
              if( ! m_is_propagated_call ){
                // NB the counterpart new FiltNode is given exactly the same user object as its counterpart: no duplication
                // of the user object...
                new_counterpart_filt_node = new FiltNode( new_filt_node.getUserObject() );
                counterpart_parent_filt_node = parent_filt_node.m_counterpart_node;
                // set up the 2 counterpart relationships between the node in the ON tree and the node in the OFF tree
                new_counterpart_filt_node.m_counterpart_node = new_filt_node;
                new_filt_node.m_counterpart_node = new_counterpart_filt_node;
              }
      
              if( TreeCoupling.this == m_on_coupling ){
                // ... we are in the ON coupling
      
                // if first call and the parent has no counterpart (i.e. in the OFF coupling) sthg has gone wrong
                if( ! m_is_propagated_call && counterpart_parent_filt_node == null ){
                  throw new NullPointerException();
                }
                if( ! is_filtered_out( new_filt_node ) ){
                  // only insert here (ON coupling) if the node is NOT filtered out...
                  super.insertNodeInto( new_filt_node, parent_filt_node, index);
                }
                else {
                  // enable the originally submitted new node (now rejected) to be unlinked and garbage-collected...
                  // (NB if you suspect the first line here is superfluous, try commenting out and see what happens)
                  new_filt_node.m_counterpart_node.m_counterpart_node = null;
                  new_filt_node.m_counterpart_node = null;
                }
                if( ! m_is_propagated_call  ){
                  // as we are in the ON coupling we can't assume that the index value should be passed on unchanged to the
                  // OFF coupling: some siblings (of lower index) may be missing here... but we **do** know that the previous 
                  // sibling (if there is one) of the new node has a counterpart in the OFF tree... so get the index of its
                  // OFF counterpart and add 1...
                  int off_index = 0;
                  if( index > 0 ){
                    FiltNode prev_sib = (FiltNode)parent_filt_node.getChildAt( index - 1 );
                    off_index = counterpart_parent_filt_node.getIndex( prev_sib.m_counterpart_node ) + 1;
                  }
                  m_is_propagated_call = true;
                  m_off_coupling.m_tree_model.insertNodeInto( new_counterpart_filt_node, counterpart_parent_filt_node, off_index);
                }
      
              }
              else {
                // ... we are in the OFF coupling
      
                super.insertNodeInto( new_filt_node, parent_filt_node, index);
                if( ! m_is_propagated_call  ){
      
                  // we are in the OFF coupling: it is perfectly legitimate for the parent to have no counterpart (i.e. in the 
                  // ON coupling: indicates that it, or an ancestor of it, has been filtered out)
                  if( counterpart_parent_filt_node != null ){
                    // OTOH, if the parent **is** available, we can't assume that the index value should be passed on unchanged: 
                    // some siblings of the new incoming node (of lower index) may have been filtered out... to find the 
                    // correct index value we track down the index value until we reach a node which has a counterpart in the 
                    // ON coupling... or if not found the index must be 0 
                    int on_index = 0;
                    if( index > 0 ){
                      for( int i = index - 1; i >= 0; i-- ){
                        FiltNode counterpart_sib = ((FiltNode)parent_filt_node.getChildAt( i )).m_counterpart_node;
                        if( counterpart_sib != null ){
                          on_index = counterpart_parent_filt_node.getIndex( counterpart_sib ) + 1;
                          break;
                        }
                      }
                    }
                    m_is_propagated_call = true;
                    m_on_coupling.m_tree_model.insertNodeInto( new_counterpart_filt_node, counterpart_parent_filt_node, on_index);
                  }
                  else {
                    // ... no ON-coupling parent node "counterpart": the new ON node must be discarded  
                    new_filt_node.m_counterpart_node = null;
                  }
      
      
                }
              }
              m_is_propagated_call = false;
            }
          }
      
          JTree m_tree;
          FilterTreeModel m_tree_model;
          TreeCoupling( boolean on ){
            m_tree = new JTree();
            m_tree_model = on ? new FilterTreeModel( m_on_root ) : new FilterTreeModel( m_off_root ); 
            m_tree.setModel( m_tree_model );
          }
        }
      
         void toggle_filter(){
          m_filter_on = ! m_filter_on;
          m_main_frame.setTitle( m_filter_on? "FilterTree - ON (Ctrl-F6 to toggle)" : "FilterTree - OFF (Ctrl-F6 to toggle)" ); 
        }
      
        TreeCoupling getCurrCoupling(){
          return m_filter_on? m_on_coupling : m_off_coupling;
        }
      }
      
      
      class ShowIt implements Runnable {
        @Override
        public void run() {
          JFrame frame = new JFrame("FilterTree");
          final FilterPair pair = new FilterPair( frame ); 
          final JScrollPane jsp = new JScrollPane( pair.getCurrCoupling().m_tree );
          Action toggle_between_views = new AbstractAction( "toggle filter" ){
            @Override
            public void actionPerformed(ActionEvent e) {
              pair.toggle_filter();
              jsp.getViewport().setView( pair.getCurrCoupling().m_tree );
              jsp.requestFocus();
            }};
          JPanel cpane = (JPanel)frame.getContentPane(); 
          cpane.getActionMap().put("toggle between views", toggle_between_views );
          InputMap new_im = new InputMap();
          new_im.put(KeyStroke.getKeyStroke(KeyEvent.VK_F6, InputEvent.CTRL_DOWN_MASK), "toggle between views");
          cpane.setInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, new_im);
      
          frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
          frame.getContentPane().add(jsp);
          frame.pack();
          frame.setBounds(50, 50, 800, 500);
          frame.setVisible(true);
      
          // populate the tree(s) NB we are currently viewing the OFF tree
          FilterPair.TreeCoupling curr_coupling = pair.getCurrCoupling(); 
          curr_coupling.m_tree_model.insertNodeInto( new FiltNode( "scrags 1" ), (FiltNode)curr_coupling.m_tree_model.getRoot(), 0 );
          FiltNode d2 = new FiltNode( "scrags 2" );
          curr_coupling.m_tree_model.insertNodeInto( d2, (FiltNode)curr_coupling.m_tree_model.getRoot(), 1 );
          curr_coupling.m_tree_model.insertNodeInto( new FiltNode( "scrags 3" ), (FiltNode)curr_coupling.m_tree_model.getRoot(), 2 );
          curr_coupling.m_tree_model.insertNodeInto( new FiltNode( "scrags 2.1" ), d2, 0 );
      
          // this will be filtered out of the ON tree
          FiltNode nobble = new FiltNode( "nobble" );
          curr_coupling.m_tree_model.insertNodeInto( nobble, d2, 1 );
          // this will also be filtered out of the ON tree
          FiltNode son_of_nobble = new FiltNode( "son of nobble");
          curr_coupling.m_tree_model.insertNodeInto( son_of_nobble, nobble, 0 );    
      
          curr_coupling.m_tree_model.insertNodeInto( new FiltNode( "peewit (granddaughter of n****e)"), son_of_nobble, 0 );    
      
          // expand the OFF tree
          curr_coupling.m_tree.expandPath( new TreePath( curr_coupling.m_tree_model.getRoot() ) );
          curr_coupling.m_tree.expandPath( new TreePath( d2.getPath() ) );
          curr_coupling.m_tree.expandPath( new TreePath( nobble.getPath() ) );
          curr_coupling.m_tree.expandPath( new TreePath( son_of_nobble.getPath() ) );
      
          // switch view (programmatically) to the ON tree
          toggle_between_views.actionPerformed( null );
      
          // expand the ON tree
          curr_coupling = pair.getCurrCoupling();
          curr_coupling.m_tree.expandPath( new TreePath( curr_coupling.m_tree_model.getRoot() ) );
          curr_coupling.m_tree.expandPath( new TreePath( d2.m_counterpart_node.getPath() ) );
      
          // try to expand the counterpart of "nobble"... there shouldn't be one...
          FiltNode nobble_counterpart = nobble.m_counterpart_node;
          if( nobble_counterpart != null ){
            curr_coupling.m_tree.expandPath( new TreePath( nobble_counterpart.getPath() ) );
            System.err.println( "oops..." );
          }
          else {
            System.out.println( "As expected, node \"nobble\" has no counterpart in the ON coupling" );
          }
      
      
      
          // try inserting a node into the ON tree which will immediately be "rejected" by the ON tree (due to being 
          // filtered out before the superclass insertNodeInto is called), but will nonetheless appear in the 
          // OFF tree as it should...
          curr_coupling.m_tree_model.insertNodeInto( new FiltNode( "yet another nobble"), d2.m_counterpart_node, 0 );
      
      
        }
      }
      

      再想一想:如何概括它,以便 FilterTreeModel 扩展您自己提供的 DefaultTreeModel 子类? (并且在完整的实现中以便 FilterJTree 扩展您提供的 JTree 子类?)。我最初在 Jython 中编写了这段代码,其中将 A 类作为 B 类定义的参数传递是微不足道的!使用庄严的旧 Java,它可能通过反射和静态工厂方法来完成,或者可能通过一些巧妙的封装技术来完成。不过,这将是一个艰难的过程。如果可能,最好切换到 Jython!

      【讨论】:

        【解决方案9】:

        老问题,我偶然发现......对于所有想要快速简便解决方案的人

        只是过滤视图:

        我知道它不像过滤模型那么干净,并且可能会出现回撤,但如果您只是想要一个小型应用程序的快速解决方案:

        扩展 DefaultTableCellRenderer,覆盖 getTreeCellRendererComponent - 调用 super.getTreeCellRendererComponent(...) ,然后将要隐藏的所有节点的首选高度设置为零。 在构建 JTree 时,请务必设置 setRowHeight(0); - 这样它就会尊重每一行的首选高度...

        瞧 - 所有过滤的行都不可见!

        完整的工作示例

        import java.awt.BorderLayout;
        import java.awt.Component;
        import java.awt.Dimension;
        import java.awt.EventQueue;
        import java.awt.event.ActionEvent;
        import java.awt.event.ActionListener;
        
        import javax.swing.Box;
        import javax.swing.BoxLayout;
        import javax.swing.JButton;
        import javax.swing.JFrame;
        import javax.swing.JTree;
        import javax.swing.UIManager;
        import javax.swing.tree.DefaultMutableTreeNode;
        import javax.swing.tree.DefaultTreeCellRenderer;
        import javax.swing.tree.DefaultTreeModel;
        
        public class JTreeExample
        {
            public static void main( final String[] args ) throws Exception
            {
                UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
        
                // The only correct way to create a SWING Frame...
                EventQueue.invokeAndWait( new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            swingMain();
                        }
                    } );
            }
        
            protected static void swingMain()
            {
                final JFrame f = new JFrame( "JTree Test" );
                f.setLocationByPlatform( true );
                f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        
                final int items = 5;
        
                final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode( "JTree", true );
                final DefaultTreeModel myModel = new DefaultTreeModel( rootNode );
        
                final Box buttonBox = new Box( BoxLayout.X_AXIS );
        
                for( int i = 0; i < items; i++ )
                {
                    final String name = "Node " + i;
                    final DefaultMutableTreeNode newChild = new DefaultMutableTreeNode( name );
                    rootNode.add( newChild );
        
                    final JButton b = new JButton( "Show/Hide " + i );
                    buttonBox.add( b );
                    b.addActionListener( new ActionListener()
                        {
                            @Override
                            public void actionPerformed( final ActionEvent e )
                            {
                                // If the node has a Text, set it to null, otherwise reset it
                                newChild.setUserObject( newChild.getUserObject() == null ? name : null );
                                myModel.nodeStructureChanged( newChild.getParent() );
                            }
                        } );
                }
        
                final JTree tree = new JTree( myModel );
                tree.setRowHeight( 0 );
                tree.setCellRenderer( new JTreeExample.TreeRenderer() );
        
                f.add( tree, BorderLayout.CENTER );
                f.add( buttonBox, BorderLayout.SOUTH );
        
                f.setSize( 600, 500 );
                f.setVisible( true );
            }
        
            public static class TreeRenderer extends DefaultTreeCellRenderer
            {
                @Override
                public Component getTreeCellRendererComponent( final JTree tree, final Object value, final boolean selected,
                                                                final boolean expanded, final boolean leaf, final int row, final boolean hasFocus )
                {
                    // Invoke default Implementation, setting all values of this
                    super.getTreeCellRendererComponent( tree, value, selected, expanded, leaf, row, hasFocus );
        
                    if( !isNodeVisible( (DefaultMutableTreeNode)value ) )
                    {
                        setPreferredSize( new Dimension( 0, 0 ) );
                    }
                    else
                    {
                        setPreferredSize( new Dimension( 200, 15 ) );
                    }
        
                    return this;
                }
            }
        
            public static boolean isNodeVisible( final DefaultMutableTreeNode value )
            {
                // In this example all Nodes without a UserObject are invisible
                return value.getUserObject() != null;
            }
        }
        

        【讨论】:

        • 它不起作用,你只是得到了一些整体,比如过滤视图的所有方法,包括那些返回空标签的方法和那些使用 setRowHeight() 来指示行高是可变的。跨度>
        • 我已经在我编写的应用程序中启动并运行它......如果我有时间,我会将代码分解为一个工作最小的示例。如果您在构建 JTree 时 setRowHeight() ,它将查询每个节点的高度并相应地缩放。
        • 我就是这样做的,我查看了Java的源代码,我看到当行高大于或等于0时,它被认为是非常数。因此,我调用了一次 setRowHeight(0),并在方法 getTreeCellRendererComponent 中将首选大小设置为 (0,0)。问题是可变大小的管理也取决于渲染器。我在Windows下使用了默认的UI,这个技巧并没有去掉空格。
        • 所以 - 我插入了一个完整的工作示例 - 使用 WindowsUI ;-)
        • 由于节点高度为 0,此方法为我中断了 JTree 的鼠标滚动
        【解决方案10】:

        给出了解决方案http://forums.sun.com/thread.jspa?forumID=57&threadID=5378510

        我们在这里实现了它,它就像一个魅力。

        您只需实现 TreeModel,并在 JTree 上使用 FilterTreeModelTreeFilter

        实现很简单,也许在监听器上需要做一些事情,因为实际的代码会调用两次,这一点都不好。我的想法是将监听器传递给委托模型,我没有看到在过滤器模型上添加监听器的意义......但这是另一个问题。

        【讨论】:

        • 那个实现看起来不正确。例如,添加到FilterTreeModel 的侦听器将接收来自委托模型的事件。不仅源树模型出乎意料,事件还可能包含在 FilterTreeModel 上根本不可用的索引。
        • @Robin,是的,实现不能完美,因为委托模型可以有其他侦听器。我只是将代码更改为始终使用委托模型侦听器(并尽可能填充它们)。我知道您不能将此代码用于一般目的,但如果您完全控制委托模型的侦听器,我非常适合我。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-10-24
        • 1970-01-01
        • 2016-08-20
        • 2015-02-13
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多