JTable with a “close” button in the column header
Asked Answered
T

3

8

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.

Trillby answered 10/2, 2011 at 18:44 Comment(0)
H
2

Yes, I was suprised the first time I tried to do something similar, like putting JButtons into a JList - it doesn't quite work for the reasons you stated. The correct way to do what I wanted to use a list-like LayoutManager to place the JButtons in a list.

In your case, I would try to use a combination of JLabels and MouseListeners, which I believe will work correctly. Using a JLabel as your header will allow you provide both an image and text, and you can register a MouseListener on the JLabel. Granted, you won't get the same visual effects as you would when you click on a JButton (i.e. the button depresses), but it'll allow for the same functionality.

Another thing to watch out for is using static JButtons (or any JComponent). The problem with doing this is that the coordinates of the JButton are stored in the JButton itself, which means you can't use the same JButton in different places. In other words, if you try adding the same static JButton to a list multiple times, you won't see multiple JButtons, you'll just see the JButton in the location that you last added it. The proper way to achieve your desired effect is to instead keep a static reference to the contents of the JButton - namely the image and text - instead of the JButton itself. Then, just instantiate a new JButton using the image and text and place it wherever you want. In general, it's best to stay away from keep static references toSwing components as it makes it impossible to have multiple instances of the same component.

Swing ain't so swinging sometimes. Hopefully I've provided some useful information - keep fighting the good fight!

Hormuz answered 10/2, 2011 at 19:48 Comment(1)
Thank you! I tried something similar -- decided to just calculate if the clicks fall on the button. For that, I need to keep the button in a single place. Say, it's 16x16, and I need to always keep it 8 pixels to the left of the column edge. Although I was able to do that using Boxes and glue, I was not able to get it to play well with the JLabel. Say, my column looks like this: | Column name [x] |. If the column gets re-sized, I'd like it to look like | Colum...[x] |. IOW, the ellipses should appear automatically. Instead, the button just paints over the label. Any ideas?Trillby
A
1

You can use JXTable (from swingx palette) which have that option to 'disable' a particular column from your table.

Andrei Ionut Apopei

Ade answered 2/8, 2012 at 13:14 Comment(0)
I
0

I got the solution:

public class CustomColumnHeadersTable {

    public static Vector<JButton> buttons =  new Vector<JButton>();

    private static String[] columnNames = {
            "Column 1", "Column 2", "Column 3"
    };
    private static String[][] data = {
            {"A", "B", "C"},
            {"D", "E", "F"},
            {"G", "H", "I"}
    };

    class ColumnHeaderListener extends MouseAdapter {
        public void mouseClicked(MouseEvent evt) {
            JTable table = ((JTableHeader) evt.getSource()).getTable();
            TableColumnModel colModel = table.getColumnModel();

            int index = colModel.getColumnIndexAtX(evt.getX());
            if (index == -1) {
                return;
            }
            Rectangle headerRect = table.getTableHeader().getHeaderRect(index);

            if( 1== index){
                if(headerRect.contains(evt.getX() , evt.getY())){
                    int xx = evt.getX() - headerRect.x ;

                    for (int i = 0; i < buttons.size(); i++) {
                        JButton  button = buttons.get(i);
                        Rectangle re = button.getBounds();
                        if(re.contains( xx, evt.getY())){
                            System.out.println("Bingle");
                        }

                    }
                }
            }
        }
    }

    public CustomColumnHeadersTable() {
        DefaultTableModel model = new DefaultTableModel((Object[][]) data, columnNames);
        JTable table = new JTable(model);
        JScrollPane scrollPane = new JScrollPane(table);

        JTableHeader header = table.getTableHeader();
        header.addMouseListener(new ColumnHeaderListener());

        //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(buttons));
        }

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

    Vector<JButton> buttons = null;

    JPanel panel = new JPanel();
    JLabel label = new JLabel();
    JButton button = new JButton();

    CustomColumnCellRenderer( Vector<JButton> buttons) {
        this.buttons  = buttons;

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

        BorderLayout layout = new BorderLayout();
        panel.setLayout(layout);
        panel.add(label, BorderLayout.CENTER);
        panel.add(button, BorderLayout.EAST);
        panel.setBorder(panelBorder);

        buttons.add(button);
    }

    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {

        label.setText(table.getColumnName(column));
        return panel;
    }
}
Ilmenite answered 22/8, 2012 at 10:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.