Pregunta Filtrado en un JTree


Problema

Aplicar el filtrado en un JTree para evitar que ciertos nodos / hojas aparezcan en la versión representada del JTree. Idealmente, estoy buscando una solución que permita tener un filtro dinámico, pero ya estaría contento de poder hacer funcionar un filtro estático.

Para hacerlo un poco más fácil, supongamos que JTree solo admite la representación y no la edición. Mover, agregar, quitar nodos debería ser posible.

Un ejemplo es un campo de búsqueda sobre un JTree, y al tipear el JTree solo mostraría el subárbol con coincidencias.

Algunas restricciones: se utilizará en un proyecto que tenga acceso a JDK y SwingX. Me gustaría evitar incluir otras librerías de terceros.

Ya pensé en algunas posibles soluciones, pero ninguna de ellas parecía ideal

Enfoques

Filtrado basado en modelo

  • decorar el TreeModel para filtrar algunos de los valores. Una versión rápida y sucia es fácil de escribir. Filtrar los nodos, y en cada cambio del filtro o el delegado TreeModel el decorador puede disparar un evento que todo el árbol tiene cambios (treeStructureChanged con el nodo raíz como nodo). Combine esto con oyentes que restablecen el estado de selección y el estado de expansión de la JTree y obtienes una versión que funciona más o menos, pero los eventos que se originan en TreeModel están en mal estado. Este es más o menos el enfoque utilizado en esta pregunta
  • decorar el TreeModel pero intenta disparar los eventos correctos. No (todavía) logré encontrar una versión funcional de esto. Parece requerir una copia del delegado TreeModel para poder activar un evento con los índices secundarios correctos cuando los nodos se eliminan del modelo de delegado. Creo que con algo más de tiempo podría hacer que esto funcione, pero se siente mal (el filtrado parece algo que la vista debería hacer, y no el modelo)
  • decorar cualquier estructura de datos utilizada para crear la inicial TreeModel. Sin embargo, esto es completamente no reutilizable, y probablemente tan difícil como escribir un decorador para un TreeModel

Ver el filtrado basado

Esto parece ser el camino a seguir. El filtrado no debe afectar el modelo sino solo la vista.

  • Eché un vistazo a RowFilter clase. Aunque el javadoc parece sugerir que puede usarlo en combinación con un JTree:

    cuando se asocia con un JTree, una entrada corresponde a un nodo.

    No pude encontrar ningún enlace entre RowFilter (o RowSorter) y el JTree clase. Las implementaciones estándar de RowFilter y los tutoriales de Swing parecen sugerir que RowFilter solo se puede usar directamente con un JTable (ver JTable#setRowSorter) No hay métodos similares disponibles en un JTree

  • También miré el JXTree javadoc. Tiene un ComponentAdapter disponible y el javadoc de ComponentAdapter indica una RowFilter podría interactuar con el componente de destino, pero no veo cómo hago el enlace entre el RowFilter y el JTree
  • Todavía no he visto cómo un JTable maneja el filtrado con RowFilters, y tal vez lo mismo se puede hacer en una versión modificada de JTree.

En resumen: no tengo idea de cuál es el mejor enfoque para resolver este problema

Nota: esta pregunta es un posible duplicado de esta pregunta, pero esa pregunta sigue sin respuesta, la pregunta es bastante breve y las respuestas parecen incompletas, así que pensé en publicar una nueva pregunta. Si esto no se hace (las preguntas frecuentes no dieron una respuesta clara al respecto) voy a actualizar esa pregunta de 3 años.


32
2018-02-10 20:19


origen


Respuestas:


El filtrado basado en la vista es definitivamente el camino a seguir. Puedes usar algo como el ejemplo que he codificado a continuación. Otra práctica común al filtrar árboles es cambiar a una vista de lista cuando filtre un árbol, ya que la lista no requerirá que muestre los nodos ocultos cuyos descendientes deben mostrarse.

Este es un código absolutamente horrendo (traté de cortar todos los rincones posibles para batirlo justo ahora), pero debería ser suficiente para comenzar. Simplemente escriba su consulta en el cuadro de búsqueda y presione Entrar, y se filtrará el modelo predeterminado de JTree. (Para su información, las primeras 90 líneas son solo un código repetitivo y un código de diseño).

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

}

Cuando lo implemente de manera real, probablemente quiera crear sus propias implementaciones TreeNode y TreeCellRenderer, usar un método menos estúpido para activar una actualización y seguir la separación de MVC. Tenga en cuenta que los nodos "ocultos" aún se representan, pero son tan pequeños que no puede verlos. Sin embargo, si usa las teclas de flecha para navegar por el árbol, notará que todavía están allí. Si solo necesitas algo que funcione, esto podría ser suficiente.

