Blog-Archiv

Freitag, 27. Februar 2026

JButton in Java/Swing JTable

There are numerous examples on the Internet about Java/Swing JButton instances in a JTable. None convinced me, so "I did it my way":-) Here comes the result.

JTable and JTree are the most complex Swing classes. They require understanding of a design-pattern called "Flyweight". In context of a table-view (which may show just a small part of the data), this pattern tells us that we should not use one object per table cell to draw it, because this could end up in big memory consumption. Better use a single "stamp" to draw any cell, and pass the cell data to that stamp directly before drawing. In other words, the stamp receives all the state it needs before its usage through a mostly big list of parameters. The cell-renderer in Swing implements exactly this concept.

Now lets think of a button in a table. We should not hold one button instance per table cell, the "Flyweight" pattern tells us. Instead we use a cell-renderer that paints all cells using the same button.

But what about the click onto that button? The cell-editor will be called when the user clicks onto a cell, that way we can receive it. The cell-editor is not for painting, it is for editing cell data. Because there are no data for a button, we use AbstractAction instances in the table data instead. In other words, the actions to be performed by the button must be added to the table data. Following the "Flyweight" pattern we use just one AbstractAction instance for all cells, so we always add the same action.

Let me start with these actions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
abstract class TableAction extends AbstractAction
{
    public static final String ROW_COLUMN_SEPARATOR = "/";
    
    protected JTable table;
    protected DefaultTableModel tableModel;
    protected int row;
    protected int column;
    protected Vector<Object> rowData;

    protected abstract void actionPerformed();
    
    @Override
    public final void actionPerformed(ActionEvent event) {
        table = (JTable) event.getSource();
        
        String actionCommand = event.getActionCommand();
        String[] split = actionCommand.split(TableAction.ROW_COLUMN_SEPARATOR);
        row = Integer.valueOf(split[0].trim());
        column = Integer.valueOf(split[1].trim());
        
        tableModel = (DefaultTableModel) table.getModel();
        rowData = tableModel.getDataVector().get(row);
        
        actionPerformed();
    }
}

This will be the base class for all actions that must be contained in the table's data. It receives an ActionEvent that will be built by the cell-editor. From that event it provides an execution environment for its sub-classes, including clicked row and column of the table (line 17 - 20).

Lines 5 to 9 represent the execution environment. The actionPerformed(event) method on line 14 receives the table and a row/column string in event.getActionCommand(). It unpacks everthing and finally calls the abstract actionPerformed() method on line 25 that will be implemented by sub-classes.

I can derive an edit-action and a delete-action from TableAction.

class EditCellAction extends TableAction
{
    private final int editingColumn;
    
    public EditCellAction(int editingColumn) {
        this.editingColumn = editingColumn;
    }
    
    @Override
    protected void actionPerformed() {
        String input = JOptionPane.showInputDialog(table, null, rowData.get(editingColumn));
        if (input != null)
            rowData.set(editingColumn, input);
        
        table.revalidate();
        table.repaint();
    }
}

This action receives the column number for its editing content in constructor. On action-performed, it opens an input-dialog for editing the content which must be text. When the dialog was committed, it sets the text back into the table data.

class DeleteRowAction extends TableAction
{
    @Override
    protected void actionPerformed() {
        tableModel.getDataVector().remove(row);
        table.revalidate();
        table.repaint();
    }
}

These actions anticipate that a data-vector is backing the table. If this is not the case for your table, you may find a way to adapt the source-code. This article is just about how to get buttons into a table.

As you can see, it is easy to write other actions that can work upon the table data in any way. Before showing how to add them to the table data, let me introduce the "Flyweight" cell-editor that drives these actions. It will be just one cell-editor for all kinds of actions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class ButtonCellEditor extends AbstractCellEditor implements TableCellEditor
{
    @Override
    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
        Action action = (Action) value;
        ActionEvent event = new ActionEvent(
                table, 
                ActionEvent.ACTION_PERFORMED, 
                row + TableAction.ROW_COLUMN_SEPARATOR + column);
        action.actionPerformed(event);
        return null;
    }
    
    @Override
    public Object getCellEditorValue() {
        return null;
    }
}

