JTree + JCheckBox + JLabel com ícone

1 resposta
MisaelJavaPJ

Bom noite a todos.

Estou precisando de uma JTree customizada onde seus nós são substituídos por controles checkbox e label's com ícone.

Vasculhando a net por ai, consegui obter alguns resultados interessantes, e até consegui implementar alguma coisa bem bacana.

Tenho um problema e uma dúvida:

Problema: quando a JTree é alimentada pelo modelo que crio, a JLabel com o ícone é truncada pelo JCheckBox e a String de dentro da JLabel também fica truncada, procurei substituir os layouts por outros, mas não tive sucesso.

A dúvida é referente a seleção dos nós, pois na ideia, pretendo ler os nós e exibir quais estão selecionados.

Segue os fontes do que tenho até agora:

Program para o fluxo principal

package br.com.misael;

import br.com.misael.controller.CtrJTree;

public class Program {

	public static void main(String[] args) {
		
		new CtrJTree();
		
	}
	
}

O Formulário.

package br.com.misael.view;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.border.EmptyBorder;

import br.com.misael.view.uc.JCheckBoxTree;
import javax.swing.JButton;
import javax.swing.JList;

public class FrmJTree extends JFrame {

	private static final long serialVersionUID = -3780261717671688258L;
	private JPanel pnl;
	private JScrollPane scrollPane1;
	private JScrollPane scrollPane2;
	private JCheckBoxTree tree;
	private JList<String> listCheckedNode;
	private JButton btnShowCheckedNodes;

	/**
	 * Create the frame.
	 */
	public FrmJTree() {
		setResizable(false);
		setTitle("Prototype JTree + JCheckBox + JLabel with icon");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setBounds(100, 100, 837, 557);
		pnl = new JPanel();
		pnl.setBorder(new EmptyBorder(5, 5, 5, 5));
		setContentPane(pnl);
		pnl.setLayout(null);

		scrollPane1 = new JScrollPane();
		scrollPane1
				.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
		scrollPane1.setBounds(10, 11, 405, 497);
		pnl.add(scrollPane1);

		tree = new JCheckBoxTree();
		scrollPane1.setViewportView(tree);

		btnShowCheckedNodes = new JButton("Show checked nodes");
		btnShowCheckedNodes.setBounds(425, 9, 396, 23);
		pnl.add(btnShowCheckedNodes);

		scrollPane2 = new JScrollPane();
		scrollPane2
				.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
		scrollPane2.setBounds(425, 43, 396, 465);
		pnl.add(scrollPane2);

		listCheckedNode = new JList<String>();
		scrollPane2.setViewportView(listCheckedNode);
		setVisible(true);
	}

	public JPanel getPnl() {
		return pnl;
	}

	public void setPnl(JPanel pnl) {
		this.pnl = pnl;
	}

	public JScrollPane getScrollPane1() {
		return scrollPane1;
	}

	public void setScrollPane1(JScrollPane scrollPane1) {
		this.scrollPane1 = scrollPane1;
	}

	public JScrollPane getScrollPane2() {
		return scrollPane2;
	}

	public void setScrollPane2(JScrollPane scrollPane2) {
		this.scrollPane2 = scrollPane2;
	}

	public JCheckBoxTree getTree() {
		return tree;
	}

	public void setTree(JCheckBoxTree tree) {
		this.tree = tree;
	}

	public JList<String> getListCheckedNode() {
		return listCheckedNode;
	}

	public void setListCheckedNode(JList<String> listCheckedNode) {
		this.listCheckedNode = listCheckedNode;
	}

	public JButton getBtnShowCheckedNodes() {
		return btnShowCheckedNodes;
	}

	public void setBtnShowCheckedNodes(JButton btnShowCheckedNodes) {
		this.btnShowCheckedNodes = btnShowCheckedNodes;
	}
}

Um controlador.

package br.com.misael.controller;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.util.Enumeration;

import javax.swing.DefaultListModel;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;

import br.com.misael.view.FrmJTree;

public class CtrJTree implements ActionListener, WindowListener {

	private FrmJTree frmJTree;
	
	public CtrJTree() {
		
		this.frmJTree = new FrmJTree();
		this.frmJTree.addWindowListener(this);
		this.frmJTree.getBtnShowCheckedNodes().addActionListener(this);
		
	}
	
