Подтвердить что ты не робот

Фильтрация на JTree

Проблема

Применение фильтрации на JTree, чтобы избежать появления определенных узлов/листов в отображаемой версии JTree. В идеале я ищу решение, которое позволяет иметь динамический фильтр, но я уже был бы рад, если бы смог заставить статический фильтр работать.

Чтобы сделать это немного проще, допустим, что JTree поддерживает только рендеринг, а не редактирование. Перемещение, добавление, удаление узлов должно быть возможным.

Пример - это поле поиска над JTree, и при вводе JTree будет отображаться только поддерево с совпадением.

Несколько ограничений: он должен использоваться в проекте, который имеет доступ к JDK и SwingX. Я хотел бы избежать включения других сторонних библиотек.

Я уже думал о нескольких возможных решениях, но ни один из них не казался идеальным

Подходы

Фильтрация на основе моделей

  • украсить TreeModel, чтобы отфильтровать некоторые из значений. Быстрая и грязная версия проста в написании. Отфильтруйте узлы, а при каждом изменении фильтра или делегата TreeModel декоратор может запустить событие, которое изменило все дерево (treeStructureChanged с корнем node как node). Объедините это с слушателями, которые восстанавливают состояние выбора и состояние расширения JTree, и вы получаете версию, которая работает более или менее, но события, происходящие из TreeModel, испорчены. Это более или менее подход, используемый в этом вопросе
  • украсьте TreeModel, но попробуйте запустить правильные события. Мне еще не удалось найти рабочую версию этого. Кажется, требуется копия делегата TreeModel, чтобы иметь возможность запускать событие с правильными дочерними индексами, когда узлы удаляются из модели делегата. Я думаю, что еще некоторое время я мог бы заставить это работать, но он просто чувствует себя не так (фильтрация похожа на то, что должен делать вид, а не на модель).
  • украсить любую структуру данных, используемую для создания начального TreeModel. Однако это полностью невозможно использовать повторно и, возможно, так сложно записать декоратор для TreeModel

Просмотр фильтрации на основе

Это похоже на путь. Фильтрация не должна влиять на модель, а только на представление.

  • Я просмотрел класс RowFilter. Хотя javadoc, кажется, предлагает вам использовать его в комбинации с JTree:

    когда он связан с JTree, запись соответствует node.

    Я не мог найти никакой связи между RowFilter (или RowSorter) и классом JTree. Стандартные реализации RowFilter и обучающих программ Swing, по-видимому, предполагают, что RowFilter можно использовать непосредственно с помощью JTable (см. JTable#setRowSorter). Подобных методов не существует на JTree

  • Я также посмотрел JXTree javadoc. Он имеет ComponentAdapter, а javadoc ComponentAdapter указывает, что RowFilter может взаимодействовать с целевым компонентом, но я не могу см., как я делаю ссылку между RowFilter и JTree
  • Я еще не посмотрел, как JTable обрабатывает фильтрацию с помощью RowFilter s, и, возможно, то же самое можно сделать в модифицированной версии JTree.

Короче говоря: я не знаю, как лучше всего решить этот

Примечание: этот вопрос является возможным дубликатом этого вопроса, но этот вопрос по-прежнему остается без ответа, вопрос довольно короткий, и ответы кажутся неполными, поэтому я подумал опубликуйте новый вопрос. Если это не сделано (в FAQ не было четкого ответа на этот вопрос), я обновлю этот 3-летний вопрос

4b9b3361

Ответ 1

Фильтрация на основе изображений - это, безусловно, путь. Вы можете использовать что-то вроде примера, который я закодировал ниже. Другой распространенной практикой при фильтрации деревьев является переход к представлению списка при фильтрации дерева, поскольку список не потребует от вас отображения скрытых узлов, чьи потомки должны быть показаны.

Это абсолютно ужасный код (я попытался разрезать каждый угол, возможно, взбивая его сейчас), но этого должно быть достаточно, чтобы вы начали. Просто введите запрос в поле поиска и нажмите Enter, и он будет фильтровать модель по умолчанию JTree. (FYI, первые 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. Обратите внимание, что "скрытые" узлы все еще отображаются, но они настолько малы, что вы их не видите. Если вы используете клавиши со стрелками для навигации по дереву, вы заметите, что они все еще там. Если вам просто нужно что-то, что работает, это может быть достаточно хорошим.

Filtered tree (windows)

Edit

Вот скриншоты нефильтрованной и отфильтрованной версии дерева в Mac OS, показывающие, что пробелы видны в Mac OS:

Unfiltered treeFiltered tree

Ответ 2

Взгляните на эту реализацию: http://www.java2s.com/Code/Java/Swing-Components/InvisibleNodeTreeExample.htm

Он создает подклассы DefaultMutableNode, добавляя свойство isVisible, а не фактически удаляя/добавляя узлы из TreeModel. Довольно сладкий, я думаю, и он решил мою проблему фильтрации аккуратно.

Ответ 3

Старый вопрос, я наткнулся на... для тех, кто хочет быстрое и простое решение

ТОЛЬКО ФИЛЬТРЫ ПРОСМОТРА:

Я знаю, что это не так чисто, как Фильтрация модели и поставляется с возможными backdraws, но если вам просто нужно быстрое решение для небольшого приложения:

Расширьте DefaultTableCellRenderer, переопределите getTreeCellRendererComponent - вызовите super.getTreeCellRendererComponent(...), после чего просто установите предпочтительную высоту в ZERO для всех узлов, которые вы хотите скрыть. При построении JTree обязательно установите setRowHeight (0); - поэтому он будет уважать предпочтительную высоту каждой строки...

voila - все отфильтрованные строки невидимы!

ПОЛНЫЙ РАБОЧИЙ ПРИМЕР

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

Ответ 4

Здесь возможно решение, в котором используются только стандартные компоненты Swing:

Я еще не использовал это, но мне нравится его реализация гораздо больше, чем другие быстрые "грязные решения", которые я нашел в Интернете.

Ответ 5

Я работал над рабочим столом для фильтрации расширенного 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;
   }

В принципе, идея заключалась в том, чтобы подключить/отсоединить узлы от исходной модели до фильтрованного корня модели, отслеживая текущие семейные узлы.

Отфильтрованная модель будет иметь сопоставление между детьми и родителями, с подходящим методом для восстановления этого семейства. Метод "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);
    }
}

