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
Model based filtering
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 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)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
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
JTable
handles the filtering with RowFilter
s, 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
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.
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.
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:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With