How can I put a control in the JTableHeader of a JTable?

21,478

Solution 1

There are two parts of the problem (as I see it :-)

Usability: inventing UI-interaction/elements is prone to confusing users. In no particular order:

  • the column header title is meant to describe the content of the column, that content description is lost when in replacing it with an action description
  • it's not immediately (for me, the dumbest user on earth :-) clear that the header cell has the function of a toggle button. Accidentally clicking it will loose all earlier content state in that column

So even if interaction analysis comes out with a clear we-do-need/want-it,

  • action only in-addition to the content
  • use a widget that's clearer (e.g. a tri-state checkbox all-de-/selected, mixed content). Also, de-/selecting must both be possible from mixed content. On second thought, a checkbox probably isn't the best choice either, didn't dig further
  • minimize the possibility to accidentally (just for me :-) change bulk state, (e.g. by a clear visual separation of an active area - the checkbox icon) from the "normal header" region.

Technical aspects

  • TableHeader is not designed for "live" components. Whatever is wanted has to be controlled by ourselves
  • examples are around (e.g. JIDE grid supports adding components)
  • fiddling with header tends to look unattractive because it's not trivial to change the renderer and at the same time keep the LAF provided appearance

Solution 2

The article How to Use Tables: Using Custom Renderers offers TableSorter as an example of how to detect mouse events on a column header. Using a similar approach, SelectAllHeader extends JToggleButton and implements TableCellRenderer in the example below to achieve a similar effect. A TableModelListener is used to condition the toggle button when all the check boxes are in a uniform state.

enter image description here

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.*;

/**
 * @see http://stackoverflow.com/questions/7137786
 * @see http://stackoverflow.com/questions/7092219
 * @see http://stackoverflow.com/questions/7093213
 */
public class SelectAllHeaderTest {

    private static final int BOOLEAN_COL = 2;
    private static final Object colNames[] = {"Column 1", "Column 2", ""};
    private DefaultTableModel model = new DefaultTableModel(null, colNames) {

        @Override
        public Class<?> getColumnClass(int columnIndex) {
            if (columnIndex == BOOLEAN_COL) {
                return Boolean.class;
            } else {
                return String.class;
            }
        }
    };
    private JTable table = new JTable(model);

    public void create() {
        for (int x = 1; x < 6; x++) {
            model.addRow(new Object[]{
                    "Row " + x + ", Col 1", "Row " + x + ", Col 2", false
                });
        }
        table.setAutoCreateRowSorter(true);
        table.setPreferredScrollableViewportSize(new Dimension(320, 160));
        TableColumn tc = table.getColumnModel().getColumn(BOOLEAN_COL);
        tc.setHeaderRenderer(new SelectAllHeader(table, BOOLEAN_COL));
        JFrame f = new JFrame();
        f.add(new JScrollPane(table));
        f.pack();
        f.setLocationRelativeTo(null);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                new SelectAllHeaderTest().create();
            }
        });
    }
}

/**
 * A TableCellRenderer that selects all or none of a Boolean column.
 * 
 * @param targetColumn the Boolean column to manage
 */
class SelectAllHeader extends JToggleButton implements TableCellRenderer {

    private static final String ALL = "✓ Select all";
    private static final String NONE = "✓ Select none";
    private JTable table;
    private TableModel tableModel;
    private JTableHeader header;
    private TableColumnModel tcm;
    private int targetColumn;
    private int viewColumn;

    public SelectAllHeader(JTable table, int targetColumn) {
        super(ALL);
        this.table = table;
        this.tableModel = table.getModel();
        if (tableModel.getColumnClass(targetColumn) != Boolean.class) {
            throw new IllegalArgumentException("Boolean column required.");
        }
        this.targetColumn = targetColumn;
        this.header = table.getTableHeader();
        this.tcm = table.getColumnModel();
        this.applyUI();
        this.addItemListener(new ItemHandler());
        header.addMouseListener(new MouseHandler());
        tableModel.addTableModelListener(new ModelHandler());
    }

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

    private class ItemHandler implements ItemListener {

        @Override
        public void itemStateChanged(ItemEvent e) {
            boolean state = e.getStateChange() == ItemEvent.SELECTED;
            setText((state) ? NONE : ALL);
            for (int r = 0; r < table.getRowCount(); r++) {
                table.setValueAt(state, r, viewColumn);
            }
        }
    }

