Как я могу поместить элемент управления в JTableHeader JTable?

Учитывая JTable со столбцом типа Boolean.class, средство визуализации по умолчанию – это JCheckBox. Достаточно просто выбрать отдельные ячейки на основе выбора пользователя. , но также может быть удобно установить все флажки или ни один из них. Эти последние примеры упоминались с использованием JCheckBox в заголовке таблицы, но реализация была неудобной и непривлекательной. Если мне не нужно сортировать столбец, как я могу поместить корректный элемент управления в JTableHeader?

Приложение: для удобства я добавил свой sscce как ответ, но я был бы рад принять ответ, который обращается к хорошему поведению аспекту проблемы.


person trashgod    schedule 21.08.2011    source источник
comment
хм... в чем именно вопрос, в частности, что вы подразумеваете под благовоспитанным? Мы все знаем, что в шапке нет поддержки живых компонентов, все нужно делать самим :-) Что касается использования переключателя, не знаю, понимают ли пользователи, что он делает и когда - случайное нажатие на ячейку приведет к потерять все текущие данные в столбце   -  person kleopatra    schedule 21.08.2011
comment
@kleopatra: Хороший вопрос; Я полагаю, что в первую очередь следует усомниться в необходимости такого устройства.   -  person trashgod    schedule 21.08.2011


Ответы (3)


Есть две части проблемы (как я это вижу :-)

Юзабилити: изобретение UI-взаимодействия/элементов может сбить пользователей с толку. В произвольном порядке:

  • заголовок столбца предназначен для описания содержимого столбца, это описание содержимого теряется при замене его описанием действия
  • не сразу (для меня, самого тупого пользователя на земле :-) ясно, что ячейка заголовка имеет функцию переключателя. Случайный щелчок по ней приведет к потере всего предыдущего состояния содержимого в этом столбце.

Таким образом, даже если анализ взаимодействия дает четкое представление о том, что мы действительно нуждаемся/хотим,

  • действие только в дополнении к содержанию
  • используйте виджет, который более четкий (например, флажок с тремя состояниями — все де-/выбрано, смешанный контент). Кроме того, отмена/выбор должны быть возможны из смешанного контента. Если подумать, флажок, вероятно, тоже не лучший выбор, дальше не копал
  • свести к минимуму возможность случайного (только для меня :-) изменения массового состояния (например, путем четкого визуального отделения активной области - значка флажка) от области «нормального заголовка».

Технические аспекты

  • TableHeader не предназначен для "живых" компонентов. Все, что мы хотим, должно контролироваться нами
  • примеры есть (например, сетка JIDE поддерживает добавление компонентов)
  • возня с заголовком имеет тенденцию выглядеть непривлекательно, потому что не так просто изменить средство визуализации и в то же время сохранить внешний вид, предоставляемый LAF.
person kleopatra    schedule 22.08.2011

Статья Как использовать таблицы: использование пользовательских средств визуализации предлагает TableSorter< /a> в качестве примера обнаружения событий мыши в заголовке столбца. Используя аналогичный подход, SelectAllHeader extends JToggleButton и implements TableCellRenderer в приведенном ниже примере для достижения аналогичного эффекта. TableModelListener используется для настройки кнопки-переключателя, когда все флажки находятся в одинаковом состоянии.

введите здесь описание изображения

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();
    }
}
person trashgod    schedule 21.08.2011
comment
а) удары в металле б) не следует отменять/выбирать при нажатии в области изменения размера - person kleopatra; 21.08.2011
comment
@kleopatra: Это то, что я хотел знать; пожалуйста, подумайте о том, чтобы сделать это ответом. При дальнейшем тестировании Metal, кажется, отвергает переработанную стрелу; Я должен сделать свой собственный. - person trashgod; 21.08.2011
comment
если вы вставите новую строку, вызовите событие fireTableRowsInserted, оно вызовет исключение для itemStateChanged с ошибкой недопустимого диапазона, кто-нибудь сталкивался с этим? так что здесь table.getRowCount() неверен, потому что таблица там не отображается.... - person Alter Hu; 12.06.2016
comment
@trashgod, да, вы правы, но здесь проблема в том, что класс SelectAllHeader вызовет проверку данных каждого столбца. См. ниже подробные комментарии из моего бокового обновления для этого класса. - person Alter Hu; 13.06.2016
comment
@trashgod, извините, это моя вина, здесь я использую класс AbstractTableModel для настройки модели таблицы, поэтому я встретил это исключение. но для DefaultTableModel все в порядке. спасибо. - person Alter Hu; 13.06.2016

введите здесь описание изображения

Используйте пользовательский 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);
        }
    }
}

Обратите внимание, что компоненты с всплывающим меню (например, JComboBox или JMenu) работают некорректно. См.: JComboBox не расширяется в JTable TableHeader). Но вы можете использовать MenuButton в заголовке таблицы:

введите здесь описание изображения

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;
    }
}
person luca    schedule 30.04.2015
comment
Где я могу прослушать действия для JButton и JToggleButton выше? Как второй, почему вы удаляете редактор после отпускания мыши? Это заставляет редактор исчезнуть после события click. - person The_Cute_Hedgehog; 06.08.2016
comment
Эта вещь ResourceManager.ARROW_BOTTOM выдает ошибку, как я могу найти класс ResourceManager? в какой банке @luca - person JAVA; 01.11.2016
comment
Это пользовательский класс, который я использовал для иконок. Вместо этого вы можете передать свой собственный значок - person luca; 02.11.2016