Line 4 is the core of cell-editors, it returns the Component that lets edit the table cell data. But as there are no real data, I return null on line 11. In-between, this method receives the AbstractAction instance (from table data) on line 5. After casting it to Action, which is the AbstractAction interface, it can execute the action on line 10. On lines 6 - 9, the ActionEvent is built together, all from the long "Flyweight" parameter list.

This class derives AbstractCellEditor because that super-class provides default implementations for the big interface TableCellEditor. Important is that it returns true from isCellEditable(), that makes our cell-editor receive the user's click. The getCellEditorValue() method is a duty inherited from interface TableCellEditor, returning null does not cause any damage.

The last thing I need now is the cell-renderer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private static class ButtonCellRenderer implements TableCellRenderer
{
    private final JButton button;
    
    public ButtonCellRenderer(String label) {
        this.button = new JButton(label);
        button.setBorder(null); // else label is just "..."
    }
    
    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
        return button;
    }
}

That was easy. The "Flyweight" method on line 11 just returns the button that was built in constructor.

Now I have everything to build together a JTable with buttons. Mind that this is just a demo, and there can be lots of improvements on following code. The table will show just one text column, and two buttons, one to edit the text column, one to delete the table row where it is in.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.util.Vector;
import javax.swing.*;
import javax.swing.table.*;

public class ButtonsInTableExample
{
    private JTable table;
    
    public ButtonsInTableExample() {
        Vector<Object> columnHeaders = new Vector<>();
        columnHeaders.add("Message");
        columnHeaders.add("Edit");
        columnHeaders.add("Delete");

        // one single action for all rows of a column
        TableAction editAction = new EditCellAction(0); // edit column 0
        TableAction deleteAction = new DeleteRowAction();
        
        Vector<Vector<Object>> data = new Vector<>(); // example rows
        for (int i = 0; i < 3; i++) {
            final Vector<Object> row = new Vector<>();
            row.add("Hello "+i); // column 0
            row.add(editAction);
            row.add(deleteAction);
            data.add(row);
        }
        
        TableModel model = new DefaultTableModel(data, columnHeaders) {
            @Override
            public Class<?> getColumnClass(int columnIndex) {
                if (columnIndex == 1)
                    return EditCellAction.class;
                if (columnIndex == 2)
                    return DeleteRowAction.class;
                return String.class;
            }
        };
        
        this.table = new JTable(model);
        
        // one cell-renderer per button-column
        table.setDefaultRenderer(EditCellAction.class, new ButtonCellRenderer("\u270E"));
        table.setDefaultRenderer(DeleteRowAction.class, new ButtonCellRenderer("\u2715"));
        
        // one generic cell-editor for all button-columns
        table.setDefaultEditor(AbstractAction.class, new ButtonCellEditor());
        
        table.getColumnModel().getColumn(1).setMaxWidth(40);
        table.getColumnModel().getColumn(2).setMaxWidth(40);
    }
}

For try out, you can find the full source-code in just one class on bottom of the article.

On line 10 you see the target of the constructor below, the JTable to build.
Lines 13 - 16 create the column header labels.
Lines 19 and 20 create the action instances that have to be in the table's data.
Lines 22 - 29 build three example rows, so that there is something to try out. Important are lines 26 and 27 where I add actions to the table data.
Lines 31 - 40 create the table-model. I overwrite the getColumnClass() method to return the correct action-classes according to the given column-index. This is important.
Line 42 builds the table from the model.
Lines 45 and 46 set the cell-renderers for the edit- and the delete-action.
Line 49 sets the generic cell-editor for all kinds of AbstractAction cells. Remember that its duty is just to cast the cell-value to Action and then perform it. This is not bound to any class. Mind further that it is important to use AbstractAction and not Action as first parameter in the setDefaultEditor() call, because the getDefaultEditor() method searches for a cell-editor only using super-classes of the column-class, interfaces are ignored!
Lines 51 and 52 just restrict the widths of the button cells.


Here comes the full source-code for trying this out - click left-side arrow to expand!
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.util.Vector;
import javax.swing.*;
import javax.swing.table.*;

