Search code examples
javaswingjtablecursorselector

Displaying a row selector (cursor / pointer) in a JTable


I'm migrating an MS-Access app to Java. My concern is about data grids. My first one is ok (JTable in a JScrollPane), but lacks the row selector (cursor) that you can see in MS-Access grids or OpenOffice Base grids. By "row selector", I mean the little black arrow at the left hand side of the row. Is there a standard way to achieve this visual effect with JTable. (I'd also like to know the rationale behind this missing feature : what problem (if any) was Sun trying to avoid by not implementing it ?).

Thanks.

-- EDIT : Camickr, I've used your snippet with the modifications you showed in your edit. It worked great ! Thank you !!!

import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

/*
 *    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 RowNumberTable extends JTable
    implements ChangeListener, PropertyChangeListener, TableModelListener
{
    private JTable main;

    public static void main(String[] args) {
        JTable mainTable = new JTable(new MyTableModel());
        mainTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        JScrollPane scrollPane = new JScrollPane(mainTable);
        RowNumberTable rowTable = new RowNumberTable(mainTable);
        rowTable.getSelectionModel()
            .addListSelectionListener(rowTable.new RowListener());
        scrollPane.setRowHeaderView(rowTable);
        scrollPane.setCorner(JScrollPane.UPPER_LEFT_CORNER, rowTable.getTableHeader());

        // Create a panel to hold all other components
        JPanel topPanel = new JPanel();
        topPanel.setLayout( new BorderLayout() );
        topPanel.add( scrollPane, BorderLayout.CENTER );

        // Set the frame characteristics
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setTitle( "Selector" );
        frame.setBackground( Color.gray );
        frame.getContentPane().add( topPanel );
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public RowNumberTable(JTable table)
    {
        main = table;
        main.addPropertyChangeListener( this );
        main.getModel().addTableModelListener( this );

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

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

        getColumnModel().getColumn(0).setPreferredWidth(22);
        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)
    {
        int rowHeight = main.getRowHeight(row);

        if (rowHeight != super.getRowHeight(row))
        {
            super.setRowHeight(row, rowHeight);
        }

        return rowHeight;
    }

    /*
     *  No model is being used for this table so just use the row number
     *  as the value of the cell.
     */
    @Override
    public Object getValueAt(int row, int column)
    {
        //return Integer.toString(row + 1);
        if (main.isRowSelected(row))
            return "\u25BA"; // Unicode Black Right-pointing Pointer
        else
            return " ";
    }

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

    /*
     *  Do nothing since the table ignores the model
     */
    @Override
    public void setValueAt(Object value, int row, int column) {}

    //
    //  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 ("rowHeight".equals(e.getPropertyName()))
        {
            repaint();
        }

        if ("model".equals(e.getPropertyName()))
        {
            main.getModel().addTableModelListener( this );
            revalidate();
        }
    }

    //
    //  Implement the TableModelListener
    //
    @Override
    public void tableChanged(TableModelEvent e)
    {
        revalidate();
    }

    /*
     *  Attempt to mimic the table header renderer
     */
    private static class RowNumberRenderer extends DefaultTableCellRenderer
    {
        public RowNumberRenderer()
        {
            setHorizontalAlignment(JLabel.CENTER);
        }

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

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

            return this;
        }
    }

    private class RowListener implements ListSelectionListener {
        public void valueChanged(ListSelectionEvent event) {
            if (event.getValueIsAdjusting()) {
                return;
            }
            int row =  main.getSelectedRow();
            System.out.println("selected row : "  +
                main.getSelectionModel().getLeadSelectionIndex() + " - " +
                main.getModel().getValueAt(row, 0) ); 
        }
    }
}

class MyTableModel extends AbstractTableModel {
    private String[] columnNames = {"First Name",
        "Last Name",
        "Sport",
        "# of Years",
        "Vegetarian"};
    private Object[][] data = {
        {"Kathy", "Smith",
            "Snowboarding", new Integer(5), new Boolean(false)},
        {"John", "Doe",
            "Rowing", new Integer(3), new Boolean(true)},
        {"Sue", "Black",
            "Knitting", new Integer(2), new Boolean(false)},
        {"Jane", "White",
            "Speed reading", new Integer(20), new Boolean(true)},
        {"Joe", "Brown",
            "Pool", new Integer(10), new Boolean(false)}
        };

    public int getColumnCount() {
        return columnNames.length;
    }

    public int getRowCount() {
        return data.length;
    }

    public String getColumnName(int col) {
        return columnNames[col];
    }

    public Object getValueAt(int row, int col) {
        return data[row][col];
    }

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

Solution

  • You would need to add a "row header" to the scroll pane.

    Check out Row Number Table for an example of this approach. The default implementation displays row numbers with the currently selected line bold.

    You could customize the renderer to display an "arrow icon" instead of the bold if you want.

    Edit:

    The only change you need to make to the original code is the following:

    @Override
    public Object getValueAt(int row, int column)
    {
        //return Integer.toString(row + 1);
        if (main.isRowSelected(row))
            return "\u25BA";
        else
            return " ";
    }
    

    There is no need for a ListSelectionListener. You can just query the row selection when the row is rendered.

    There is no need to pass a TableModel to the constructor of the RowTableModel.