Filtered tree (windows)

Editar

Aquí hay capturas de pantalla de la versión sin filtro y filtrada del árbol en Mac OS, que muestra que el espacio en blanco es visible en Mac OS:

Unfiltered treeFiltered tree


7
2018-04-03 21:47



Echa un vistazo a esta implementación: http://www.java2s.com/Code/Java/Swing-Components/InvisibleNodeTreeExample.htm

Crea subclases de DefaultMutableNode agregando una propiedad "isVisible" en lugar de quitar / agregar nodos de TreeModel. Bastante dulce, creo, y resolvió perfectamente mi problema de filtrado.


6
2017-12-14 13:31



Pregunta anterior, me encontré con ... para todos aquellos que quieren una solución rápida y fácil de

SÓLO FILTRANDO LA VISIÓN:

Sé que no es tan limpio como Filtrar el modelo y viene con posibles backdraws, pero si solo quieres una solución rápida para una aplicación pequeña:

Extienda DefaultTableCellRenderer, anule getTreeCellRendererComponent - invoque a super.getTreeCellRendererComponent (...) y después de eso solo configure la Altura preferida en CERO para todos los Nodos que quiera ocultar. Al construir su JTree, asegúrese de configurar setRowHeight (0); - por lo que respetará la altura preferida de cada fila ...

voila - ¡todas las filas filtradas son invisibles!

EJEMPLO DE TRABAJO COMPLETO

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
2017-12-19 16:41



Aquí hay una posible solución que usa solo componentes Swing estándar:

No he usado esto todavía, pero me gusta su implementación mucho más que otras soluciones rápidas y sucias que encontré en línea.


1
2018-04-03 22:10



He estado trabajando en un workarround para filtrar una extensión JXTreeTable. He seguido el dos modelos enfoque para la simplicidad.

El modelo filtrado

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

Básicamente, la idea era conectar / desconectar nodos del modelo original a la raíz del modelo filtrado manteniendo un registro de la corriente familia nodos.

El modelo filtrado tendrá un mapeo entre niños y padres, con el método apropiado para restaurar esta familia. El método'restoreFamily' volverá a conectar esos niños desaparecidos.

El modelo filtrado hará la mayor parte del trabajo en su filter() método dejando el abstract método shouldBeFiltered(node) a las implementaciones:

Se debe tener en cuenta que no es necesario desconectar TODOS los niños de la familia antes de conectar los filtrados a la raíz filtrada. Si el rendimiento es clave, ese comportamiento podría analizarse más a fondo.

Extendiendo JXTreeTable

Por último, pero lo más importante, existe la necesidad de ampliar la tabla de árbol subyacente implementando un método y anulando otro:

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

Un ejemplo completo y funcional con una tabla de árbol se puede encontrar en este enlazar. Incluye un Main.java con un árbol listo para construir. La prueba GUI tiene un botón que agrega nodos en el nodo seleccionado (si corresponde) y en la parte superior del marco un campo de texto que se filtra mientras se escribe.


1
2017-07-02 16:09



Finalmente logré sacar algo que los trajes mis necesidades perfectamente, y pensé que compartiría en caso de que alguien más pueda usarlo.

He intentado mostrar dos JTrees uno al lado del otro, uno que contiene una lista filtrada del otro.

Básicamente, he creado dos TreeModels y uso el mismo nodo raíz para ambos. Esto parece funcionar bien hasta el momento, siempre y cuando me asegure de anular CADA método que se llama desde DefaultTreeModel en mi código, como nodeChanged (nodo TreeNode) o de lo contrario habrá dolor.

El problema viene del hecho de que la única vez que se consultan los nodos para obtener información, como childcount, es cuando se llaman a los métodos de tipo de estructura en DefaultTreeModel. Aparte de eso, todas las llamadas para información de estructura de árbol pueden ser interceptadas y filtradas como se muestra a continuación.

Esto puede volverse desagradable, ya que tienes que asegurarte de que excaves cada vez que se consultan los nodos si usas DefaultTreeModel como la base como lo hice. Este problema podría no estar allí si implementa TreeModel directamente en lugar de ser flojo como yo. La fuente de NodesChanged vino directamente de la fuente JDK.

Tuve la suerte de que lo que quería significaba que siempre había un camino de regreso al nodo raíz de cada elemento en la lista filtrada.

Esto era todo lo que tenía que hacer, aunque estuve todo el día probando inventos salvajes y caóticos, como recrear una copia superficial del árbol, etc., ¡sin mencionar leer muchos en 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);
        }
    }
}
}

1
2017-08-15 10:01



ETable, una subclase de JTable y la clase de padres Outline, descrito aquí, incluye "características de filtro rápido que permiten mostrar solo ciertas filas del modelo (ver setQuickFilter()). "Si bien esto viola el requisito de no" libs de terceros ", el OutlineJAR no tiene dependencias que no sean el JDK.


1
2018-06-04 11:47



Principio que utilicé: Llenar ArrayList de DB, y luego completar el árbol. Cuando tengo que filtrar nodos de árbol, simplemente itero a través de ArrayList, elimino todos los nodos que no coinciden con los criterios, y luego reconstruyo el árbol con ArrayList modificado ...


0
2018-06-11 13:00



Tengo una sugerencia para esto que puede ser de interés. Lo puse en práctica en mi propia aplicación y parece funcionar bien ... a continuación se muestra una implementación mínima absoluta de SSCCE que demuestra "insertNodeInto".

El diseño central es múltiples acoplamientos de J Tree TreeModel, que se mantienen sincronizados entre sí ... excepto, obviamente, que se aplica un patrón de filtrado para que ciertos nodos (y sus subárboles) no estén presentes en un modelo. Mientras tanto, cada nodo en el árbol ON tiene un nodo "homólogo" en el árbol DESACTIVADO (aunque el inverso no es necesariamente cierto).

El diseño más simple por lo tanto implica 2 de tales acoplamientos: uno con el filtro "OFF" y el otro con el filtro "ON" (por cierto, puede tener más de 1 filtro, por lo que necesita n ^ 2 acoplamientos, donde n es el número de filtros ... ¡y tengo esto funcionando!).

Para cambiar de un acoplamiento a otro (es decir, de ON a OFF o viceversa), simplemente sustituye un JTree por otro en el JViewport que lo contiene ... y esto sucede en un abrir y cerrar de ojos, completamente indesmallable. Entonces es como una ilusión óptica.

Por cierto, el filtro utilizado aquí es "does the toString () del nodo contiene la cadena 'nobble'"? (vea el método FilterPair.is_filtered_out)

Algunos podrían decir que tal idea sería ridículamente ineficiente en la memoria ... pero de hecho los nodos en los diferentes acoplamientos, aunque diferentes nodos, usan el mismo objeto de usuario ... así que sugiero que la estructura sea bastante liviana.

Mucho, mucho más difícil es lograr que la mecánica de dos acoplamientos (y mucho menos 4 u 8) se sincronice entre sí. A continuación, se muestra una implementación bastante completa de insertNodeInto ... pero muchos métodos de DefaultTreeModel, de JTree, y también en relación con la selección, requieren una gran cantidad de reflexión. P.ej. si la selección en el árbol (de filtro) DESACTIVADO está en un nodo que no tiene contraparte en el árbol ENCENDIDO (porque él o uno de sus antepasados ​​se ha filtrado), ¿dónde debe ir la selección en el árbol ENCENDIDO? He encontrado respuestas a todas estas preguntas, pero no hay espacio aquí para mostrarlas ...

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


  }
}

Un pensamiento más: ¿cómo generalizarlo para que FilterTreeModel extienda su propia subclase suministrada de DefaultTreeModel? (y en la implementación completa para que FilterJTree extienda su subclase JTree suministrada?). Escribí este código originalmente en Jython, ¡donde pasar una clase A como parámetro de una definición de clase B es trivial! Utilizando Java viejo y majestuoso, posiblemente se podría hacer con métodos de reflexión y de fábrica estáticos, o posiblemente mediante alguna ingeniosa técnica de encapsulación. Sin embargo, sería un duro trabajo. ¡Mejor cambiar a Jython si es posible!


0
2017-10-27 12:47



Una solución fue dada http://forums.sun.com/thread.jspa?forumID=57&threadID=5378510

Lo hemos implementado aquí, y funciona como un encanto.

Solo tiene que implementar TreeModel, y en el JTree use un FilterTreeModel con un TreeFilter.

La implementación es bastante simple, tal vez haya algunas cosas que hacer en el oyente, ya que el código real se llamará dos veces, lo que no es bueno en absoluto. Mi idea es simplemente pasar al oyente para delegar el modelo, no veo el punto de agregar oyente en el modelo de filtro ... Pero esa es otra pregunta.


-1
2018-01-03 19:10