2

I have a program where I have a JTable with text in most cells, however the last cell in each row I need to have a JButton. I am using a custom button renderer as well as editor ( i don't want users editing the content of anything in the table). I'm unsure as how to get my buttons clickable though. My main code is:

StartingPoint.java

import java.util.Vector;

import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;

public class StartingPoint {
    public static void main(String[] args) {
        String column_names[] = {"Text", "Button"};
        DefaultTableModel dtm = new DefaultTableModel(column_names, 0);

        JButton button1 = new JButton();
        button1.setText("Button 1");
        button1.setToolTipText("Button");

        JButton button2 = new JButton();
        button2.setText("Button 2");
        button2.setToolTipText("Buttonnn");

        Vector<Object> row1 = new Vector<Object>();
        row1.add("Testing");
        row1.add(button1);

        Vector<Object> row2 = new Vector<Object>();
        row2.add("More Testing");
        row2.add(button2);

        dtm.addRow(row1);
        dtm.addRow(row2);

        JTable table = new JTable(dtm);
        JScrollPane scrolly = new JScrollPane(table);
        table.setFillsViewportHeight(true);

        JTableButtonRenderer buttonRenderer = new JTableButtonRenderer();
        JTableButtonEditor buttonEditor = new JTableButtonEditor();
        table.getColumn("Button").setCellRenderer(buttonRenderer);
        table.getColumn("Button").setCellEditor(buttonEditor);

        JFrame frame = new JFrame("Testing");
        frame.getContentPane().add(scrolly);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }
}

JTableButtonRenderer

import java.awt.Component;

import javax.swing.JButton;
import javax.swing.JTable;
import javax.swing.table.TableCellRenderer;

public class JTableButtonRenderer implements TableCellRenderer {
    JTableButtonRenderer() {}
    @Override
    public Component getTableCellRendererComponent(JTable table, Object 
value, boolean isSelected, boolean hasFocus, int rows, int columns) {
        JButton button = (JButton)value;
        return button;
    }
}

JTableButtonEditor.java

import java.awt.Component;
import java.util.EventObject;

import javax.swing.JTable;
import javax.swing.event.CellEditorListener;
import javax.swing.table.TableCellEditor;

public class JTableButtonEditor implements TableCellEditor {

    @Override
    public void addCellEditorListener(CellEditorListener arg0) {
        // TODO Auto-generated method stub

    }

    @Override
    public void cancelCellEditing() {
        // TODO Auto-generated method stub

    }

    @Override
    public Object getCellEditorValue() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public boolean isCellEditable(EventObject arg0) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public void removeCellEditorListener(CellEditorListener arg0) {
        // TODO Auto-generated method stub

    }

    @Override
    public boolean shouldSelectCell(EventObject arg0) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean stopCellEditing() {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public Component getTableCellEditorComponent(JTable arg0, Object arg1, 
boolean arg2, int arg3, int arg4) {
        // TODO Auto-generated method stub
        return null;
    }

}

Do I need to change something in the renderer / editor classes? I also tried adding actionlistener in my StartingPoint class when I create the button.

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
Vince
  • 2,596
  • 11
  • 43
  • 76
  • First of all, don't add components to your model, a model carries data or state, the renderer/editor reflects that state in some meaningful way. The question then becomes, what is it that these buttons need to do? As an editor is "suppose" to return a value back to the model – MadProgrammer Apr 22 '19 at 21:42
  • I'm going to throw [this example](https://stackoverflow.com/questions/25070511/add-jbutton-to-each-row-of-a-jtable/25071138#25071138) out there, because I have a pathological dislike for buttons in `JTable`s, but, you should also consider having a look at [Table Button Column](http://tips4java.wordpress.com/2009/07/12/table-button-column/) for another example – MadProgrammer Apr 22 '19 at 21:44
  • The button in the end will open a new window, displaying an image given a directory (which will be stored in the buttons tooltip) – Vince Apr 22 '19 at 21:49

1 Answers1

2

"Conceptually", the idea is pretty simple and is covered in some detail in How to Use Tables

You need to start by defining your editor (and renderer). I've chosen to wrap both together for simplicity, as much of the functionality is repeated for both.

public class TableDeleteButtonEditor extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {

    private File source;
    private JButton button;

    public TableDeleteButtonEditor() {
        button = new JButton();
        button.addActionListener(new LoadActionListener());
    }

    @Override
    public boolean shouldSelectCell(EventObject anEvent) {
        return true;
    }

    protected JButton prepare(JTable table, Object value, boolean isSelected, int row, int column) {
        if (!(value instanceof File)) {
            source = null;
            button.setEnabled(false);
            return null;
        }
        source = (File) value;
        button.setEnabled(true);
        button.setText(source.getName());
        button.setToolTipText(source.getPath());
        return button;
    }

    @Override
    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
        return prepare(table, value, isSelected, row, column);
    }

    @Override
    public Object getCellEditorValue() {
        return source;
    }

    @Override
    public Component getTableCellRendererComponent(JTable table,
                                  Object value,
                                  boolean isSelected,
                                  boolean hasFocus,
                                  int row,
                                  int column) {
        return prepare(table, value, isSelected, row, column);
    }

    public class LoadActionListener implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent evt) {
            // Here, you need to make some decisions about what to do...
            // You have a reference to the File instance
            System.out.println("You clicked " + source);
            stopCellEditing();
        }

    }
}

