【问题标题】:How can I put a control in the JTableHeader of a JTable?如何在 JTable 的 JTableHeader 中放置控件?
【发布时间】:2011-10-31 13:18:36
【问题描述】:

给定一个JTable 和一个Boolean.class 类型的列,default renderer 是一个JCheckBox。根据user selection 选择单个单元格很容易,但也可以方便地选择所有复选框或不选择任何复选框。这些recent examples 提到在表头中使用JCheckBox,但实现起来很尴尬且没有吸引力。如果我不需要对列进行排序,如何在JTableHeader中放置一个行为良好的控件?

附录:为方便起见,我已将我的sscce 添加为answer,但我很乐意接受解决问题的表现良好方面的答案。

【问题讨论】:

  • hmm ...问题到底是什么,特别是,你所说的乖巧是什么意思?我们都知道标题中不支持“实时”组件,一切都需要我们自己完成:-) 至于使用切换按钮,不知道用户是否理解它的作用以及何时 - 不小心点击了单元格将丢失列中的所有当前数据
  • @kleopatra:好点;我推断首先应该质疑对这种设备的需求。

标签: java swing jtable jtableheader jtogglebutton


【解决方案1】:

问题有两个部分(我认为 :-)

可用性:发明 UI 交互/元素很容易让用户感到困惑。排名不分先后:

  • 列标题用于描述列的内容,当用操作描述替换它时,该内容描述会丢失
  • 标题单元格具有切换按钮的功能并不是立即(对我来说,地球上最愚蠢的用户 :-)。不小心点击它会丢失该列中所有早期的内容状态

因此,即使交互分析得出明确的我们需要/想要的结果,

  • 仅在内容之外的动作
  • 使用更清晰的小部件(例如,三态复选框全部取消/选中,混合内容)。此外,从混合内容中取消/选择都必须是可能的。再想一想,复选框可能也不是最好的选择,没有进一步挖掘
  • 最大限度地减少意外(仅对我而言 :-)更改批量状态的可能性,(例如,通过将活动区域 - 复选框图标清晰地视觉分离)与“正常标题”区域。

技术方面

  • TableHeader 不是为“实时”组件设计的。想要什么,都得靠我们自己控制
  • 周围有示例(例如 JIDE 网格支持添加组件)
  • 摆弄标题往往看起来没有吸引力,因为更改渲染器并同时保持 LAF 提供的外观并非易事

【讨论】:

    【解决方案2】:

    使用自定义 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 fails to expand in JTable TableHeader)。 但是你可以在 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;
        }
    }
    

    【讨论】:

    • 我在哪里可以听到上面 JButton 和 JToggleButton 的动作?至于第二个,为什么鼠标放开后要去掉编辑器?这使得编辑器在点击事件后消失。
    • 这个“ResourceManager.ARROW_BOTTOM”的东西给出错误,我怎么能找到 ResourceManager 类?在什么罐子里@luca
    • 这是我用于图标的自定义类。您可以改为传递自己的图标
    【解决方案3】:

    文章How to Use Tables: Using Custom Renderers 提供TableSorter 作为如何检测列标题上的鼠标事件的示例。使用类似的方法,在下面的例子中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();
        }
    }
    

    【讨论】:

    • a) 吹入金属 b) 在调整大小区域中单击时不应取消/选择
    • @kleopatra:这是我想知道的事情;请考虑将其作为答案。在进一步测试中,Metal 似乎拒绝回收箭头;我应该自己做。
    • 如果插入新行,触发fireTableRowsInserted事件,会抛出itemStateChanged异常,范围无效错误,有人遇到过吗?所以这里 table.getRowCount() 是不正确的,因为表格没有呈现在那里 ....
    • @Alter.hu: DefaultTableModel 为您触发事件;如果您有任何问题,请查看How to Ask。
    • @trashgod,是的,你是对的,但这里的问题是 SelectAllHeader 类会导致每列数据都被检查。请参阅下面我对此类更新的详细信息.
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-12
    • 1970-01-01
    • 2010-09-20
    • 1970-01-01
    相关资源
    最近更新 更多