¿Cómo puedo poner un control en el JTableHeader de una JTable?

Dada una JTable con una columna de tipo Boolean.class , el renderizador predeterminado es un JCheckBox . Es bastante fácil seleccionar celdas individuales basadas en una selección de usuario , pero también puede ser conveniente seleccionar todas o ninguna de las casillas de verificación. Estos ejemplos recientes mencionaron el uso de JCheckBox en el encabezado de la tabla, pero la implementación fue incómoda y poco atractiva. Si no necesito ordenar la columna, ¿cómo puedo poner un control de buen comportamiento en JTableHeader ?

Adición: para mayor comodidad, agregué mi sscce como respuesta , pero estaría encantado de aceptar una respuesta que aborde el aspecto de buen comportamiento del problema.

Hay dos partes del problema (como lo veo 🙂

Usabilidad: inventar elementos de interacción con la interfaz de usuario es propenso a confundir a los usuarios. Sin ningún orden en particular:

  • el título del encabezado de la columna está destinado a describir el contenido de la columna, que la descripción del contenido se pierde al reemplazarlo con una descripción de la acción
  • no es inmediatamente (para mí, el usuario más estúpido de la tierra 🙂 que la celda del encabezado tenga la función de un botón para alternar. Al hacer clic accidentalmente perderá todo el estado de contenido anterior en esa columna

Entonces, incluso si el análisis de interacción sale con un claro “lo necesitamos / lo necesitamos”,

  • acción solo en adición al contenido
  • use un widget que sea más claro (p. ej., una casilla de selección de tres estados totalmente desprovista / seleccionada, contenido mixto). Además, la des / selección debe ser posible a partir del contenido mixto. Pensándolo bien, una checkbox probablemente tampoco sea la mejor opción, no profundizó más
  • minimice la posibilidad de que accidentalmente (solo para mí 🙂 cambie el estado masivo, (por ejemplo, mediante una clara separación visual de un área activa – el icono de la checkbox) desde la región del “encabezado normal”.

Aspectos técnicos

  • TableHeader no está diseñado para componentes “activos”. Todo lo que se quiere debe ser controlado por nosotros mismos
  • Hay ejemplos alrededor (p. ej., la cuadrícula JIDE admite agregar componentes)
  • juguetear con el encabezado tiende a parecer poco atractivo porque no es trivial cambiar el renderizador y al mismo tiempo mantener el aspecto proporcionado por LAF

El artículo How to Use Tables: Using Custom Renderers ofrece TableSorter como un ejemplo de cómo detectar eventos de mouse en un encabezado de columna. Usando un enfoque similar, SelectAllHeader extends JToggleButton e implements TableCellRenderer en el ejemplo siguiente para lograr un efecto similar. Un TableModelListener se usa para condicionar el botón de alternar cuando todas las casillas de verificación están en un estado uniforme.

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

enter image description here

Use un TableCellRenderer personalizado:

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

Tenga en cuenta que los componentes con popupmenu (por ejemplo, JComboBox o JMenu) no funcionan bien. Ver: JComboBox no se puede expandir en JTable TableHeader ). Pero puede usar un MenuButton en 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; } }