Jtable Model Listener Detects Inserted Rows Too Soon (Before They Are Drawn)

JTable model listener detects inserted rows too soon (before they are drawn)

This example uses scrollRectToVisible() to (conditionally) scroll to the last cell rectangle. As a feature you can click on the thumb to suspend scrolling and release to resume.

private void scrollToLast() {
if (isAutoScroll) {
int last = table.getModel().getRowCount() - 1;
Rectangle r = table.getCellRect(last, 0, true);
table.scrollRectToVisible(r);
}
}

Addendum: I tried scrollRectToVisible in my SSCCE, and it still exhibits the same problem.

This Action provides both mouse and keyboard control:

JButton btnAddRow = new JButton(new AbstractAction("Add Row") {

@Override
public void actionPerformed(ActionEvent e) {
tableModel.addRow(new Object[]{"new row"});
int last = table.getModel().getRowCount() - 1;
Rectangle r = table.getCellRect(last, 0, true);
table.scrollRectToVisible(r);
}
});

Sample Image

Addendum: Here's a variation on your example that illustrates a revised layout strategy.

/** @see https://stackoverflow.com/a/14429388/230513 */
public class TableListenerTest {

private static final int N = 8;
private JFrame frame;
private JScrollPane scrollPane;
private JTable table;
private DefaultTableModel tableModel;

public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {

@Override
public void run() {
TableListenerTest window = new TableListenerTest();
window.frame.setVisible(true);
}
});
}

public TableListenerTest() {
initialize();
}

private void initialize() {
frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.X_AXIS));
tableModel = new DefaultTableModel(new Object[]{"Stuff"}, 0);
for (int i = 0; i < N; i++) {
tableModel.addRow(new Object[]{"new row"});
}
table = new JTable(tableModel) {

@Override
public Dimension getPreferredScrollableViewportSize() {
return new Dimension(200, table.getRowHeight() * N);
}
};
scrollPane = new JScrollPane();
scrollPane.setViewportView(table);
JButton btnAddRow = new JButton(new AbstractAction("Add Row") {

@Override
public void actionPerformed(ActionEvent e) {
tableModel.addRow(new Object[]{"new row"});
int last = table.getModel().getRowCount() - 1;
Rectangle r = table.getCellRect(last, 0, true);
table.scrollRectToVisible(r);
}
});
frame.add(scrollPane);
frame.add(btnAddRow);
frame.pack();
}
}

JTable won't dynamically add rows

Your issue might be due to setting the scroll pane's size to 0. You should delete table.setPreferredScrollableViewportSize(table.getPreferredSize()) or set a preferred size of your table by table.setPreferredSize() before you use table.getPreferredSize().

Large gap between JTables

Absent a complete example, it's hard to be sure what you're seeing. Given that one should avoid setPreferredSize(), note that JTable implements Scrollable and can calculate it's own preferred size. Unfortunately, the arbitrary Dimension calculated may be causing the problem. Overriding getPreferredScrollableViewportSize() to return a multiple of getRowHeight(), for example, may be a useful alternative.

Scroll to last added row in JTable

Scrolling to a newly inserted row in a potentially sorted table involves

  • listening to changes in the table model
  • converting the rowIndex of the event (it is in model coordiates) to view coordinates
  • scrolling to the view position

In code:

final JTable table = new JTable();
TableModelListener l = new TableModelListener() {

@Override
public void tableChanged(TableModelEvent e) {
if (TableUtilities.isInsert(e)) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
int viewRow = table.convertRowIndexToView(e.getFirstRow());
table.scrollRectToVisible(table.getCellRect(viewRow, 0, true));
}
});
}
}
};
table.getModel().addTableModelListener(l);

Two important aspects:

  • your model implemenation must fire the correct event, that is a insert, not a dataChanged
  • invoking both index conversion and scrolling guarantees that the table (which is listening to the model as well) has updated all internal state according to the model change.

Unable to change location and size of JTable

The reason why the table component is not resizing is that you set a so called null layout manager with the setLayout(null). Setting this means that you should take care of locating and resizing components in your GUI.
This is what the layout managers do.

Once you use a layout manager, such as GroupLayout, the example works as
expected.

package com.zetcode;

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import javax.swing.GroupLayout;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.TableCellRenderer;

public class HotelReservationSystemEx extends JFrame {

private JTable table;

public HotelReservationSystemEx() {

initUI();
}

private void initUI() {

String[] columns = {"Name", "Age", "Gender"};

String[][] data = {{"John", "18", "Male"},
{"Daisy", "19", "Female"},
{"Dave", "23", "Male"},
{"Jake", "30", "Male"}};

table = new JTable(data, columns) {
@Override
public boolean isCellEditable(int data, int columns) {
return false;
}

@Override
public Component prepareRenderer(TableCellRenderer r, int data, int columns) {
Component c = super.prepareRenderer(r, data, columns);

if ((data % 2 == 0)) {
c.setBackground(Color.WHITE);
} else {
c.setBackground(Color.LIGHT_GRAY);
}

return c;
}
};

//table.setFillsViewportHeight(true);

JScrollPane spane = new JScrollPane(table);

createLayout(spane);

setTitle("Hotel reservation system");
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
}

private void createLayout(JComponent... arg) {

Container pane = getContentPane();
GroupLayout gl = new GroupLayout(pane);
pane.setLayout(gl);

gl.setAutoCreateContainerGaps(true);

gl.setHorizontalGroup(gl.createParallelGroup()
.addComponent(arg[0])
);

gl.setVerticalGroup(gl.createSequentialGroup()
.addComponent(arg[0])
);

pack();
}

public static void main(String[] args) {

SwingUtilities.invokeLater(() -> {
HotelReservationSystemEx ex = new HotelReservationSystemEx();
ex.setVisible(true);
});
}
}

