Highlighting a column header of a JTable

2019-02-10 05:50发布

问题:

i'm currently building a little JTable, and want to highlight the column header (and row headers - the row-header part is actually working) when a cell is selected to make it easier to find the associated names with this cell. Here is a picture:

I already tried switching out the renderer for the header with this:

table.getTableHeader().setDefaultRenderer(new ColumnHeaderRenderer());

But it's only called when i click on the header and always says isSelected is false.

This is the code i use for the row-names, including the highlight inside the renderer - code is not by me, i just modified it a little:

/*
 *  Use a JTable as a renderer for row numbers of a given main table.
 *  This table must be added to the row header of the scrollpane that
 *  contains the main table.
 */
public class RowNameTable extends JTable
        implements ChangeListener, PropertyChangeListener {

    private JTable main;

    public RowNameTable(JTable table) {
        main = table;
        main.addPropertyChangeListener(this);

        setFocusable(false);
        setAutoCreateColumnsFromModel(false);
        setModel(main.getModel());
        setSelectionModel(main.getSelectionModel());

        TableColumn column = new TableColumn();
        column.setHeaderValue(" ");
        addColumn(column);
        column.setCellRenderer(new RowNameRenderer(main));

        getColumnModel().getColumn(0).setPreferredWidth(table.getColumnModel().getColumn(0).getPreferredWidth());
        setPreferredScrollableViewportSize(getPreferredSize());
    }

    @Override
    public void addNotify() {
        super.addNotify();

        Component c = getParent();

        //  Keep scrolling of the row table in sync with the main table.

        if (c instanceof JViewport) {
            JViewport viewport = (JViewport) c;
            viewport.addChangeListener(this);
        }
    }

    /*
     *  Delegate method to main table
     */
    @Override
    public int getRowCount() {
        return main.getRowCount();
    }

    @Override
    public int getRowHeight(int row) {
        return main.getRowHeight(row);
    }

    /*
     *  This table does not use any data from the main TableModel,
     *  so just return a value based on the row parameter.
     */
    @Override
    public Object getValueAt(int row, int column) {
        return Integer.toString(row + 1);
    }

    /*
     *  Don't edit data in the main TableModel by mistake
     */
    @Override
    public boolean isCellEditable(int row, int column) {
        return false;
    }
//
//  Implement the ChangeListener
//

    public void stateChanged(ChangeEvent e) {
        //  Keep the scrolling of the row table in sync with main table

        JViewport viewport = (JViewport) e.getSource();
        JScrollPane scrollPane = (JScrollPane) viewport.getParent();
        scrollPane.getVerticalScrollBar().setValue(viewport.getViewPosition().y);
    }
//
//  Implement the PropertyChangeListener
//

    public void propertyChange(PropertyChangeEvent e) {
        //  Keep the row table in sync with the main table

        if ("selectionModel".equals(e.getPropertyName())) {
            setSelectionModel(main.getSelectionModel());
        }

        if ("model".equals(e.getPropertyName())) {
            setModel(main.getModel());
        }
    }

    /*
     *  Borrow the renderer from JDK1.4.2 table header
     */
    private static class RowNameRenderer extends DefaultTableCellRenderer {

        private JTable main;

        public RowNameRenderer(JTable main) {
            this.main = main;
            setHorizontalAlignment(JLabel.CENTER);
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            if (table != null) {
                JTableHeader header = table.getTableHeader();

                if (header != null) {
                    setForeground(header.getForeground());
                    setBackground(header.getBackground());
                    setFont(header.getFont());
                }
            }

            if (isSelected) {
                setFont(getFont().deriveFont(Font.BOLD));
            }

            setText((value == null) ? "" : main.getColumnName(row));
            setBorder(UIManager.getBorder("TableHeader.cellBorder"));

            return this;
        }
    }
}

And here we have the relevant part to create the table:

    costTableModel = new CostTableModel(costCalc);
    table = new JTable(costTableModel);
    table.setPreferredScrollableViewportSize(table.getPreferredSize());
    table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
    table.setCellSelectionEnabled(true);

    scrollPane = new JScrollPane(table);

    RowNameTable nameTable = new RowNameTable(table);
    scrollPane.setRowHeaderView(nameTable);

And the class costTableModel, just for completeness sake:

public class CostTableModel extends AbstractTableModel {
    private CostCalculator costCalc;

    public CostTableModel(CostCalculator costCalc) {
        this.costCalc = costCalc;
    }

    @Override
    public int getRowCount() {
        return costCalc.getPersonsList().size();
    }

    @Override
    public int getColumnCount() {
        return costCalc.getPersonsList().size();
    }

    @Override
    public String getColumnName(int col) {
        return costCalc.getPersonsList().get(col).getName();
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        Person debtor = costCalc.getPersonsList().get(rowIndex);
        Person debtee = costCalc.getPersonsList().get(columnIndex);

        return costCalc.getAmountOwed(debtor, debtee);
    }

    @Override
    public Class getColumnClass(int c) {
        return getValueAt(0, c).getClass();

    }
}

Thank you for your help in advance!

回答1:

The basic issue I had was there was no connection between the table header and the selection change. In fact, the header is really clever with it's repaints...

I ended up providing my own header, which attached a listener to the table's selection model and repainted the header on the selection changed.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.util.List;
import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.RowSorter;
import javax.swing.RowSorter.SortKey;
import static javax.swing.SortOrder.ASCENDING;
import static javax.swing.SortOrder.DESCENDING;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;

