Tuesday, 20 August 2013

Swing TreeTable Example using JXTreeTable

This post provides an example using JXTreeTable which is a tree-table component in SwingX. SwingX is an open source Swing extension toolkit from SwingLabs.

Most of the examples using tree-table that we find usually have only one type of entity - for example, showing a 'folder' view is most common. But, in this case, the file and the folder share some common traits and would have a IS-A relationship (from an OOPS perspective). However, in a real scenario, we may have to deal with objects that are not directly related.

For example, think of a Department object which has a list of Employee objects. These may share a relationship at the db level, but it will be a HAS-A relationship. So, in a tree-table component, the parent is one type of object and child (leaf) is another. This leads to handling of different conditions in most methods. Added to this, we might have a completely different object represent the root component - say, an Organization object.

However, for the moment, let us deal with an example where the root is not shown. This example displays the departments in an organization. Under each department, there may be n number of employees working.

Let us first create two classes representing the employee and department:

public class Employee {

    private int id;
    private String name;
    private Date doj;
    private String photo;

    public Employee(int id, String name, Date doj, String photo) {
        this.id = id;
        ...
    }
    //setters and getters not shown for brevity
}

public class Department {

    private int id;
    private String name;
    private List<Employee> employeeList;

    public Department(int id, String name, List<Employee> empList) {
        this.id = id;        
        ...
    }

    public List<Employee> getEmployeeList() {
        return employeeList;
    }

    public void setEmployeeList(List<Employee> employeeList) {
        this.employeeList = employeeList;

    }

    //other setters and getters
}

Let us now write the tree-table model. We need to extend the org.jdesktop.swingx.treetable.AbstractTreeTableModel and override the methods (this is an abstract implementation of org.jdesktop.swingx.treetable.TreeTableModel):

import java.util.List;
import org.jdesktop.swingx.treetable.AbstractTreeTableModel;

public class NoRootTreeTableModel extends AbstractTreeTableModel {
    private final static String[] COLUMN_NAMES = {"Id", "Name", "Doj", "Photo"};
    
    private List<Department> departmentList;

    public NoRootTreeTableModel(List<Department> departmentList) {
        super(new Object());
        this.departmentList = departmentList;
    }

    @Override
    public int getColumnCount() {
        return COLUMN_NAMES.length;
    }

    @Override
    public String getColumnName(int column) {
        return COLUMN_NAMES[column];
    }
    
    @Override
    public boolean isCellEditable(Object node, int column) {
        return false;
    }

    @Override
    public boolean isLeaf(Object node) {
        return node instanceof Employee;
    }
    ...

}

The treetable is going to show a list of departments. So, in our implementation of the model, we declare a constructor that takes a List<Department>. The getColumnCount(), getColumnName() and isCellEditable() implementations are same as we do for a JTable. The isLeaf() method is implemented for trees. This method should return a boolean to indicate whether the node is a leaf. In our case, the employee objects should be displayed as leaf, so, we simply do an instanceof check on the Employee object. Note the usage of a dummy object to indicate the root.

Let us continue with other methods:

    @Override
    public int getChildCount(Object parent) {
        if (parent instanceof Department) {
            Department dept = (Department) parent;
            return dept.getEmployeeList().size();
        }
        return departmentList.size();
    }

    @Override
    public Object getChild(Object parent, int index) {
        if (parent instanceof Department) {
            Department dept = (Department) parent;
            return dept.getEmployeeList().get(index);
        }
        return departmentList.get(index);
    }

The getChildCount() and getChild() methods are similar to what we do for a JTree model. The getChildCount() should return the number of departments first. In case the parent is a department itself, it should return the number of employees present in the department. So, we handle this with an if condition. 

Same way, in the getChild() implementation, we check if the passed in node is an instance of Department object. If so, we return an Department object by getting if from the list with the index number. If not, Employee object is returned. Note that, the getChild() and getChildCount() methods are closely related.

Next is the getIndexOfChild() method which is a bit tricky. The getIndexofChild() method should return the index of an Employee object within a Department object.

    @Override
    public int getIndexOfChild(Object parent, Object child) {
        Department dept = (Department) parent;
        Employee emp = (Employee) child;
        return dept.getEmployeeList().indexOf(emp);
    }

