I am trying to create a table with custom column headers. I want the column headers to include a button that users can click on. The function of the button will be to remove the column from the table. Essentially, I am trying to build something like this.
Here's my code:
public class CustomColumnHeadersTable {
private static String[] columnNames = {
"Column 1", "Column 2", "Column 3"
};
private static String[][] data = {
{"A", "B", "C"},
{"D", "E", "F"},
{"G", "H", "I"}
};
public CustomColumnHeadersTable() {
DefaultTableModel model = new DefaultTableModel((Object[][]) data, columnNames);
JTable table = new JTable(model);
JScrollPane scrollPane = new JScrollPane(table);
//set Header Renderer of each column to use the Custom renderer
Enumeration enumeration = table.getColumnModel().getColumns();
while (enumeration.hasMoreElements()) {
TableColumn aColumn = (TableColumn) enumeration.nextElement();
aColumn.setHeaderRenderer(new CustomColumnCellRenderer());
}
JFrame frame = new JFrame();
frame.getContentPane().add(scrollPane, BorderLayout.CENTER);
frame.setPreferredSize(new Dimension(300, 150));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
public static void main(String[] args) {
CustomColumnHeadersTable ccht = new CustomColumnHeadersTable();
}
}
class CustomColumnCellRenderer implements TableCellRenderer {
private static String iconURL = "http://www.accessdubuque.com/images/close_icon.gif";
//using a URL for the icon, so I don't have to upload the icon with the question
private static Dimension buttonSize = new Dimension(16, 16);
private static Dimension buttonBoxSize = new Dimension(16, 16);
private static Border panelBorder = BorderFactory.createRaisedBevelBorder();
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
JPanel panel = new JPanel();
JLabel label = new JLabel();
JButton button = new JButton();
Box buttonBox = Box.createHorizontalBox();
BorderLayout layout = new BorderLayout();
label.setText(table.getColumnName(column));
try { button.setIcon(new ImageIcon(new URL(iconURL))); }
catch (MalformedURLException ex) {
Logger.getLogger(CustomColumnCellRenderer.class.getName()).log(Level.SEVERE, null, ex);
}
//set size of the button and it's box
button.setMaximumSize(buttonSize);
button.setSize(buttonSize);
button.setPreferredSize(buttonSize);
buttonBox.setMaximumSize(buttonBoxSize);
buttonBox.setSize(buttonBoxSize);
buttonBox.setPreferredSize(buttonBoxSize);
button.addMouseListener(new CustomMouseListener()); //doesn't work...
buttonBox.add(button);
panel.add(label, BorderLayout.CENTER);
panel.add(buttonBox, BorderLayout.EAST);
panel.setBorder(panelBorder);
return panel;
}
}
class CustomMouseListener implements MouseListener
{
public void mouseClicked(MouseEvent e) { System.out.println("Mouse Clicked."); }
public void mousePressed(MouseEvent e) { System.out.println("Mouse Pressed."); }
public void mouseReleased(MouseEvent e) { System.out.println("Mouse Released."); }
public void mouseEntered(MouseEvent e) { System.out.println("Mouse Entered."); }
public void mouseExited(MouseEvent e) { System.out.println("Mouse Exited."); }
}
By default, if I understand correctly, JTable uses a JLabel to render column headers. My idea is to use a custom TableCellRenderer implementation, and build my own column header out of several components, namely, a JPanel that contains a JLabel and a JButton. I build and return that in getTableCellRendererComponent(...) function.
Visually, this works. The problem is that I cannot detect mouse clicks on the button (or, for that matter, on the panel that holds it). Simply adding a MouseListener to the button does not work. The event never reaches it.
I found several similar things on the web, but they do not achieve the functionality I need.
First, there's an example of how to put a JCheckBox into the header, here:
http://java-swing-tips.blogspot.com/2009/02/jtableheader-checkbox.html
The problem with this is that the entire header is the checkbox. Clicking on the checkbox or on the associated label produces the same effect. Thus, it's not possible to sort the column. I'd like to make it so that clicking on the label sorts the column, and clicking on the close button removes the column from the table. In other words, the header needs to have two separate areas with separate mouse event handlers.
I found another example here:
http://www.devx.com/getHelpOn/10MinuteSolution/20425/1954?pf=true
This involves placing JButtons into the cells of the table, then detecting mouse clicks on the table itself, calculating the column and row where the click occurred, and dispatching the event to the appropriate button.
The are several problem with this, too. First, buttons are in the cells, not in the headers. And second, this is again just one component, not several components inside a JPanel. Although I got the idea of dispatching events from this example, I cannot make it work for a composite component.
I tried another approach. I reasoned that if I can get the coordinates of the close buttons, then knowing the coordinates of the mouse click I can calculate which button was clicked, and dispatch the event appropriately. I ran several tests, and discovered that components inside the table header are not actually located on the screen.
I added a static JButton variable to my main (public) class, and made the class that implements TableCellRenderer an inner class of the main class. In getTableCellRendererComponent(...), prior to returning, I assign the JButton I just created to that static variable. This way, I can get a handle on it, so to speak. Then, in main, I tried to getX(), getY(), getWidth(), getHeight(), and getLocationOnScreen() using that static variable. X, Y, Width and Height all return 0's. GetLocationOnScreen() crashes the program, saying that the component must be present on the screen for this function to work.
The code for this looks something like this:
public class CustomColumnHeadersTable {
private static JButton static_button;
///the rest as before....
"class CustomColumnCellRenderer implements TableCellRenderer" becomes an inner class of CustomColumnHeadersTable. For that, I have to give up static variables in CustomColumnCellRenderer, so I did not bother with icons or url's or anything like that. Instead of a button with an icon, I just used a simple button that said "BUTTON"...
Next, inside getTableCellRendererComponent(...), just before the return statement, I did
static_button = button;
And then finally, inside main(), I tried doing this:
System.out.println("X: " + static_button.getX());
System.out.println("Y: " + static_button.getY());
System.out.println("W: " + static_button.getWidth());
System.out.println("H: " + static_button.getHeight());
System.out.println("LOC: " + static_button.getLocation());
System.out.println("LOC on screen: " + static_button.getLocationOnScreen());
The output looks like this:
X: 0
Y: 0
W: 0
H: 0
LOC: java.awt.Point[x=0,y=0]
Exception in thread "main" java.awt.IllegalComponentStateException: component must be showing on the screen to determine its location
at java.awt.Component.getLocationOnScreen_NoTreeLock(Component.java:1943)
at java.awt.Component.getLocationOnScreen(Component.java:1917)
...
In other words, the button has all dimensions of 0, and according to Java it is not actually located on the screen (even though I can see it...). Calling getLocationOnScreen() crashes the program.
So, please help if you can. Maybe someone knows how to do this. Maybe you could just suggest some other approach to try out. Or, maybe you know it's not possible at all...
Thank you for your help.