JTree + JCheckBox + JLabel com ícone

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

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