Finally, the most important method:

    @Override
    public Object getValueAt(Object node, int column) {
        if (node instanceof Department) {
            Department dept = (Department) node;
            switch (column) {
                case 0:
                    return dept.getId();
                case 1:
                    return dept.getName();
            }
        } else if (node instanceof Employee) {
            Employee emp = (Employee) node;
            switch (column) {
                case 0:
                    return emp.getId();
                case 1:
                    return emp.getName();
                case 2:
                    return emp.getDoj();
                case 3:
                    return emp.getPhoto();
            }
        }
        return null;
    }

The getValueAt() method is the one that returns the value of every cell in a JTable. Note that the JXTreeTable extends JTable, so correct implementation of this method is important.

In our case, the node might be a Department or Employee. So, an instanceof check is first applied on the node. Then, based on the column number, the corresponding method is called (this is similar to what we do for JTable). Note that the Department has only 2 columns of data to display and the Employee 4. The final "return null" statement takes care of the missing columns.

Let us now use this model by building our GUI (I have used the current time for the doj of all the records as this is just a demonstration):

import java.util.*;
import javax.swing.*;
import org.jdesktop.swingx.JXTreeTable;

public class TreeTableTest extends JFrame {

    private JXTreeTable treeTable;

    public TreeTableTest() {
        //sample doj
        final Date doj = Calendar.getInstance().getTime();        
        List<Department> departmentList = new ArrayList<Department>();

        //create and add the first department with its list of Employee objects
        List<Employee> empList1 = new ArrayList<Employee>();
        empList1.add(new Employee(1, "Kiran", doj, "emp1.jpg"));
        empList1.add(new Employee(2, "Prabhu", doj, "emp2.jpg"));
        empList1.add(new Employee(3, "Murugavel", doj, "emp1.jpg"));        
        departmentList.add(new Department(1, "Sales", empList1));

        //create and add the second department with its list of Employee objects
        List<Employee> empList2 = new ArrayList<Employee>();
        empList2.add(new Employee(4, "Deiveegan", doj, "emp2.jpg"));
        empList2.add(new Employee(5, "Saravanan", doj, "emp1.jpg"));
        departmentList.add(new Department(2, "Production", empList2));
        
        //we use a no root model
        NoRootTreeTableModel noRootTreeTableModel = new NoRootTreeTableModel(departmentList);
        treeTable = new JXTreeTable(noRootTreeTableModel);
        treeTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);        
        treeTable.setRootVisible(false);  // hide the root

        add(new JScrollPane(treeTable));

        setTitle("JXTreeTable Example");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        pack();
        setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {        
                new TreeTableTest();
            }
        });
    }
}

When we run the example and expand both the department nodes, we get an output like:

Note that the doj and the photo column for department row(s) are empty. This is a practical example of a tree-table.

Edit: As per the request of some readers, am providing code for showing an image. The Employee class has a member 'photo' which has the name of the image of the employee. Currently, it shows only as a text value. To show the photo as an actual image, we need to write an renderer. This is similar to what we do for JTable:

public class PhotoRenderer extends JLabel
                           implements TableCellRenderer {
    public Component getTableCellRendererComponent(
                            JTable table, Object photo,
                            boolean isSelected, boolean hasFocus,
                            int row, int column) {
        if(photo != null) {
            ImageIcon imageIcon = ...;
            setIcon(imageIcon);
        }
        else {
            setIcon(null);
        }
        return this;
    }
}

This renderer extends JLabel as we can set an icon for a JLabel. So, we can freely call the setIcon method of the JLabel to set the icon.

Next, we need to set this renderer to the particular column, like:
treeTable.getColumnModel().getColumn(3).setCellRenderer(new PhotoRenderer());

As the photo column is the 4h column, I am using the index 3 to get the TableColumn and set the renderer. When I run the code, I get the following output (I used some dummy images for photos):



Although the image is successfully displayed, it is not sufficient as we are not able to see the full image. To correct this, we need to increase the 'row height' of the table, like:
treeTable.setRowHeight(50);