    @Override
    public void updateUI() {
        super.updateUI();
        applyUI();
    }

    private void applyUI() {
        this.setFont(UIManager.getFont("TableHeader.font"));
        this.setBorder(UIManager.getBorder("TableHeader.cellBorder"));
        this.setBackground(UIManager.getColor("TableHeader.background"));
        this.setForeground(UIManager.getColor("TableHeader.foreground"));
    }

    private class MouseHandler extends MouseAdapter {

        @Override
        public void mouseClicked(MouseEvent e) {
            viewColumn = header.columnAtPoint(e.getPoint());
            int modelColumn = tcm.getColumn(viewColumn).getModelIndex();
            if (modelColumn == targetColumn) {
                doClick();
            }
        }
    }

    private class ModelHandler implements TableModelListener {

        @Override
        public void tableChanged(TableModelEvent e) {
            if (needsToggle()) {
                doClick();
                header.repaint();
            }
        }
    }

    // Return true if this toggle needs to match the model.
    private boolean needsToggle() {
        boolean allTrue = true;
        boolean allFalse = true;
        for (int r = 0; r < tableModel.getRowCount(); r++) {
            boolean b = (Boolean) tableModel.getValueAt(r, targetColumn);
            allTrue &= b;
            allFalse &= !b;
        }
        return allTrue && !isSelected() || allFalse && isSelected();
    }
}

Solution 3

enter image description here

Use a custom TableCellRenderer:

    // column 1
    col = table.getColumnModel().getColumn(1);
    col.setHeaderRenderer(new EditableHeaderRenderer( new JButton("Button")));
    // column 2     
    col = table.getColumnModel().getColumn(2);
    col.setHeaderRenderer(new EditableHeaderRenderer( new JToggleButton("Toggle")));
    // column 3
    col = table.getColumnModel().getColumn(3);
    col.setHeaderRenderer(new EditableHeaderRenderer( new JCheckBox("CheckBox")));



class EditableHeaderRenderer implements TableCellRenderer {

    private JTable table = null;
    private MouseEventReposter reporter = null;
    private JComponent editor;

    EditableHeaderRenderer(JComponent editor) {
        this.editor = editor;
        this.editor.setBorder(UIManager.getBorder("TableHeader.cellBorder"));
    }

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) {
        if (table != null && this.table != table) {
            this.table = table;
            final JTableHeader header = table.getTableHeader();   
            if (header != null) {   
                this.editor.setForeground(header.getForeground());   
                this.editor.setBackground(header.getBackground());   
                this.editor.setFont(header.getFont());
                reporter = new MouseEventReposter(header, col, this.editor);
                header.addMouseListener(reporter);
            }
        }

        if (reporter != null) reporter.setColumn(col);

        return this.editor;
    }

    static public class MouseEventReposter extends MouseAdapter {

        private Component dispatchComponent;
        private JTableHeader header;
        private int column  = -1;
        private Component editor;

        public MouseEventReposter(JTableHeader header, int column, Component editor) {
            this.header = header;
            this.column = column;
            this.editor = editor;
        }

        public void setColumn(int column) {
            this.column = column;
        }

        private void setDispatchComponent(MouseEvent e) {
            int col = header.getTable().columnAtPoint(e.getPoint());
            if (col != column || col == -1) return;

            Point p = e.getPoint();
            Point p2 = SwingUtilities.convertPoint(header, p, editor);
            dispatchComponent = SwingUtilities.getDeepestComponentAt(editor, p2.x, p2.y);
        }

        private boolean repostEvent(MouseEvent e) {
            if (dispatchComponent == null) {
                return false;
            }
            MouseEvent e2 = SwingUtilities.convertMouseEvent(header, e, dispatchComponent);
            dispatchComponent.dispatchEvent(e2);
            return true;
        }

        @Override
        public void mousePressed(MouseEvent e) {
            if (header.getResizingColumn() == null) {
                Point p = e.getPoint();

                int col = header.getTable().columnAtPoint(p);
                if (col != column || col == -1) return;

                int index = header.getColumnModel().getColumnIndexAtX(p.x);
                if (index == -1) return;

                editor.setBounds(header.getHeaderRect(index));
                header.add(editor);
                editor.validate();
                setDispatchComponent(e);
                repostEvent(e);
            }
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            repostEvent(e);
            dispatchComponent = null;
            header.remove(editor);
        }
    }
}

