Filtering on a JTree [closed]

2019-01-09 05:24发布

Problem

Applying filtering on a JTree to avoid certain nodes/leaves to show up in the rendered version of the JTree. Ideally I am looking for a solution which allows to have a dynamic filter, but I would already be glad if I can get a static filter to work.

To make it a bit easier, let us suppose the JTree only supports rendering, and not editing. Moving, adding, removing of nodes should be possible.

An example is a search field above a JTree, and on typing the JTree would only show the subtree with matches.

A few restrictions: it is to be used in a project which has access to JDK and SwingX. I would like to avoid to include other third party libs.

I already thought of a few possible solutions, but neither of those seemed ideal

Approaches

Model based filtering

  • decorate the TreeModel to filter out some of the values. A quick-and-dirt version is easy to write. Filter out nodes, and on every change of the filter or the delegate TreeModel the decorator can fire an event that the whole tree has changes (treeStructureChanged with the root node as node). Combine this with listeners which restore the selection state and the expansion state of the JTree and you get a version which works more or less, but the events originating from the TreeModel are messed up. This is more or less the approach used in this question
  • decorate the TreeModel but try fire the correct events. I did not (yet) managed to come up with a working version of this. It seems to require a copy of the delegate TreeModel in order to be able to fire an event with the correct child indices when nodes are removed from the delegate model. I think with some more time I could get this to work, but it just feels wrong (filtering feels like something the view should do, and not the model)
  • decorate whatever data structure was used to create the initial TreeModel. However, this is completely non-reusable, and probably as hard as to write a decorator for a TreeModel

View based filtering

This seems like the way to go. Filtering should not affect the model but only the view.

  • I took a look at RowFilter class. Although the javadoc seems to suggest you can use it in combination with a JTree:

    when associated with a JTree, an entry corresponds to a node.

    I could not find any link between RowFilter (or RowSorter) and the JTree class. The standard implementations of RowFilter and the Swing tutorials seems to suggest that RowFilter can only be used directly with a JTable (see JTable#setRowSorter). No similar methods are available on a JTree

  • I also looked at the JXTree javadoc. It has a ComponentAdapter available and the javadoc of ComponentAdapter indicates a RowFilter could interact with the target component, but I fail to see how I make the link between the RowFilter and the JTree
  • I did not yet look at how a JTable handles the filtering with RowFilters, and perhaps the same can be done on a modified version of a JTree.

So in short: I have no clue on what's the best approach to solve this

Note: this question is a possible duplicate of this question, but that question is still unanswered, the question rather short and the answers seems incomplete, so I thought to post a new question. If this is not done (the FAQ did not provide a clear answer on this) I will update that 3year old question

10条回答
我命由我不由天
2楼-- · 2019-01-09 05:34

View-based filtering is definitely the way to go. You can use something like the example I've coded below. Another common practice when filtering trees is to switch to a list view when filtering a tree, since the list won't require you to show hidden nodes whose descendants need to be shown.

This is absolutely horrendous code (I tried to cut every corner possible in whipping it up just now), but it should be enough to get you started. Just type your query in the search box and press Enter, and it'll filter the JTree's default model. (FYI, the first 90 lines are just generated boilerplate and layout code.)

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

}

When you implement it for real, you'll probably want to create your own TreeNode and TreeCellRenderer implementations, use a less stupid method for triggering an update, and follow MVC separation. Note that the "hidden" nodes are still rendered, but they're so small that you can't see them. If you use the arrow keys to navigate the tree, though, you'll notice that they're still there. If you just need something that works, this might be good enough.

Filtered tree (windows)

Edit

Here are screenshots of the unfiltered and filtered version of the tree in Mac OS, showing that the whitespace is visible in Mac OS:

Unfiltered treeFiltered tree

查看更多
爷、活的狠高调
3楼-- · 2019-01-09 05:38

ETable, a subclass of JTable and the parent class of Outline, described here, includes "Quick-Filter features allowing to show only certain rows from the model (see setQuickFilter())." While this violates the no "third party libs" requirement, the Outline JAR has no dependencies other than the JDK.

查看更多
男人必须洒脱
4楼-- · 2019-01-09 05:53

Take a look at this implementation: http://www.java2s.com/Code/Java/Swing-Components/InvisibleNodeTreeExample.htm

It creates subclasses of DefaultMutableNode adding a "isVisible" property rather then actually removing/adding nodes from the TreeModel. Pretty sweet I think, and it solved my filtering problem neatly.

查看更多
beautiful°
5楼-- · 2019-01-09 05:53

Old Question, I stumbled upon... for all those who want a quick and easy Solution of

JUST FILTERING THE VIEW:

I know it isn't as clean as Filtering the Model and comes with possible backdraws, but if you just want a quick solution for a small Application:

Extend the DefaultTableCellRenderer, override getTreeCellRendererComponent - invoke super.getTreeCellRendererComponent(...) and after that just set the Preferred Height to ZERO for all Nodes you want to hide. When constructing your JTree be sure to set setRowHeight(0); - so it will respect the Preferred Height of each row...

voila - all filtered Rows invisible!

COMPLETE WORKING EXAMPLE

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;
    }
}
查看更多
【Aperson】
6楼-- · 2019-01-09 05:53

I've finally managed to pull of something the suits my needs perfectly, and thought I'd share in case someone else can use it.

I have been trying to display two JTrees side by side - one that contains a filtered list of the other.

Basically I have made two TreeModels, and use the same root node for both. This seems to work fine so far, so long as I make sure to override EVERY method that gets called from DefaultTreeModel in my code, such as nodeChanged( TreeNode node ) or else there will be pain.

The pain comes from the fact that the only time the nodes themselves are queried for information like childcount, is when nodestructure type methods are called on the DefaultTreeModel. Aside from that, all calls for tree structure information can be intercepted and filtered out as shown below.

This can get nasty as you have to make sure you dig out every time the nodes themselves are queried if you use DefaultTreeModel as the base like I did. This problem might not be there if you implement TreeModel directly instead of being lazy like me. NodesChanged source came straight from the JDK source.

I was lucky in that what I wanted meant there was always a path back to the root node from every item in the filtered list.

This was all I needed to do, even tho I spent the whole day trying wild and chaotic inventions, like recreating a shallow copy of the tree etc, not to mention reading lots on 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);
        }
    }
}
}
查看更多
相关推荐>>
7楼-- · 2019-01-09 05:53

A solution was given http://forums.sun.com/thread.jspa?forumID=57&threadID=5378510

We have implemented it here, adn it works as a charm.

You just have to implement you TreeModel, and on the JTree use a FilterTreeModel with a TreeFilter.

The implementation is quite simple, there is perhaps maybe some stuff to do on listener, since the actual code will called twice, which is not good at all. My idea is to just pass listener to delegate model, I don't see the point to add listener on the filter model... But that is another question.

查看更多
登录 后发表回答