Now, when I run the program, I get the following output:


The complete source code is available in the form of a Maven project in my github repo.

In a subsequent post, I have shown a similar example with root being visible (for example root represented by an Organization).

30 comments:

  1. Very Nice work worth to visit...
    Thanks man for help.....

    How to add Image in particular field.. suppose you have one more column name as image ..
    and in row how it is display ?

    pls tell me my email id is khatri.arpit@gmail.com

    ReplyDelete
    Replies
    1. Thanks Arpit.

      Adding an image is very similar to how you do for a table. Suppose you have a 'photo' member in the Employee class which you want to display here in the 4th column, you need to make appropriate changes in the model. And then, you have to write a table cell renderer. And then attach it to the table column.

      btw, I posted a follow-up article with sample to display the root of the tree.

      Delete
    2. Can you write code for showing image in 4th column

      Delete
  2. Hi Everyone,
    I have updated the post as per requests to include and show an image.

    ReplyDelete
  3. Hello! Can you send me the .java file working? I have adapted your code to mine; i have a recursive table (in mysql) where i have sections and subsections. I dont know if i should declare 2 differents classes to make it work, given that i use just one class: "Section", and, i differentiate sections from sub-sections through a field: "parent", because the treetable doesnt show the information that i'm consulting on my database. All it's fine, data retrieved, the implementation on the class it's good; but the treetable doesnt show nothing. My email: jcvasquezc@gmail.com

    ReplyDelete
  4. How can we do recursive roots .. i.e. departments inside departments

    ReplyDelete
    Replies
    1. Hi Iftekhar,
      There are two approaches. You can have another POJO named SubDepartment which will represent the sub departments and modify the hierarchy accordingly. Second approach will be to indicate this with a flag which will be much harder to code.

      In my other post I have shown the first approach with an object Organization which holds a list of departments - you can check that out here: http://javanbswing.blogspot.in/2013/09/swing-treetable-example-with-root-using.html

      Delete
  5. Hello Ranga,
    Thanks for the great job but how I can contact you directly ?

    ReplyDelete
  6. Thanks a lot... time spent delivering this tutorial well appreciated.

    ReplyDelete
  7. Nice article. Nice example. Good work.

    ReplyDelete
  8. It is a very useful post for me. Thanks for sharing it. Let's say Employee class holds a List of Employees(like Employee working under them) , we should add another list of Employee nodes to specific Employee node, right? Can we do it by a recursive call within the getValueAt() method. Can you help me understand how to do this?

    ReplyDelete
    Replies
    1. Thank you very much. Regarding your question, I think you can take a look at my other post with a root: http://javanbswing.blogspot.in/2013/09/swing-treetable-example-with-root-using.html

      Delete
  9. Thank you very much!
    Helped me a lot!


    Sent from Brazi...

    ReplyDelete
  10. Hello!
    It is possible to place a Checkbox in place of the image?

    ReplyDelete
    Replies
    1. Adding a checkbox is very simple in Swing. The datatype needs to be a Boolean and you need to override the getColumnClass() method in the model to return a 'Boolean.class' for that particular column. Swing will do the rest by automatically showing a checkbox. The same will work for JXTreeTable also.

      Delete
  11. thanks a lot for the easy explanation and demo

    ReplyDelete
  12. how to update treetable in runtime

    ReplyDelete
    Replies
    1. Please help in this.. am struck..!! am getting data from database to treetable. when the new data is added,it is not showing on refresh..but when i close the application and reopen it is showing

      Delete
    2. This is similar to how its done on JTable (when we need to update the view, we would call the fireXXX methods).
      Likewise, we can iterate over the listeners in the TreeModel and call the treeXXX methods.
      Alternatively, you can call the valueForPathChanged method on the model.

      Delete
    3. thank you so much,can you please give me a sample for your code? i have created jxtreetable using your code only. please help little bit in this case

      Delete
  13. Please sir! Can you give me some idea for filter Treetable please

    ReplyDelete
    Replies
    1. You should add a method on the model through which you can set the filtered data.

      Delete