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