public class ButtonsInTableExample
{
    public static void main(String[] args) {
        JFrame frame = new JFrame("JTable button example");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(new JScrollPane(new ButtonsInTableExample().table));
        frame.setLocationByPlatform(true);
        frame.setSize(new Dimension(300, 200));
        frame.setVisible(true);
    }
    
    
    // button actions
    
    private static abstract class TableAction extends AbstractAction
    {
        public static final String ROW_COLUMN_SEPARATOR = "/";
        
        protected JTable table;
        protected DefaultTableModel tableModel;
        protected int row;
        protected int column;
        protected Vector<Object> rowData;

        protected abstract void actionPerformed();
        
        @Override
        public void actionPerformed(ActionEvent event) {
            table = (JTable) event.getSource();
            
            String actionCommand = event.getActionCommand();
            String[] split = actionCommand.split(TableAction.ROW_COLUMN_SEPARATOR);
            row = Integer.valueOf(split[0].trim());
            column = Integer.valueOf(split[1].trim());
            
            tableModel = (DefaultTableModel) table.getModel();
            rowData = tableModel.getDataVector().get(row);
            
            actionPerformed();
        }
    }
    
    private static class EditCellAction extends TableAction
    {
        private final int editingColumn;
        
        public EditCellAction(int editingColumn) {
            this.editingColumn = editingColumn;
        }
        
        @Override
        protected void actionPerformed() {
            String input = JOptionPane.showInputDialog(table, null, rowData.get(editingColumn));
            if (input != null)
                rowData.set(editingColumn, input);
            
            table.revalidate();
            table.repaint();
        }
    }
    
    private static class DeleteRowAction extends TableAction
    {
        @Override
        protected void actionPerformed() {
            tableModel.getDataVector().remove(row);
            table.revalidate();
            table.repaint();
        }
    }
    
    
    // cell renderer and editor
    
    private static class ButtonCellRenderer implements TableCellRenderer
    {
        private final JButton button;
        
        public ButtonCellRenderer(String label) {
            this.button = new JButton(label);
            button.setBorder(null); // else label is just "..."
        }
        
        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            return button;
        }
    }
    
    private static class ButtonCellEditor extends AbstractCellEditor implements TableCellEditor
    {
        @Override
        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
            Action action = (Action) value;
            ActionEvent event = new ActionEvent(
                    table, 
                    ActionEvent.ACTION_PERFORMED, 
                    row + TableAction.ROW_COLUMN_SEPARATOR + column);
            action.actionPerformed(event);
            return null;
        }
        
        @Override
        public Object getCellEditorValue() {
            return null;
        }
    }
    
    
    // table build
    
    private JTable table;
    
    public ButtonsInTableExample() {
        Vector<Object> columnHeaders = new Vector<>();
        columnHeaders.add("Message");
        columnHeaders.add("Edit");
        columnHeaders.add("Delete");

        // one single action for all rows of a column
        TableAction editAction = new EditCellAction(0); // edit column 0
        TableAction deleteAction = new DeleteRowAction();
        
        Vector<Vector<Object>> data = new Vector<>(); // example rows
        for (int i = 0; i < 3; i++) {
            final Vector<Object> row = new Vector<>();
            row.add("Hello "+i); // column 0
            row.add(editAction);
            row.add(deleteAction);
            data.add(row);
        }
        
        TableModel model = new DefaultTableModel(data, columnHeaders) {
            @Override
            public Class<?> getColumnClass(int columnIndex) {
                if (columnIndex == 1)
                    return EditCellAction.class;
                if (columnIndex == 2)
                    return DeleteRowAction.class;
                return String.class;
            }
        };
        
        this.table = new JTable(model);
        
        // one cell-renderer per button-column
        table.setDefaultRenderer(EditCellAction.class, new ButtonCellRenderer("\u270E"));
        table.setDefaultRenderer(DeleteRowAction.class, new ButtonCellRenderer("\u2715"));
        
        // one generic cell-editor for all button-columns
        table.setDefaultEditor(AbstractAction.class, new ButtonCellEditor());
        
        table.getColumnModel().getColumn(1).setMaxWidth(40);
        table.getColumnModel().getColumn(2).setMaxWidth(40);
    }
}



Keine Kommentare: