Como posso colocar um controle no JTableHeader de um JTable?

Dado um JTable com uma coluna do tipo Boolean.class , o renderizador padrão é um JCheckBox . É fácil selecionar células individuais com base em uma seleção de usuário , mas também pode ser conveniente selecionar todas ou nenhuma das checkboxs de seleção. Estes exemplos recentes mencionaram o uso do JCheckBox no header da tabela, mas a implementação foi desajeitada e desagradável. Se eu não precisar classificar a coluna, como posso colocar um controle bem comportado no JTableHeader ?

Adendo: Por conveniência, adicionei meu sscce como resposta , mas ficaria satisfeito em aceitar uma resposta que aborda o aspecto bem comportado do problema.

   

Existem duas partes do problema (como eu vejo 🙂

Usabilidade: inventar a interface do usuário / elementos é propenso a confundir os usuários. Em nenhuma ordem particular:

  • o título do header da coluna destina-se a descrever o conteúdo da coluna, essa descrição do conteúdo é perdida ao substituí-lo por uma descrição de ação
  • não é imediatamente (para mim, o usuário mais idiota do mundo 🙂 claro que a célula do header tem a function de um botão de alternância. Clicando acidentalmente, perderá todo o estado do conteúdo anterior nessa coluna

Então, mesmo que a análise de interação surja com uma clara necessidade de nós,

  • ação somente em adição ao conteúdo
  • use um widget que seja mais claro (por exemplo, uma checkbox de seleção tri-state all-de- / selected, mixed content). Além disso, a seleção / seleção deve ser possível a partir de conteúdo misto. Pensando bem, uma checkbox de seleção provavelmente também não é a melhor escolha,
  • minimizar a possibilidade de acidentalmente (apenas para mim 🙂 alterar o estado de volume, (por exemplo, por uma clara separação visual de uma área ativa – o ícone da checkbox de seleção) da região “header normal”.

Aspectos tecnicos

  • TableHeader não foi projetado para componentes “ativos”. O que quer que seja desejado, deve ser controlado por nós mesmos
  • exemplos estão por aí (por exemplo, a grade JIDE suporta a adição de componentes)
  • mexer com o header tende a parecer pouco atraente porque não é trivial mudar o renderizador e ao mesmo tempo manter a aparência fornecida pelo LAF

O artigo Como Usar as Tabelas: Usando Custom Renderers oferece TableSorter como um exemplo de como detectar events de mouse em um header de coluna. Usando uma abordagem semelhante, o SelectAllHeader extends JToggleButton e implements TableCellRenderer no exemplo abaixo para obter um efeito semelhante. Um TableModelListener é usado para condicionar o botão de alternância quando todas as checkboxs de seleção estão em um estado uniforme.

insira a descrição da imagem aqui

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

insira a descrição da imagem aqui

Use um 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); } } } 

Por favor, note que componentes com popupmenu (por exemplo, JComboBox ou JMenu) não funcionam bem. Veja: JComboBox não consegue expandir em JTable TableHeader ). Mas você pode usar um MenuButton no TableHeader :

insira a descrição da imagem aqui

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