public class TestColumnHighlight {

    public static void main(String[] args) {
        new TestColumnHighlight();
    }

    public TestColumnHighlight() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                JTable table = new JTable();
                DefaultTableModel model = new DefaultTableModel(
                                new Object[]{"abc", "def", "ghi", "jkl"},
                                0);

                model.addRow(new Object[]{0, 0, 0, 0});
                model.addRow(new Object[]{0, 0, 0, 0});
                model.addRow(new Object[]{0, 0, 0, 0});
                model.addRow(new Object[]{0, 0, 0, 0});
                model.addRow(new Object[]{0, 0, 0, 0});

                table.setModel(model);
                table.setTableHeader(new CustomTableHeader(table));
                table.getTableHeader().setDefaultRenderer(new ColumnHeaderRenderer());

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new JScrollPane(table));
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class CustomTableHeader extends JTableHeader {

        public CustomTableHeader(JTable table) {
            super();
            setColumnModel(table.getColumnModel());
            table.getColumnModel().getSelectionModel().addListSelectionListener(new ListSelectionListener() {
                @Override
                public void valueChanged(ListSelectionEvent e) {
                    repaint();
                }
            });
        }

        @Override
        public void columnSelectionChanged(ListSelectionEvent e) {
            repaint();
        }

    }

    public class ColumnHeaderRenderer extends DefaultTableHeaderCellRenderer {

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean selected, boolean focused, int row, int column) {
            super.getTableCellRendererComponent(table, value, selected, focused, row, column);

            int selectedColumn = table.getSelectedColumn();
            System.out.println("Selected " + selectedColumn + "-" + column);
            if (selectedColumn == column) {
                Color bg = table.getSelectionBackground();
                setBackground(bg);
                setOpaque(true);
            } else {
                setOpaque(false);
            }

            return this;
        }

    }

    public class DefaultTableHeaderCellRenderer extends DefaultTableCellRenderer {

        public DefaultTableHeaderCellRenderer() {
            setHorizontalAlignment(CENTER);
            setHorizontalTextPosition(LEFT);
            setVerticalAlignment(BOTTOM);
            setOpaque(false);
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value,
                        boolean isSelected, boolean hasFocus, int row, int column) {
            super.getTableCellRendererComponent(table, value,
                            isSelected, hasFocus, row, column);
            JTableHeader tableHeader = table.getTableHeader();
            if (tableHeader != null) {
                setForeground(tableHeader.getForeground());
            }
            setIcon(getIcon(table, column));
            setBorder(UIManager.getBorder("TableHeader.cellBorder"));
            return this;
        }

        protected Icon getIcon(JTable table, int column) {
            SortKey sortKey = getSortKey(table, column);
            if (sortKey != null && table.convertColumnIndexToView(sortKey.getColumn()) == column) {
                switch (sortKey.getSortOrder()) {
                    case ASCENDING:
                        return UIManager.getIcon("Table.ascendingSortIcon");
                    case DESCENDING:
                        return UIManager.getIcon("Table.descendingSortIcon");
                }
            }
            return null;
        }

        protected SortKey getSortKey(JTable table, int column) {
            RowSorter rowSorter = table.getRowSorter();
            if (rowSorter == null) {
                return null;
            }

            List sortedColumns = rowSorter.getSortKeys();
            if (sortedColumns.size() > 0) {
                return (SortKey) sortedColumns.get(0);
            }
            return null;
        }
    }
}


回答2:

A slight variant: as I read the question, the main problem is the header not updating on column selection change. Having a custom header listen to row selection changes doesn't help much for that scenario.

In fact, a JTableHeader already is listening to the ColumnModel and the model's change notification includes selection changes. Only the columnSelectionChange method is intentionally implemented to do nothing:

// --Redrawing the header is slow in cell selection mode.
// --Since header selection is ugly and it is always clear from the
// --view which columns are selected, don't redraw the header.

A custom header can simply implement to repaint (here lazy me does it in the table's factory method just to spare me the wiring to the table, you can easily make it a stand-alone class :-).

final JTable table = new JTable(new AncientSwingTeam()) {

    @Override
    protected JTableHeader createDefaultTableHeader() {
        // subclassing to take advantage of super's auto-wiring
        // as ColumnModelListener
        JTableHeader header = new JTableHeader(getColumnModel()) {

            @Override
            public void columnSelectionChanged(ListSelectionEvent e) {
                repaint();
            }

        };
        return header;
    }

};
table.setCellSelectionEnabled(true);
table.getTableHeader().setDefaultRenderer(new ColumnHeaderRenderer());

Also tweaked Mad's renderer a bit, using table api:

/**
 * Slightly adjusted compared to @Mad
 * - use table's selectionBackground
 * - use table's isColumnSelected to decide on highlight
 */
public static class ColumnHeaderRenderer extends DefaultTableCellHeaderRenderer {

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, boolean selected,  
        boolean focused, int row, int column) {
        super.getTableCellRendererComponent(table, value, selected, focused, row, column); 
        if (table.isColumnSelected(column)) {
            setBackground(table.getSelectionBackground());
        }
        return this;
    }
}

As to the observation:

always says isSelected is false

The reason is a slight quirk in BasicTableHeaderUI:

ui.selected != columnModel.selected

uiSelected is the column which will be accessible to keybindings - if the laf supports it and the header is focusOwner. Doesn't really make sense to me, but fully defining the semantics of ui and columnModel selection fell into the excitement about the new babe fx, that is got forgotten ;-)