Since it's likely that you'll only have one active editor, when ever getTableCellEditorComponent is called, you need to grab a reference to the underlying data (ie the File reference).

Typically, a editor will return a value back to the model, in this case, I'm not sure that makes sense. Not saying you can't do it, but I'd be questioning the purpose.

For my example, I only needed the reference to the File itself, so, technically, I only needed a single column. Instead, I devised a model which required two columns, but used the reference of the File to populate both. This is a neat example which demonstrates the ability for a "simple" object to be expanded into multiple parts and represented by the model in a different way...

public class FileTableModel extends AbstractTableModel {

    private List<File> files;

    public FileTableModel(List<File> files) {
        this.files = files;
    }

    @Override
    public int getRowCount() {
        return files.size();
    }

    @Override
    public int getColumnCount() {
        return 2;
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return columnIndex == 1;
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
        switch (columnIndex) {
            case 1: return File.class;
            default: return String.class;
        }
    }

    @Override
    public Object getValueAt(int row, int col) {
        File file = files.get(row);
        switch (col) {
            case 0: return file.getName();
            case 1: return file;
        }
        return null;
    }

}

Now, the important parts here are the isCellEditable and getColumnClass methods. These help determine which cells can be edited and provide a entry point for looking up renderers/editors for the JTable, which brings us to the next step, you need to configure the JTable to support your custom editor/renderer

There's a few ways that you can do this, but for simplicity, setDefaultRenderer and setDefaultEditor should work just fine...

List<File> files = Arrays.asList(new File(".").listFiles());
FileTableModel model = new FileTableModel(files);
JTable table = new JTable(model);
table.setDefaultEditor(File.class, new TableDeleteButtonEditor());
table.setDefaultRenderer(File.class, new TableDeleteButtonEditor());

nb: You can use a single instance of TableDeleteButtonEditor, I'm just been lazy with my copy and pasting

And from there, you should now be able to represent a list of Files in a JTable, where the last column is a button (with the file name) and when clicked, in this example, will print the File reference

Runnable Example...

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.Arrays;
import java.util.EventObject;
import java.util.List;
import javax.swing.AbstractCellEditor;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;

public class Main {

    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(new BorderLayout());

            List<File> files = Arrays.asList(new File(".").listFiles());
            FileTableModel model = new FileTableModel(files);
            JTable table = new JTable(model);
            table.setDefaultEditor(File.class, new TableDeleteButtonEditor());
            table.setDefaultRenderer(File.class, new TableDeleteButtonEditor());

            add(new JScrollPane(table));
        }

    }

    public class FileTableModel extends AbstractTableModel {

        private List<File> files;

        public FileTableModel(List<File> files) {
            this.files = files;
        }

        @Override
        public int getRowCount() {
            return files.size();
        }

        @Override
        public int getColumnCount() {
            return 2;
        }

        @Override
        public boolean isCellEditable(int rowIndex, int columnIndex) {
            return columnIndex == 1;
        }

        @Override
        public Class<?> getColumnClass(int columnIndex) {
            switch (columnIndex) {
                case 1:
                    return File.class;
                default:
                    return String.class;
            }
        }

        @Override
        public Object getValueAt(int row, int col) {
            File file = files.get(row);
            switch (col) {
                case 0:
                    return file.getName();
                case 1:
                    return file;
            }
            return null;
        }

    }

    public class TableDeleteButtonEditor extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {

        private File source;
        private JButton button;

        public TableDeleteButtonEditor() {
            button = new JButton();
            button.addActionListener(new LoadActionListener());
        }

        @Override
        public boolean shouldSelectCell(EventObject anEvent) {
            return true;
        }

        protected JButton prepare(JTable table, Object value, boolean isSelected, int row, int column) {
            if (!(value instanceof File)) {
                source = null;
                button.setEnabled(false);
                return null;
            }
            source = (File) value;
            button.setEnabled(true);
            button.setText(source.getName());
            button.setToolTipText(source.getPath());
            return button;
        }

        @Override
        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
            return prepare(table, value, isSelected, row, column);
        }

        @Override
        public Object getCellEditorValue() {
            return source;
        }

        @Override
        public Component getTableCellRendererComponent(JTable table,
                Object value,
                boolean isSelected,
                boolean hasFocus,
                int row,
                int column) {
            return prepare(table, value, isSelected, row, column);
        }

        public class LoadActionListener implements ActionListener {

            @Override
            public void actionPerformed(ActionEvent evt) {
                // Here, you need to make some decisions about what to do...
                // You have a reference to the File instance
                System.out.println("You clicked " + source);
                stopCellEditing();
            }

        }
    }
}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366