	private void loadJTree() {
		
		DefaultMutableTreeNode _root = null;
		DefaultMutableTreeNode _node = null;
		DefaultMutableTreeNode _leaf = null;
		
		_root = new DefaultMutableTreeNode("DATA BASE NAME ");
		
		for(int n = 0; n < 3; n++) {
			
			_node = new DefaultMutableTreeNode("TABLE NAME " + (n + 1) );
			
			for(int f = 0; f < 5; f++) {
				
				_leaf = new DefaultMutableTreeNode("COLUMN NAME " + (f + 1));
				_node.add(_leaf);
				
			}
			
			_root.add(_node);
			
		}
		
		DefaultTreeModel defaultTreeModel = new DefaultTreeModel(_root);
		this.frmJTree.getTree().setModel(defaultTreeModel);
		this.frmJTree.getScrollPane1().setViewportView(this.frmJTree.getTree());
		this.frmJTree.getTree().repaint();
		
	}
	
	private void showCheckedNode() {
		
		DefaultListModel<String> listContent = new DefaultListModel<String>();
		this.frmJTree.getListCheckedNode().setModel(listContent);
		
		// Recupera o nome do nó raiz do modelo da JTree.
		listContent.addElement( this.frmJTree.getTree().getModel().getRoot().toString() );
		
		// O nome da classe
		// conteudoLista.addElement( this.frmJTree.getTree().getModel().getRoot().getClass().getName() );
		
		DefaultMutableTreeNode no =  (DefaultMutableTreeNode)this.frmJTree.getTree().getModel().getRoot();

		for(Enumeration<?> e = no.children(); e.hasMoreElements(); ) {
			
			// Navega para o próximo
			DefaultMutableTreeNode noChild = (DefaultMutableTreeNode) e.nextElement();
			
			// Imprime o nome na JList
			listContent.addElement( "    " + noChild.getUserObject().toString() );
			
			// Itera sobre os nós filhos dos filhos do nó raiz.
			for(Enumeration<?> en = noChild.children(); en.hasMoreElements(); ) {
				
				DefaultMutableTreeNode noChildChild = (DefaultMutableTreeNode) en.nextElement();
				listContent.addElement( "        " + noChildChild.getUserObject().toString() );
				
			}
		}
		
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		
		if(e.getSource() == this.frmJTree.getBtnShowCheckedNodes()) {
			
			this.showCheckedNode();
			
		}
		
	}

	@Override
	public void windowActivated(WindowEvent e) {
		
	}

	@Override
	public void windowClosed(WindowEvent e) {
	
	}

	@Override
	public void windowClosing(WindowEvent e) {

	}

	@Override
	public void windowDeactivated(WindowEvent e) {
	
	}

	@Override
	public void windowDeiconified(WindowEvent e) {
		
	}

	@Override
	public void windowIconified(WindowEvent e) {
		
	}

	@Override
	public void windowOpened(WindowEvent e) {
		
		this.loadJTree();
		
	}
	
}

E o principal: o modelo de componente JTree customizado!
Eu pegue esse fonte http://stackoverflow.com/questions/21847411/java-swing-need-a-good-quality-developed-jtree-with-checkboxes, e fiz pequenas modificações, inserindo a JLabel com o ícone, dei uma estudada no fonte mas não consegui resolver ainda a questão do truncamento das Label’s.

package br.com.misael.view.uc;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.InputStream;
import java.util.EventListener;
import java.util.EventObject;
import java.util.HashMap;
import java.util.HashSet;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.event.EventListenerList;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;

/**
 * Original implementation from http://stackoverflow.com/questions/21847411/java-swing-need-a-good-quality-developed-jtree-with-checkboxes
 * @author SomethingSomething
 *
 */
public class JCheckBoxTree extends JTree {

    private static final long serialVersionUID = -4194122328392241790L;

    JCheckBoxTree selfPointer = this;

    // Defining data structure that will enable to fast check-indicate the state of each node
    // It totally replaces the "selection" mechanism of the JTree
    private class CheckedNode {
    	
        boolean isSelected;
        boolean hasChildren;
        boolean allChildrenSelected;

        public CheckedNode(boolean isSelected_, boolean hasChildren_, boolean allChildrenSelected_) {
        	
            isSelected          = isSelected_;
            hasChildren         = hasChildren_;
            allChildrenSelected = allChildrenSelected_;
            
        }
    }
    
    HashMap<TreePath, CheckedNode> nodesCheckingState;
    HashSet<TreePath> checkedPaths = new HashSet<TreePath>();

    // Defining a new event type for the checking mechanism and preparing event-handling mechanism
    protected EventListenerList listenerList = new EventListenerList();

    public class CheckChangeEvent extends EventObject {
    	
        private static final long serialVersionUID = -8100230309044193368L;

        public CheckChangeEvent(Object source) {
            super(source);          
        }
        
    }   

    public interface CheckChangeEventListener extends EventListener {
        public void checkStateChanged(CheckChangeEvent event);
    }