In addition, you are unnecessary creating a JPanel and instantiating
the JTable twice.

Here is the screenshot:

Screenshot from the example

JTable sizing issue

  1. JTable can't returns proper Dimension or PreferredSize, there are three ways

  2. table.setPreferredScrollableViewportSize(table.getPreferredSize()); but notice for small JTables with a few Rows and Columns too

  3. to calculate desired size for (part) of Columns and (part) Rows too, then pass this Dimension in form table.setPreferredScrollableViewportSize(new Dimension(x, y));

  4. override getPreferredSize for JScrollPane

  5. then JFrame.pack(before JFrame.setVisible(true)) to calculate desired Size on the screen

  6. JPanel has FlowLayout implemented in API, I'd to suggest to change to BorderLayout, then JScrollPane in CENTER area can fill whole (available) area and will be resizable with JFrame, not possible to resize JComponent (together with its container) layed by FlowLayout

  7. have to call data.revalidate(), data.repaint() and Show.pack() as last code lines instead of (remove this code line) Show.setVisible(true);

  8. rename Show to myFrame and show to myButton

JTable height calculation

You can substitute your own implementation of Scrollable to get a variety of effects. In particular, override getPreferredScrollableViewportSize() and return a suitable multiple of the row height. Let N be the number of rows after which you want the scrollbar to appear.

JTable table = new JTable(tableModel) {

@Override
public Dimension getPreferredScrollableViewportSize() {
return new Dimension(SOME_WIDTH, N * table.getRowHeight());
}
};

Add the table to a scroll pane:

JScrollPane sp = new JScrollPane(table);

Add the pane to a panel having a layout that respects preferred size:

JPanel p = new JPanel(new GridLayout());
p.add(sp);

A table having more than N rows will display scroll bars, as shown here.

I just want to find the actual table height [in the context of] unit tests from the huge project.

I don't know exactly how your total height calculation fails, but the result will inevitably change based on the user's platform, the chosen look & feel, and the component validation state; an erroneous approach is suggested here. Instead, attempt to meet the test requirement by ensuring that a paricular number of rows is visible using the approach outlined above.

Jtable - scroll to bottom with variable height rows

Is there a listener I can override, or an event I can listen to, that will happen after the table has been redrawn with the new rows at the bottom?

Is there an event I can listen to to know when the height has been reset on the table, so I can then scrollToBottom?

Yes. Found in a parent class of JTable, Component#addComponentListener( ComponentListener ) where I can override the componentResized method.

JAR Bundler using OSXAdapter causing application to lag or terminate

After doing it, I'm not fully convinced a SwingWorker is a simpler (aka: better) solution - still requires additional thread synching (between the worker thread and the "outer" thread which passes in the file/names). Anyway (taking the opportunity to learn, and be it by errors :), below is a crude proof of concept example for the basic idea:

  • implement the Controller as SwingWorker, which funnels the input from the outer thread into the EDT
  • make it accept input (from the adapter, f.i.) via a method doWork(..) which queues the input for publishing
  • implement doInBackground to succesively publish the input

open issues

  • synch the access to the local list (not an expert in concurrency, but pretty sure that needs to be done)
  • reliably detecting the end of the outer thread (here simply stops when the input queue is empty)

Feedback welcome :-)

public class GUI {
private JFrame frame = new JFrame();
private DefaultTableModel model = new DefaultTableModel();
private JTable table = new JTable(model);
private JScrollPane pane = new JScrollPane(table);

public GUI() {
model.addColumn("Name");

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(pane);
frame.pack();
frame.setVisible(true);
}

public void addRow(String name) {
model.addRow(new Object[] { name });
}

/**
* Controller is a SwingWorker.
*/
public static class Controller extends SwingWorker<Void, String> {
private GUI gui;

private List<String> pending;

public Controller() {
gui = new GUI();
}

public void doWork(String newLine) {
if (pending == null) {
pending = new ArrayList<String>();
pending.add(newLine);
execute();
} else {
pending.add(newLine);
}
}

@Override
protected Void doInBackground() throws Exception {
while (pending.size() > 0) {
publish(pending.remove(0));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return null;
}

/**
* @inherited <p>
*/
@Override
protected void process(List<String> chunks) {
for (String object : chunks) {
gui.addRow(object);
}
}

}

/**
* Simulating the adapter.
*
* Obviously, the real-thingy wouldn't have a reference
* to the controller, but message the doWork refectively
*/
public static class Adapter implements Runnable {

Controller controller;

public Adapter(Controller controller) {
this.controller = controller;
}

@Override
public void run() {
for (int i=0; i<10; i++)
{
controller.doWork("Line "+(i+1));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

}
public static void main(String[] args)
{
System.err.println("Initializing controller");
new Adapter(new Controller()).run();
}

@SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(GUI.class.getName());
}


Related Topics



Leave a reply



Submit