В этом ссылке можно найти полный и рабочий пример с тэстером. Он включает Main.java с готовым деревом. В тесте GUI есть кнопка, которая добавляет узлы в выбранном node (если есть), а в верхней части кадра - текстовое поле, которое фильтрует при написании.

Ответ 6

Наконец-то мне удалось вытащить что-то, что соответствует моим потребностям, и подумал, что я поделюсь им, если кто-то еще сможет его использовать.

Я пытался отобразить два JTrees бок о бок - тот, который содержит отфильтрованный список другого.

В основном я создал два TreeModels и использовал один и тот же корень node для обоих. Пока это работает, пока я не буду переопределять метод EVERY, который вызывается из DefaultTreeModel в моем коде, например nodeChanged (TreeNode node), иначе будет боль.

Боль возникает из-за того, что единственный раз, когда сами узлы запрашиваются для информации, такой как childcount, - это когда методы типа nodestructure вызывают в DefaultTreeModel. Кроме того, все вызовы информации древовидной структуры могут быть перехвачены и отфильтрованы, как показано ниже.

Это может стать неприятным, поскольку вы должны убедиться, что вы выкапываете каждый раз, когда сами узлы запрашиваются, если вы используете DefaultTreeModel в качестве базы, как я. Эта проблема может отсутствовать, если вы реализуете TreeModel напрямую, а не ленитесь, как я. Источник NodesChanged появился прямо из источника JDK.

Мне повезло, что то, что я хотел, означало, что всегда был путь к корню node из каждого элемента в отфильтрованном списке.

Это все, что мне нужно было сделать, даже если я потратил весь день на дикие и хаотичные изобретения, например, воссоздать мелкую копию дерева и т.д., не говоря уже о том, чтобы читать лоты на 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

ETable, подкласс JTable и родительский класс Outline, описание здесь, включает в себя функции быстрого фильтра, позволяющие отображать только определенные строки из модели (см. setQuickFilter()). "Хотя это не соответствует требованию" сторонних пользователей", Outline JAR не имеет зависимостей, кроме JDK.

Ответ 8

Принцип, который я использовал: Заполнение ArrayList из базы данных, а затем заполнение дерева. Когда я должен фильтровать узлы дерева, я просто перебираю ArrayList, удаляю все узлы, которые не соответствуют критериям, а затем восстанавливает дерево с измененным ArrayList...

Ответ 9

У меня есть предложение для этого, которое может представлять интерес. Я применил это на практике в своем собственном приложении и, похоже, хорошо работает... ниже - это абсолютная минимальная реализация SSCCE, демонстрирующая "insertNodeInto".

Центральная конструкция - это множественные связи JTree-TreeModel, которые все находятся в идеальной синхронизации друг с другом... кроме, очевидно, что применяется некоторый шаблон фильтрации, так что определенные узлы (и их поддеревья) отсутствуют в одном модель. Между тем каждый node в дереве ON имеет "экземпляр" node в дереве OFF (хотя обратное не обязательно верно).

Простейшая конструкция поэтому включает в себя 2 таких муфты: один с фильтром "ВЫКЛ", а другой с фильтром "ON" (кстати, у вас может быть более одного фильтра, так что тогда вам понадобятся n ^ 2 муфты, где n это количество фильтров... и у меня это работает!).

Чтобы переключиться с одной связи на другую (то есть с ON на OFF или наоборот), вы просто подставляете один JTree для другого в содержащий JViewport... и это происходит в мгновение ока, совершенно не поддающееся проверке. Так что это похоже на оптическую иллюзию.

Кстати, используемый здесь фильтр - это "делает ли toString() из node строка" благородная "? (см. метод FilterPair.is_filtered_out)

Некоторые могут сказать, что такая идея будет смехотворно неэффективной памяти... но на самом деле узлы в разных связях, хотя и разные узлы, используют один и тот же объект пользователя... поэтому я предлагаю, чтобы структура была довольно легкой.

Намного сложнее получить механику двух муфт (не говоря уже о 4 или 8), чтобы синхронизировать друг с другом. Ниже я демонстрирую довольно полную реализацию insertNodeInto... но многие методы DefaultTreeModel, JTree, а также связанные с выбором, требуют много размышлений. Например. если выбор в дереве (фильтра) OFF находится на node, который не имеет аналогов в дереве 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, это можно было бы сделать с помощью методов отражения и статики factory или, возможно, с помощью какой-то изобретательной техники инкапсуляции. Тем не менее, это было бы тяжело. Лучше переключиться на Jython, если это вообще возможно!