    public void addCheckChangeEventListener(CheckChangeEventListener listener) {
        listenerList.add(CheckChangeEventListener.class, listener);
    }
    public void removeCheckChangeEventListener(CheckChangeEventListener listener) {
        listenerList.remove(CheckChangeEventListener.class, listener);
    }

    void fireCheckChangeEvent(CheckChangeEvent evt) {
    	
        Object[] listeners = listenerList.getListenerList();
        
        for (int i = 0; i < listeners.length; i++) {
        	
            if (listeners[i] == CheckChangeEventListener.class) {
                ((CheckChangeEventListener) listeners[i + 1]).checkStateChanged(evt);
            }
            
        }
        
    }

    // Override
    public void setModel(TreeModel newModel) {
        super.setModel(newModel);
        resetCheckingState();
    }

    // New method that returns only the checked paths (totally ignores original "selection" mechanism)
    public TreePath[] getCheckedPaths() {
        return checkedPaths.toArray(new TreePath[checkedPaths.size()]);
    }

    // Returns true in case that the node is selected, has children but not all of them are selected
    public boolean isSelectedPartially(TreePath path) {
        CheckedNode cn = nodesCheckingState.get(path);
        return cn.isSelected && cn.hasChildren && !cn.allChildrenSelected;
    }

    private void resetCheckingState() { 
        
    	nodesCheckingState          = new HashMap<TreePath, CheckedNode>();
        checkedPaths                = new HashSet<TreePath>();
        DefaultMutableTreeNode node = (DefaultMutableTreeNode)getModel().getRoot();
        
        if (node == null) {
            return;
        }
        
        addSubtreeToCheckingStateTracking(node);
    }

    // Creating data structure of the current model for the checking mechanism
    private void addSubtreeToCheckingStateTracking(DefaultMutableTreeNode node) {
    	
        TreeNode[] path = node.getPath();   
        TreePath tp     = new TreePath(path);
        CheckedNode cn  = new CheckedNode(false, node.getChildCount() > 0, false);
        nodesCheckingState.put(tp, cn);
        
        for (int i = 0 ; i < node.getChildCount() ; i++) {              
            addSubtreeToCheckingStateTracking((DefaultMutableTreeNode) tp.pathByAddingChild(node.getChildAt(i)).getLastPathComponent());
        }
    }

    // Overriding cell renderer by a class that ignores the original "selection" mechanism
    // It decides how to show the nodes due to the checking-mechanism
    private class CheckBoxCellRenderer extends JPanel implements TreeCellRenderer {     
        
    	private static final long serialVersionUID = -7341833835878991719L;     
        JCheckBox checkBox;
        JLabel    label;
        
        public CheckBoxCellRenderer() {
        	
            super();
            this.setLayout(new BorderLayout());
            checkBox = new JCheckBox();
            
            // MISAEL: Insert a JLabel statement.
            label    = new JLabel();
            
            add(checkBox, BorderLayout.WEST);

            // MISAEL: Add on JPanel.
            add(label, BorderLayout.EAST);
            setOpaque(false);
            
        }

        @Override
        public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
        	
            DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
            Object obj                  = node.getUserObject();
            TreePath tp                 = new TreePath(node.getPath());
            CheckedNode cn              = nodesCheckingState.get(tp);
            
            if (cn == null) {
                return this;
            }
          
            // MISAEL - my change here, insert labels wiht icons!
            // Insert this code block. 
            try {
	        	
		        if(node.getLevel() == 0) {
		        	InputStream inputStream = JCheckBoxTree.class.getResourceAsStream("/br/com/misael/view/uc/icons/database.png");
		        	label.setIcon(new ImageIcon( ImageIO.read(inputStream) ));
		        }
		        
		        if(node.getLevel() == 1) {
		        	InputStream inputStream = JCheckBoxTree.class.getResourceAsStream("/br/com/misael/view/uc/icons/application_view_columns.png");
		        	label.setIcon(new ImageIcon( ImageIO.read(inputStream) ));
		        }
		        
		        if(node.getLevel() == 2) {
		        	InputStream inputStream = JCheckBoxTree.class.getResourceAsStream("/br/com/misael/view/uc/icons/bullet_green.png");
		        	label.setIcon(new ImageIcon( ImageIO.read(inputStream) ));
		        }
	        
	        } catch(Exception e) {
	        	System.err.println(e.getMessage());
	        }
            // MISAEL: End code block.
            
	        checkBox.setSelected(cn.isSelected);
	        
	        // MISAEL: Remove from original.
            // checkBox.setText(obj.toString());
	        