Please note that components with popupmenu (e.g. JComboBox or JMenu) don't work well. See: JComboBox fails to expand in JTable TableHeader). But you can use a MenuButton in the TableHeader:

enter image description here

class MenuButtonTableHeaderRenderer extends JPanel implements TableCellRenderer {

    private int     column  = -1;
    private JTable  table   = null;
    private MenuButton b;

    MenuButtonTableHeaderRenderer(String name, JPopupMenu menu) {
        super(new BorderLayout());
        b = new MenuButton(ResourceManager.ARROW_BOTTOM, menu);
        b.setBorder(BorderFactory.createEmptyBorder(1,1,1,1));
        JLabel l = new JLabel(name);
        l.setFont(l.getFont().deriveFont(Font.PLAIN));
        l.setBorder(BorderFactory.createEmptyBorder(1,5,1,1));
        add(b, BorderLayout.WEST);
        add(l, BorderLayout.CENTER);
        setBorder(UIManager.getBorder("TableHeader.cellBorder"));
    }

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

        if (table != null && this.table != table) {
            this.table = table;
            final JTableHeader header = table.getTableHeader();   
            if (header != null) {   
                setForeground(header.getForeground());   
                setBackground(header.getBackground());   
                setFont(header.getFont());

                header.addMouseListener(new MouseAdapter() {

                    @Override
                    public void  mouseClicked(MouseEvent e) {
                        int col = header.getTable().columnAtPoint(e.getPoint());
                        if (col != column || col == -1) return;

                        int index = header.getColumnModel().getColumnIndexAtX(e.getPoint().x);
                        if (index == -1) return;

                        setBounds(header.getHeaderRect(index));
                        header.add(MenuButtonTableHeaderRenderer.this);
                        validate();

                        b.doClick();

                        header.remove(MenuButtonTableHeaderRenderer.this);

                        header.repaint();   
                    }
                });
            }
        }
        column = col;
        return this;
    }
}
Share:
21,478
trashgod
Author by

trashgod

trashgod: Similar in concept to trash fish. quote: Your thoughtful comments have dramatically enlarged the scope of my ignorance. quote: The trash can be an important design tool.—loc. cit.

Updated on March 08, 2020

Comments

  • trashgod
    trashgod about 4 years

    Given a JTable with a column of type Boolean.class, the default renderer is a JCheckBox. It's easy enough to select individual cells based on a user selection, but it may be convenient to select all or none of the check boxes, too. These recent examples mentioned using JCheckBox in the table header, but the implementation was awkward and unappealing. If I don't need to sort the column, how can I put a well-behaved control in the JTableHeader?

    Addendum: For convenience, I've added my sscce as an answer, but I'd be pleased to accept an answer that addresses the well-behaved aspect of the problem.

  • kleopatra
    kleopatra over 12 years
    a) blows in Metal b) shouldn't de-/select when clicked in resize region
  • trashgod
    trashgod over 12 years
    @kleopatra: This is the kind of thing I wanted to know; please consider making it an answer. On further testing, Metal seems to reject the recycled arrow; I should make my own.
  • Alter Hu
    Alter Hu almost 8 years
    if you insert a new row ,trigger the event fireTableRowsInserted ,it will throw the exception for itemStateChanged with invalid range error ,does anyone met this ? so here table.getRowCount() is not correct ,because the table not render there ....
  • trashgod
    trashgod almost 8 years
    @Alter.hu: DefaultTableModel fires the event for you; if you have a question, please see How to Ask.
  • Alter Hu
    Alter Hu almost 8 years
    @trashgod,yes you're right ,but here the problem is SelectAllHeader class will cause every column data to check.see below detail comments from my side update for this class.
  • Alter Hu
    Alter Hu almost 8 years
    @trashgod,sorry ,it's my fault ,here i use the "AbstractTableModel" class to customize the table model ,that's why i met this exception. but for DefaultTableModel it's okay .thanks .
  • The_Cute_Hedgehog
    The_Cute_Hedgehog over 7 years
    Where can I listen the actions for JButton and JToggleButton above? As the second one, why do you remove the editor after the mouse released? That makes the editor disappear after the click event.
  • JAVA
    JAVA over 7 years
    This "ResourceManager.ARROW_BOTTOM" thing giving error, how could i found ResourceManager class? in what jar @luca
  • luca
    luca over 7 years
    That's a custom class I used for icons. You can pass your own icon instead