	        // MISAEL: Set text of label.
	        label.setText(obj.toString());
            checkBox.setOpaque(cn.isSelected && cn.hasChildren && ! cn.allChildrenSelected);
	        
            return this;
        }       
    }

    public JCheckBoxTree() {
    	
        super();
        // Disabling toggling by double-click
        this.setToggleClickCount(0);
        // Overriding cell renderer by new one defined above
        CheckBoxCellRenderer cellRenderer = new CheckBoxCellRenderer();
        this.setCellRenderer(cellRenderer);

        // Overriding selection model by an empty one
        DefaultTreeSelectionModel dtsm = new DefaultTreeSelectionModel() {
        	
            private static final long serialVersionUID = -8190634240451667286L;
            // Totally disabling the selection mechanism
            public void setSelectionPath(TreePath path) {}           
            public void addSelectionPath(TreePath path) {}           
            public void removeSelectionPath(TreePath path) {}
            public void setSelectionPaths(TreePath[] pPaths) {}
            
        };
        
        // Calling checking mechanism on mouse click
        this.addMouseListener(new MouseListener() {
        	
            public void mouseClicked(MouseEvent e) {
            	
                TreePath tp = selfPointer.getPathForLocation(e.getX(), e.getY());
                
                if (tp == null) {
                    return;
                }
                
                boolean checkMode = ! nodesCheckingState.get(tp).isSelected;
                checkSubTree(tp, checkMode);
                updatePredecessorsWithCheckMode(tp, checkMode);
                // Firing the check change event
                fireCheckChangeEvent(new CheckChangeEvent(new Object()));
                // Repainting tree after the data structures were updated
                selfPointer.repaint();                          
            }           
            public void mouseEntered(MouseEvent e) {}
            public void mouseExited(MouseEvent e) {}
            public void mousePressed(MouseEvent e) {}
            public void mouseReleased(MouseEvent e) {}
        });
        
        this.setSelectionModel(dtsm);
    }

    // When a node is checked/unchecked, updating the states of the predecessors
    protected void updatePredecessorsWithCheckMode(TreePath tp, boolean check) {
    	
        TreePath parentPath = tp.getParentPath();
        
        // If it is the root, stop the recursive calls and return
        if (parentPath == null) {
            return;
        }
        
        CheckedNode parentCheckedNode         = nodesCheckingState.get(parentPath);
        DefaultMutableTreeNode parentNode     = (DefaultMutableTreeNode) parentPath.getLastPathComponent();     
        parentCheckedNode.allChildrenSelected = true;
        parentCheckedNode.isSelected          = false;
        
        for (int i = 0 ; i < parentNode.getChildCount() ; i++) {
        	
            TreePath childPath = parentPath.pathByAddingChild(parentNode.getChildAt(i));
            CheckedNode childCheckedNode = nodesCheckingState.get(childPath);           
            // It is enough that even one subtree is not fully selected
            // to determine that the parent is not fully selected
            
            if (! childCheckedNode.allChildrenSelected) {
                parentCheckedNode.allChildrenSelected = false;      
            }
            
            // If at least one child is selected, selecting also the parent
            if (childCheckedNode.isSelected) {
                parentCheckedNode.isSelected = true;
            }
        }
        
        if (parentCheckedNode.isSelected) {
            checkedPaths.add(parentPath);
        } else {
            checkedPaths.remove(parentPath);
        }
        // Go to upper predecessor
        updatePredecessorsWithCheckMode(parentPath, check);
    }

    // Recursively checks/unchecks a subtree
    protected void checkSubTree(TreePath tp, boolean check) {
    	
        CheckedNode cn              = nodesCheckingState.get(tp);
        cn.isSelected               = check;
        DefaultMutableTreeNode node = (DefaultMutableTreeNode) tp.getLastPathComponent();
        
        for (int i = 0 ; i < node.getChildCount() ; i++) {              
            checkSubTree(tp.pathByAddingChild(node.getChildAt(i)), check);
        }
        
        cn.allChildrenSelected = check;
        
        if (check) {
            checkedPaths.add(tp);
        } else {
            checkedPaths.remove(tp);
        }
    }

}

Att,

Misael C. HOmem

1 Resposta

MisaelJavaPJ

Pessoal,

A parte de renderização dos ícones e ajuste com a JCheckBox eu já consegui (posto o código posteriormente), mas estou pesquisando a questão de capturar os DefaultMutableTreeNode cujas JCheckBoxs estão checcked, não sei se na abordagem que está isto é possível.
Se alguém puder ajudar, ou já tenha implementado algo parecido pra ajudar, fico grato.

Att,

Misael C. Homem

Criado 18 de julho de 2015
Ultima resposta 21 de jul. de 2015
Respostas 1
Participantes 1