Sluggish Performance Using JTable Displaying Streaming Data - java
The code I'm referencing is proprietary and requires a multicast server, so I can't post an SSCCE snippet. I understand this may preclude any helpful insight which would elicit viable responses...
I'm compiling with Java 7 u 9.
I'm currently using a JTable in a GUI app that listens to multicast data, and displays it as it arrives. When scrolling the table or resizing columns, the app responds excruciatingly slow.
I thought I structured my code appropriately.
I used a wrapper class, and in it's main() function, it creates an instance of itself, which processes command line arguments, creates listeners, creates the JFrame and calls the class that returns a JTable. This is all done outside of the event dispatch thread.
Then, in the next line, I used the invokeLater() method to create a thread that handles all of the GUI rendering. It creates a JScrollPane, adds the JTable to the scroll pane, sets the scroll bars, sets the viewport, sets the scroll mode, and adds the JScrollPane to a JFrame . This is all handled within the event dispatch thread.
The rows typically populate fairly quick, with the occasional screen freeze (some of the rows contain 30 or more lines), but the responsiveness seems acceptable. However, when scrolling or resizing columns, the response is very slow.
All of the examples I've seen, including the SwingX SwingLabs demo all refer to an initial dataset that is loaded up front. I need an example of using a JTable with streaming data.
Can anyone point me to such an example/demo?
This is my main() snippet...
public static void main(String args[])
{
final JXTrapTableGUI ttg = new JXTrapTableGUI(args);
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
ttg.createAndShowGUI();
}
});
}
PS. I want to thank each and everyone who responded. I've been pulled off this project until March 11th, but I will review all responses on that date.
I do not think that JTable works well with streaming data at all. All of the optimization techniques you mentioned, like keeping processing off of the Event Dispatch Thread, are irrlevant if the TableModel does not contain a real list but instead some connection to a data stream.
Without seeing how you attempted to handle this, it's hard to know exactly why it's slow. But here is how I would make it repsonsive:
Create a ListModel that stores a List - not a reference to a stream, just a plain List.
Have another Thread capture Multicast Data from the stream, lets call it DataStreamCollector.
Then launch a Thread that runs on a timer (javax.swing.Timer) that checks with the DataStreamCollector and updates the ListModel as needed.
My design here is assuming that UI responsiveness is more important than 100% synchronization with the data stream. Adjusting the timers should let you trade off having an up-to-date table with having a responsive UI.
Somewhere lost in Oracles clean_up to the trash (old Suns tutorials),
This project was called ChristmastTree, is about JTable & Performance,
Standard Java code before crazy & messy SwingWorker invoked from black hole called Executor
import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.basic.*;
import javax.swing.table.*;
/**
* CTTable extends JTable doing the following: <ul> <li>The UI is forced to be
* CTTableUI so that a customer CellRendererPane can be installed.
* <li>getCellRenderer is overriden to return the TableCellRenderer passed into
* the constructor. <li>tableChanged is overriden to pass the call to super only
* if the cell is visible. </ul>
*/
public class CTTable extends JTable {
private static final long serialVersionUID = 1L;
private CTTableCellRenderer renderer;
public CTTable(CTTableCellRenderer renderer) {
super();
this.renderer = renderer;
renderer.setFont(getFont());
}
#Override
public void updateUI() {
super.updateUI();
//Force the UI to be an instanceof CTTableUI. This approach will not work
//if you need to support more than one look and feel in your application.
setUI(new CTTableUI());
}
#Override
public void setFont(Font font) {
super.setFont(font);
if (renderer != null) {
renderer.setFont(font);
}
}
#Override
public TableCellRenderer getCellRenderer(int row, int column) {
return renderer;
}
#Override
public void tableChanged(TableModelEvent e) {
if (e instanceof VisibleTableModelEvent && !((VisibleTableModelEvent) e).isVisible(this)) {
return;// Do nothing if this cell isn't visible.
}
super.tableChanged(e);
}
private static class CTTableUI extends BasicTableUI {
#Override
public void installUI(JComponent c) {
super.installUI(c);// Overriden to install our own CellRendererPane
c.remove(rendererPane);
rendererPane = new CTCellRendererPane();
c.add(rendererPane);
}
}
/**
* CTCellRendererPane overrides paintComponent to NOT clone the Graphics
* passed in and NOT validate the Component passed in. This will NOT work if
* the painting code of the Component clobbers the graphics (scales it,
* installs a Paint on it...) and will NOT work if the Component needs to be
* validated before being painted.
*/
private static class CTCellRendererPane extends CellRendererPane {
private static final long serialVersionUID = 1L;
private Rectangle tmpRect = new Rectangle();
#Override
public void repaint() {
// We can safely ignore this because we shouldn't be visible
}
#Override
public void repaint(int x, int y, int width, int height) {
}
#Override
public void paintComponent(Graphics g, Component c, Container p, int x,
int y, int w, int h, boolean shouldValidate) {
if (c == null) {
if (p != null) {
Color oldColor = g.getColor();
g.setColor(p.getBackground());
g.fillRect(x, y, w, h);
g.setColor(oldColor);
}
return;
}
if (c.getParent() != this) {
this.add(c);
}
c.setBounds(x, y, w, h);
// As we are only interested in using a JLabel as the renderer,
//which does nothing in validate we can override this to do nothing,
//if you need to support components that can do layout, this will
//need to be commented out, or conditionally validate.
shouldValidate = false;
if (shouldValidate) {
c.validate();
}
boolean wasDoubleBuffered = false;
JComponent jc = (c instanceof JComponent) ? (JComponent) c : null;
if (jc != null && jc.isDoubleBuffered()) {
wasDoubleBuffered = true;
jc.setDoubleBuffered(false);
}//Don't create a new Graphics, reset the clip and translate the origin.
Rectangle clip = g.getClipBounds(tmpRect);
g.clipRect(x, y, w, h);
g.translate(x, y);
c.paint(g);
g.translate(-x, -y);
g.setClip(clip.x, clip.y, clip.width, clip.height);
if (wasDoubleBuffered) {
jc.setDoubleBuffered(true);
}
c.setBounds(-w, -h, 0, 0);
}
}
}
.
import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.table.*;
/**
* A custom TableCellRenderer that overrides a handful of methods: <ul>
* <li>isOpaque and setBackground are overridden to avoid filling the
* background, if possible. <li>firePropertyChange is overridden to do nothing.
* If you need to support HTML text in the renderer than this should NOT be
* overridden. <li>paint is overridden to forward the call directly to the UI,
* avoiding the creation of a Graphics. This will NOT work if you need the
* renderer to contain other childre or the Graphics is clobbered as part of
* painting the UI. </ul>
*/
public class CTTableCellRenderer extends DefaultTableCellRenderer {
private static final long serialVersionUID = 1L;
private Color background;
private Color foreground;
private Color editableForeground;
private Color editableBackground;
private Border focusBorder;
public CTTableCellRenderer() {
focusBorder = UIManager.getBorder("Table.focusCellHighlightBorder");
editableForeground = UIManager.getColor("Table.focusCellForeground");
editableBackground = UIManager.getColor("Table.focusCellBackground");
}
#Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
boolean negative = (value != null && ((Integer) value).intValue() < 0);
// Reset the background based on the sign of the value.
if (isSelected) {
setForeground(table.getSelectionForeground());
setBackground(table.getSelectionBackground());
} else {
setForeground(table.getForeground());
if (!negative) {
setBackground(null);
} else {
setBackground(Color.red);
}
}//NOTICE that we do NOT set the font here, because CTTable knows about
//us, it will set the font as appropriate.
if (hasFocus) {
setBorder(focusBorder);
if (table.isCellEditable(row, column)) {
setForeground(editableForeground);
setBackground(editableBackground);
}
} else {
setBorder(noFocusBorder);
}
setValue(value);
return this;
}
#Override
protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
// As long as you don't have any HTML text, this override is ok.
}
#Override// This override is only appropriate if this will never contain
// any children AND the Graphics is not clobbered during painting.
public void paint(Graphics g) {
ui.update(g, this);
}
#Override
public void setBackground(Color c) {
this.background = c;
}
#Override
public Color getBackground() {
return background;
}
#Override
public void setForeground(Color c) {
this.foreground = c;
}
#Override
public Color getForeground() {
return foreground;
}
#Override
public boolean isOpaque() {
return (background != null);
}
#Override // This is generally ok for non-Composite components (like Labels)
public void invalidate() {
}
#Override // Can be ignored, we don't exist in the containment hierarchy.
public void repaint() {
}
}
.
import javax.swing.table.*;
import java.util.*;
/**
* CTTableModel, a TableModel, models a set of Datas as the rows. The data is
* stored in a List of Lists. As the changes come in against a particular Data
* object we also contain a map from Data to row. This can obviously be made
* faster by pushing the row to the Data, but this may not be feasable in
* applications of this sort.
*/
public class CTTableModel extends AbstractTableModel {
private static final long serialVersionUID = 1L;
/**
* Maps from Data to an integer id giving the row of the data.
*/
private Map rowMap;
/**
* Number of columns to display.
*/
private int columns;
/**
* A List of Lists.
*/
private java.util.List rowData;
/**
* If true, batch cell updates using sharedModelEvent.
*/
private boolean batchChange;
/**
* Shared model event.
*/
private VisibleTableModelEvent sharedModelEvent;
public CTTableModel(int columns) {
this.columns = columns;
// Notice how they are not synchronized, we do NOT access this class
// from another thread, and therefore do not have to worry about
// synchronization.
rowData = new ArrayList();
rowMap = new HashMap();
}
public void addRow(Data rowID) {
int row = rowData.size();
rowMap.put(rowID, new Integer(row));
ArrayList colData = new ArrayList();
for (int counter = 0; counter < columns; counter++) {
colData.add(null);
}
rowData.add(colData);
fireTableRowsInserted(row, row);
}
/**
* Toggles batch updates. When true and model changes are notified using a
* VisibleTableModelEvent.
*
* #param batch
*/
public void setBatchUpdates(boolean batch) {
this.batchChange = batch;
if (sharedModelEvent == null) {
sharedModelEvent = new VisibleTableModelEvent(this);
}
sharedModelEvent.reset();
}
public boolean getBatchUpdates() {
return batchChange;
}
/**
* Sets the display value for a particular Data item at a particular cell.
* If notify is true listeners are notified, otherwise no listeners are
* notified.
*
* #param rowID
* #param col
* #param data
* #param notify
*/
public void set(Data rowID, int col, Object data, boolean notify) {
int row = ((Integer) rowMap.get(rowID)).intValue();
((java.util.List) rowData.get(row)).set(col, data);
if (notify) {
if (batchChange) {
sharedModelEvent.set(row, col);
fireTableChanged(sharedModelEvent);
} else {
fireTableCellUpdated(row, col);
}
}
}
#Override
public int getRowCount() {
return rowData.size();
}
#Override
public int getColumnCount() {
return columns;
}
#Override
public Object getValueAt(int rowIndex, int columnIndex) {
return ((java.util.List) rowData.get(rowIndex)).get(columnIndex);
}
}
.
/**
* Unique ID for the data.
*/
public class Data {
/**
* This is overriden to remind developers they should have an intelligent
* equals and hashCode. You do not need to override either of them, but if
* you override one you need to override the other. Additionaly, because
* they are used extensively to map the data that has changed to the table,
* equals and hashCode MUST be fast, cache data if you need to!
*
* #param x
*/
#Override
public boolean equals(Object x) {
return (this == x);
}
/**
* This is overriden to remind developers they should have an intelligent
* equals and hashCode. You do not need to override either of them, but if
* you override one you need to override the other. Additionaly, because
* they are used extensively to map the data that has changed to the table,
* equals and hashCode MUST be fast, cache data if you need to!
*/
#Override
public int hashCode() {
return super.hashCode();
}
}
.
import java.util.ArrayList;
/**
* DataChange is used to associate a Data Object with a column identifier that
* has changed. To avoid loads of garbage per update DataChanges are cached and
* reused.
*/
public class DataChange {
private static ArrayList sharedDataChanges = new ArrayList();
private Data data;
private int col;
private int hashCode;
/**
* Obtains a DataChange for the specified Data and column.
*
* #param data
* #param col
* #return
*/
public static DataChange getDataChange(Data data, int col) {
synchronized (sharedDataChanges) {
int size = sharedDataChanges.size();
if (size > 0) {
DataChange change = (DataChange) sharedDataChanges.remove(size - 1);
change.data = data;
change.col = col;
return change;
}
}
return new DataChange(data, col);
}
/**
* Indicates the DataChange is no longer needed and can be reused.
*
* #param change
*/
public static void releaseDataChange(DataChange change) {
synchronized (sharedDataChanges) {
sharedDataChanges.add(change);
}
}
DataChange(Data data, int col) {
this.data = data;
this.col = col;
hashCode = (data.hashCode() | col);
}
public Data getData() {
return data;
}
public int getColumn() {
return col;
}
#Override
public int hashCode() {
return hashCode;
}
public boolean equals(DataChange dc) {
if (dc == this) {
return true;
}
DataChange o = (DataChange) dc;
return (o.data == data && o.col == col);
}
}
.
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
/**
* The Main controller, responsible for wiring everything together. Pressing
* return in any of the fields will trigger recreation of everything.
*/
public class Main implements ActionListener {
// properties: columnCount, rowCount, updateSleepTime, eqSleepTime,
// threshold, generateSleep, generatorBatchCount
private static final Insets EMPTY_INSETS = new Insets(0, 0, 0, 0);
private JTextField columnCount;
private JTextField rowCount;
private JTextField updateSleepTime;
private JTextField eqSleepTime;
private JTextField threshold;
private JTextField generateSleep;
private JTextField generatorBatchCount;
private JFrame frame;
static JLabel totalUpdateTime;
static JLabel notifyTime;
static JLabel paintTime;
static JLabel updateCount;
private JTable table;
private UpdateThread updateThread;
private GeneratorThread generatorThread;
private CTTableModel tableModel;
private static int NUM_COLUMNS = 40;// Initial values for the 7 properties.
private static int NUM_ROWS = 3000;
private static int UPDATE_SLEEP_TIME = 500;
private static int EQ_SLEEP_TIME = 10;
private static int UPDATE_ALL_THRESHOLD = 400000;
private static int GENERATOR_SLEEP_TIME = 40;
private static int BATCH_SIZE = 1000;
Main() {
frame = new JFrame();
frame.getContentPane().setLayout(new GridBagLayout());
columnCount = add("Columns: ", NUM_COLUMNS);
rowCount = add("Rows: ", NUM_ROWS);
updateSleepTime = add("Update Sleep: ", UPDATE_SLEEP_TIME);
eqSleepTime = add("EQ Sleep: ", EQ_SLEEP_TIME);
threshold = add("Update All Threshold: ", UPDATE_ALL_THRESHOLD);
generateSleep = add("Generator Sleep: ", GENERATOR_SLEEP_TIME);
generatorBatchCount = add("Batch Size: ", BATCH_SIZE);
table = new CTTable(new CTTableCellRenderer());
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
JScrollPane sp = new JScrollPane(table);
frame.getContentPane().add(sp, new GridBagConstraints(0, 3, 6, 1, 1, 1,
GridBagConstraints.WEST, GridBagConstraints.BOTH, EMPTY_INSETS, 0, 0));
ChangeListener changeListener = new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
BoundedRangeModel m = (BoundedRangeModel) (e.getSource());
if (updateThread != null) {
updateThread.setUpdatesEnabled(!(m.getValueIsAdjusting()));
}
}
};
sp.getVerticalScrollBar().getModel().addChangeListener(changeListener);
sp.getHorizontalScrollBar().getModel().addChangeListener(changeListener);
totalUpdateTime = new JLabel(" ");
notifyTime = new JLabel(" ");
paintTime = new JLabel(" ");
updateCount = new JLabel(" ");
JPanel statusPanel = new JPanel(new GridBagLayout());
frame.getContentPane().add(statusPanel, new GridBagConstraints(0, 4, 6, 1, 1, 0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, EMPTY_INSETS, 0, 0));
statusPanel.add(totalUpdateTime, new GridBagConstraints(0, 0, 1, 1, 1, 0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, EMPTY_INSETS, 0, 0));
statusPanel.add(notifyTime, new GridBagConstraints(1, 0, 1, 1, 1, 0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, EMPTY_INSETS, 0, 0));
statusPanel.add(paintTime, new GridBagConstraints(2, 0, 1, 1, 1, 0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, EMPTY_INSETS, 0, 0));
statusPanel.add(updateCount, new GridBagConstraints(3, 0, 1, 1, 1, 0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, EMPTY_INSETS, 0, 0));
frame.setTitle("Christmas Tree Demo Application");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setBounds(0, 0, 1000, 800);
frame.setVisible(true);
try {
Thread.sleep(5000);
} catch (InterruptedException ie) {
}
reset();
}
#Override
public void actionPerformed(ActionEvent ae) {
reset();
}
private JTextField add(String name, int defaultValue) {
Container parent = frame.getContentPane();
int row = parent.getComponentCount() / 6;
int col = parent.getComponentCount() % 6;
parent.add(new JLabel(name), new GridBagConstraints(col, row, 1, 1, 0, 0,
GridBagConstraints.WEST, 0, EMPTY_INSETS, 0, 0));
JTextField tf = new JTextField(Integer.toString(defaultValue));
tf.addActionListener(this);
parent.add(tf, new GridBagConstraints(col + 1, row, 1, 1, 1, 0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, EMPTY_INSETS, 0, 0));
return tf;
}
private void reset() {
System.out.println("Columns: " + getInt(columnCount));
System.out.println("Rows: " + getInt(rowCount));
System.out.println("Update Sleep: " + getInt(updateSleepTime));
System.out.println("EQ Sleep: " + getInt(eqSleepTime));
System.out.println("Update All Threshold: " + getInt(threshold));
System.out.println("Generator Sleep: " + getInt(generateSleep));
System.out.println("Batch Size: " + getInt(generatorBatchCount));
if (updateThread != null) {
System.out.println("interrupting!");
updateThread.interrupt();
generatorThread.interrupt();
}
int cols = getInt(columnCount);
tableModel = new CTTableModel(cols);
ArrayList<Data> data = new ArrayList<Data>();
for (int counter = getInt(rowCount) - 1; counter >= 0; counter--) {
Data dataID = new Data();
data.add(dataID);
tableModel.addRow(dataID);
for (int colCounter = 0; colCounter < cols; colCounter++) {
if (colCounter % 2 == 0) {
tableModel.set(dataID, colCounter,
new Integer(counter * 100 + colCounter), false);
} else {
tableModel.set(dataID, colCounter,
new Integer(counter * -100 + colCounter), false);
}
}
}
table.setModel(tableModel);
generatorThread = new GeneratorThread(data, getInt(generateSleep),
getInt(generatorBatchCount), getInt(columnCount));
updateThread = new UpdateThread(generatorThread, tableModel,
getInt(updateSleepTime), getInt(eqSleepTime), getInt(threshold));
generatorThread.start();
updateThread.start();
}
private int getInt(JTextField tf) {
try {
return Integer.parseInt(tf.getText());
} catch (NumberFormatException nfe) {
System.out.println("exception getting int: " + nfe);
}
return 0;
}
public static void main(String[] args) {
Main main = new Main();
}
}
... will be continue
rest of methods
import java.awt.*;
import java.lang.reflect.*;
import java.util.*;
import javax.swing.*;
/**
* Thread responsible for publishing changes to the Model. Sleeps for a defined
* amount of time, waits for no activity in the UI and then users invokeAndWait
* to publish changes.
*/
public class UpdateThread extends Thread {
private int sleepTime;
private int eqSleepTime;
private int threshhold;
private boolean updatesEnabled;
private Runnable publishRunnable;
private Runnable emptyRunnable;
private GeneratorThread generator;
private CTTableModel model;
private Map<?, ?> lastData;
private long notifyTime;
private long paintTime;
private int updateCount;
private boolean done;
public UpdateThread(GeneratorThread generator, CTTableModel model,
int sleepTime, int eqSleepTime, int threshhold) {
super();
setPriority(Thread.MIN_PRIORITY);
this.sleepTime = sleepTime;
this.eqSleepTime = eqSleepTime;
updatesEnabled = true;
this.threshhold = threshhold;
this.generator = generator;
this.model = model;
publishRunnable = new Runnable() {
// Runnable used to publish changes to the event dispatching thread
#Override
public void run() {
publishChangesOnEventDispatchingThread();
}
};
// Empty runnable, used to wait until the event dispatching thread
// has finished processing any pending events.
emptyRunnable = new Runnable() {
#Override
public void run() {
}
};
}
#Override
public void interrupt() {
done = true;
super.interrupt();
}
#Override
public void run() {
while (!isInterrupted() && !done) {
try {
sleep(sleepTime);
publishChanges();
} catch (InterruptedException ie) {
}
}
System.out.println("UpdateThread done");
}
/**
* Publishes changes on the event dispatching thread when the system isn't
* busy. This blocks the caller until the changes have been published.
*/
private void publishChanges() {
synchronized (this) {// Wait until the user isn't scrolling
while (!updatesEnabled) {
try {
wait();
} catch (InterruptedException ie) {
}
}
}
EventQueue queue = Toolkit.getDefaultToolkit().getSystemEventQueue();
// And wait until there are no pending events.
while (queue.peekEvent() != null) {
try {
sleep(eqSleepTime);
} catch (InterruptedException ie) {
}
}
final long start = System.currentTimeMillis();
try {
SwingUtilities.invokeAndWait(publishRunnable);
// publish the changes on the event dispatching thread
} catch (InterruptedException ie) {
} catch (InvocationTargetException ite) {
}
try {
// Wait until the system has completed processing of any events we
// triggered as part of publishing changes.
SwingUtilities.invokeAndWait(emptyRunnable);
} catch (InterruptedException ie) {
} catch (InvocationTargetException ite) {
}
final long end = System.currentTimeMillis();
try {// Update the display
SwingUtilities.invokeAndWait(new Runnable() {
#Override
public void run() {
Main.totalUpdateTime.setText("Total: "
+ Integer.toString((int) (end - start)));
Main.notifyTime.setText("Notify: "
+ Integer.toString((int) notifyTime));
Main.paintTime.setText("Paint: "
+ Integer.toString((int) paintTime));
Main.updateCount.setText("Updated: "
+ Integer.toString((int) updateCount));
}
});
} catch (InterruptedException ie) {
} catch (InvocationTargetException ite) {
}
}
/**
* Does the actual publishing of changes.
*/
private void publishChangesOnEventDispatchingThread() {
long start = System.currentTimeMillis();
model.setBatchUpdates(true);
Map<?, ?> data = generator.getData();
boolean notify = !(data.size() > threshhold ||
(lastData != null && lastData.size() + data.size() > threshhold));
updateCount = data.size();
if (lastData != null) {
updateCount += lastData.size();
}//Reset the data for the last set of changes we did, this forces the cells to change color.
if (lastData != null) {
publishData(lastData, true, notify);
Iterator<?> dataIterator = lastData.keySet().iterator();
while (dataIterator.hasNext()) {
DataChange.releaseDataChange((DataChange) dataIterator.next());
}
lastData.clear();
}
publishData(data, false, notify);// Publish the current set of data.
model.setBatchUpdates(false);
if (!notify) {
model.fireTableDataChanged();
}
lastData = data;
long end = System.currentTimeMillis();
notifyTime = (end - start);
start = System.currentTimeMillis();
RepaintManager.currentManager(null).paintDirtyRegions();
end = System.currentTimeMillis();
paintTime = (end - start);
}
/**
* Publish the passed in set of data.
*/
private void publishData(Map<?, ?> data, boolean negate, boolean notify) {
Iterator<?> dataIterator = data.keySet().iterator();
while (dataIterator.hasNext()) {
DataChange change = (DataChange) dataIterator.next();
Object value = data.get(change);
if (negate) {
value = new Integer(((Integer) value).intValue() * -1);
}
model.set(change.getData(), change.getColumn(), value, notify);
}
}
/**
* If enable is true, we are allowed to publish changes, otherwise we
* aren't.
*
* #param enable
*/
public void setUpdatesEnabled(boolean enable) {
synchronized (this) {
updatesEnabled = enable;
if (updatesEnabled) {
notify();
}
}
}
public boolean getUpdatesEnabled() {
return updatesEnabled;
}
}
.
import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
/**
* VisibleTableModelEvent adds the method isVisible to test if the cell
* identified by the event is visible.
*/
public class VisibleTableModelEvent extends TableModelEvent {
private static final long serialVersionUID = 1L;
private Point tmpPoint;
// This implementation caches the information for one JTable, it is
// certainly possible to cache it for more than one should
// you have this need.
private boolean valid;
private int firstVisRow;
private int lastVisRow;
private int firstVisCol;
private int lastVisCol;
public VisibleTableModelEvent(TableModel source) {
super(source, 0, 0, 0, UPDATE);
tmpPoint = new Point();
}
/**
* Resets the underlying fields of the TableModelEvent. This assumes no ONE
* is going to cache the TableModelEvent.
*
* #param row
* #param col
*/
public void set(int row, int col) {
firstRow = row;
lastRow = row;
column = col;
}
/**
* Invoked to indicate the visible rows/columns need to be recalculated
* again.
*/
public void reset() {
valid = false;
}
public boolean isVisible(JTable table) {
if (!valid) {// Determine the visible region of the table.
Rectangle visRect = table.getVisibleRect();
tmpPoint.x = visRect.x;
tmpPoint.y = visRect.y;
firstVisCol = table.columnAtPoint(tmpPoint);
firstVisRow = table.rowAtPoint(tmpPoint);
tmpPoint.x += visRect.width;
tmpPoint.y += visRect.height;
lastVisCol = table.columnAtPoint(tmpPoint);
if (lastVisCol == -1) {
lastVisCol = table.getColumnCount() - 1;
}
if ((lastVisRow = table.rowAtPoint(tmpPoint)) == -1) {
lastVisRow = table.getRowCount();
}
valid = true;
}
return (firstRow >= firstVisRow && firstRow <= lastVisRow && column
>= firstVisCol && column <= lastVisCol);
}
}
Related
A custom JComponent subclass as a JTable cell-editor does not get mouse/keyboard events
Recently I have created two very simple, yet very useful components called AttributesEditableView and AttributesEditor. AttributesEditableView is designed to just show arbitrary number of attributes, while the AttributesEditor makes it possible to edit attributes. So, say we have three attributes that can take values 'Y', 'N' and '?' (where by '?' we mean "unspecified"). The view looks something like [[Y][N][?]]: In the editor, user can use arrow-keys to jump left-right between attributes, and by pressing the SPACE key toggle the value of particular (currently selected) attribute. (I guess those of you who are familiar with JTable and its peculiarities already understand when am I getting to right now). It all works well in a form, but when I want to make a TableCellEditor out of this AttributesEditor, I get stuck, because JTable either "eats" all events, or there is something that I am doing wrong so my AttributesEditor simply does not get events... :( My logic is the following: how come DefaultCellEditor when the editor component is JTextField gets those events (left-key or right-key presses for example), while my cell-editor does not? I know I am not giving all the classes here, but I believe the two classes below should give those who have already encountered this problem enough information to be able to tell me how to make this cell editor work as expected? UPDATE: I managed to fix the problem by refactoring the AttributesEditor class. Now it uses key-bindings instead of the KeyListener and everything works as expected. Lesson learned... You can see the AttributesEditableView, AttributesEditor and AttributesCellEditor in action on the following screen shot (I also have the renderer, but could not bother to put it on, it looks like AttributesEditableView anyway): In the screenshot above, first attribute may have 'Y', 'N' and '?' values, second one 'A', 'B', 'C', and 'D', and the third attribute may have 'T' and 'F' as values. Here are the classes: AttributesCellEditor /** * Authors: Dejan Lekic , http://dejan.lekic.org * License: MIT */ public class AttributesCellEditor extends AbstractCellEditor implements TableCellEditor, TreeCellEditor { private AttributesEditor attributesEditor; private AttributesColumnModel attrColModel; private AttributesModel model; private int clickCountToStart = 2; DefaultCellEditor dfe; public AttributesCellEditor(AttributesColumnModel argModel) { super(); attrColModel = argModel; model = new AttributesModel(attrColModel.getAllowedValues()); model.setDefaultValues(attrColModel.getDefaultValues()); attributesEditor = new AttributesEditor(model); attributesEditor.setBorder(new BevelBorder(BevelBorder.LOWERED)); } // ::::: CellEditor method implementations ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #Override public Object getCellEditorValue() { System.out.println("getCellEditorValue()"); // before we return the value, we should also modify the attributes column model and set the // selected attribute index to the index of what we last edited. attrColModel.setSelectedAttributeIndex(model.getSelectedAttributeIndex()); return model.getAttributes(); } // ::::: TableCellEditor method implementations :::::::::::::::::::::::::::::::::::::::::::::::::::::::: #Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { String val = (value == null) ? "" : value.toString(); model.setAttributes(val); model.setSelectedAttributeIndex(attrColModel.getSelectedAttributeIndex()); attributesEditor.setModel(model); return attributesEditor; } // ::::: TreeCellEditor method implementations :::::::::::::::::::::::::::::::::::::::::::::::::::::::: // TODO: implement these #Override public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row) { //To change body of generated methods, choose Tools | Templates. throw new UnsupportedOperationException("Not supported yet."); } // ::::: AbstractCellEditor method overrides :::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #Override public boolean isCellEditable(EventObject e) { if (e instanceof MouseEvent) { return ((MouseEvent) e).getClickCount() >= clickCountToStart; } return true; } /** * {#inheritDoc} * #return */ #Override public boolean stopCellEditing() { boolean ret = super.stopCellEditing(); return ret; } } // AttributesCellEditor class AttributesEditor /** * Authors: Dejan Lekic , http://dejan.lekic.org * License: MIT */ public class AttributesEditor extends AttributesEditableView implements PropertyChangeListener, FocusListener, KeyListener { public AttributesEditor() { super(); editorComponent = true; setFocusable(true); setBackground(Color.white); // Let's copy the border from the TextField Border b = (Border) UIManager.getLookAndFeelDefaults().get("TextField.border"); setBorder(b); // Let's copy the insets from the TextField Insets insets = (Insets) UIManager.getLookAndFeelDefaults().get("TextField.margin"); getInsets().set(insets.top, insets.left, insets.bottom, insets.right); addKeyListener(this); // listeners... addFocusListener(this); } public AttributesEditor(AttributesModel argModel) { this(); setOpaque(true); setFocusable(true); setBackground(Color.white); repaint(); setModel(argModel); } // :::: PropertyChangeListener method implementations ::::::::::::::::::::::::::::::::::::::::::::::::: #Override public void propertyChange(PropertyChangeEvent evt) { String str = (String) evt.getNewValue(); System.out.println("CALL: AttributesEditor.propertyChange() : " + str); updateView(); } // :::: FocusListener method implementations :::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #Override public void focusGained(FocusEvent e) { int sel = model.getSelectedAttributeIndex(); if (sel < 0) { model.setSelectedAttributeIndex(0); } sel = model.getSelectedAttributeIndex(); labels[sel].setBackground(model.getSelectedBackgroundColor()); } #Override public void focusLost(FocusEvent e) { model.setSelectedAttributeIndex(-1); updateView(); } // :::: KeyListener method implementations :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #Override public void keyTyped(KeyEvent e) { // Weird thing is, arrow keys do not trigger keyTyped() to be called, // so we have to use keyPressed here, or keyReleased } /** * Weird thing is, arrow keys do not trigger keyTyped() to be called, so we have to use keyPressed/keyReleased here. * #param e */ #Override public void keyPressed(KeyEvent e) { int key = e.getKeyCode(); switch (key) { case KeyEvent.VK_UP: model.previous(model.getSelectedAttributeIndex()); break; case KeyEvent.VK_DOWN: case KeyEvent.VK_SPACE: model.next(model.getSelectedAttributeIndex()); break; case KeyEvent.VK_LEFT: model.previousAttribute(); updateView(); break; case KeyEvent.VK_RIGHT: model.nextAttribute(); updateView(); break; default: int idx = model.getSelectedAttributeIndex(); char chr = Character.toUpperCase(e.getKeyChar()); if (model.isValid(idx, chr)) { model.setValue(idx, chr); } } // switch } // keyPressed() method #Override public void keyReleased(KeyEvent e) { // nothing } } // AttributesEditor class AttributesEditableView /** * Authors: Dejan Lekic , http://dejan.lekic.org * License: MIT */ public class AttributesEditableView extends JComponent implements PropertyChangeListener, MouseListener { protected AttributesModel model; protected JLabel[] labels; protected boolean editorComponent; protected boolean inTable; public AttributesEditableView() { super(); FlowLayout fl = new FlowLayout(FlowLayout.LEFT); fl.setVgap(0); setLayout(fl); editorComponent = false; inTable = false; } public AttributesEditableView(AttributesModel argModel) { this(); setModel(argModel); } // :::: JComponent method overrides ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** * I had to do this because it is a common problem with JComponent subclasses. * More about it: http://docs.oracle.com/javase/tutorial/uiswing/painting/problems.html * * #param g */ #Override protected void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(getBackground()); g.fillRect(0, 0, getWidth(), getHeight()); g.setColor(getForeground()); } // :::: PropertyChangeListener mthod implementations :::::::::::::::::::::::::::::::::::::::::::::::::: #Override public void propertyChange(PropertyChangeEvent evt) { String str = (String) evt.getNewValue(); updateView(); } // :::: <Interface> method implementations :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #Override public void mouseClicked(MouseEvent e) { // labels have names in form of "attributelbl-3", we need to extract the ID from the name String num = e.getComponent().getName().split("-")[1]; int idx = Integer.parseInt(num); if (editorComponent) { model.setSelectedAttributeIndex(idx); } model.next(idx); } // mouseClicked() method #Override public void mousePressed(MouseEvent e) { // do nothing } #Override public void mouseReleased(MouseEvent e) { // do nothing } #Override public void mouseEntered(MouseEvent e) { // labels have names in form of "attributelbl-3", we need to extract the ID from the name String num = e.getComponent().getName().split("-")[1]; int idx = Integer.parseInt(num); model.setSelectedAttributeIndex(idx); updateView(); } #Override public void mouseExited(MouseEvent e) { // labels have names in form of "attributelbl-3", we need to extract the ID from the name String num = e.getComponent().getName().split("-")[1]; int idx = Integer.parseInt(num); if (!editorComponent) { model.setSelectedAttributeIndex(-1); } updateView(); } /** * Use this method to forcefully highlight specific attribute. * #param argIndex */ public final void highlight(int argIndex) { for (int i = 0; i < model.getNumberOfAttributes(); i++) { if (i == argIndex) { labels[i].setBackground(model.getSelectedBackgroundColor()); } else { labels[i].setBackground(model.getBackgroundColor()); } } // for } // highlight() method /** * Extremely important method. Here we set the model, and generate JLabel objects for the attributes. * We also set the values, and initialise the listeners. * #param argModel */ public final void setModel(AttributesModel argModel) { if (model != null) { model.removePropertyChangeListener(this); } labels = null; removeAll(); labels = new JLabel[argModel.getNumberOfAttributes()]; int w = 0; int h = 0; for (int i = 0; i < argModel.getNumberOfAttributes(); i++) { String txt = "" + argModel.getValue(i); System.out.println(txt); JLabel lbl = new JLabel(txt); labels[i] = lbl; lbl.setName("attributelbl-" + i); // very important, because we will use the name to get the idx lbl.setHorizontalAlignment(SwingConstants.CENTER); lbl.setOpaque(true); lbl.setBackground(argModel.getBackgroundColor()); if (isInTable()) { lbl.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, Color.GRAY)); } int nh = lbl.getPreferredSize().height; int nw = nh; // make the width to be equal to the height lbl.setPreferredSize(new Dimension(nw, nh)); lbl.addMouseListener(this); h = Math.max(h, lbl.getPreferredSize().height); w = w + lbl.getPreferredSize().width; add(lbl); } // for Dimension ps = new Dimension(w, h); model = argModel; model.addPropertyChangeListener(this); } // setModel() method public final AttributesModel getModel() { return model; } public boolean isInTable() { return inTable; } public void setInTable(boolean inTable) { this.inTable = inTable; } /** * Updates the view */ protected void updateView() { String attrs = model.getAttributes(); for (int i = 0; i < model.getNumberOfAttributes(); i++) { labels[i].setText("" + model.getValue(i)); if (model.getSelectedAttributeIndex() == i) { labels[i].setBackground(model.getSelectedBackgroundColor()); } else { labels[i].setBackground(model.getBackgroundColor()); } } } } // AttributesEditableView class
Adding multiple JProgressBar to TableColumn of JTable
I have added multiple JProgressBar to TableColumn of JTable. I am updating all the JProgressBar with data after making certain calculations, but only the last ProgressBar(in this case ProgressBar progressObj4) which is added is getting updated. How can I update all the ProgressBars? The basic requirement is that I am displaying the status of file in progress bar while uploading . Currently I am hardcoding 4 progress bars to test if all the progress bars are getting update wrt the status of the file, but I need to create them dynamically. The total no of progress bars wrt the no of files which is getting uploaded. Also, how can I fetch the individual instances of the progress bars & update their status ? I am attaching the source code of the progressbar getting added to the table column. //tc = object of TableColumn progressObj1 = new ProgressBarRenderer("Progress1"); progressObj1.setValue(0); progressObj1.setStringPainted(true); progressObj1.setBackground(Color.WHITE); progressObj1.setBorderPainted(true); tc.setCellRenderer(progressObj1); progressObj2 = new ProgressBarRenderer("Progress2"); progressObj2.setValue(0); progressObj2.setStringPainted(true); progressObj2.setBackground(Color.WHITE); progressObj2.setBorderPainted(true); tc.setCellRenderer(progressObj2); progressObj3 = new ProgressBarRenderer("Progress3"); progressObj3.setValue(0); progressObj3.setStringPainted(true); progressObj3.setBackground(Color.WHITE); progressObj3.setBorderPainted(true); tc.setCellRenderer(progressObj3); progressObj4 = new ProgressBarRenderer("Progress4"); progressObj4.setValue(0); progressObj4.setStringPainted(true); progressObj4.setBackground(Color.WHITE); progressObj4.setBorderPainted(true); tc.setCellRenderer(progressObj4);
basically there are two ways move with JProgressBar by using SwingWorker and Runnable#Thread, example for SwingWorker import java.awt.*; import java.util.*; import javax.swing.*; import javax.swing.table.*; public class TableCellProgressBar { private String[] columnNames = {"String", "ProgressBar"}; private Object[][] data = {{"dummy", 100}}; private DefaultTableModel model = new DefaultTableModel(data, columnNames) { private static final long serialVersionUID = 1L; #Override public Class<?> getColumnClass(int column) { return getValueAt(0, column).getClass(); } #Override public boolean isCellEditable(int row, int col) { return false; } }; private JTable table = new JTable(model); public JComponent makeUI() { TableColumn column = table.getColumnModel().getColumn(1); column.setCellRenderer(new ProgressRenderer()); EventQueue.invokeLater(new Runnable() { #Override public void run() { startTask("test"); startTask("error test"); startTask("test"); } }); JPanel p = new JPanel(new BorderLayout()); p.add(new JScrollPane(table)); return p; } //http://java-swing-tips.blogspot.com/2008/03/jprogressbar-in-jtable-cell.html private void startTask(String str) { final int key = model.getRowCount(); SwingWorker<Integer, Integer> worker = new SwingWorker<Integer, Integer>() { private int sleepDummy = new Random().nextInt(100) + 1; private int lengthOfTask = 120; #Override protected Integer doInBackground() { int current = 0; while (current < lengthOfTask && !isCancelled()) { if (!table.isDisplayable()) { break; } if (key == 2 && current > 60) { //Error Test cancel(true); publish(-1); return -1; } current++; try { Thread.sleep(sleepDummy); } catch (InterruptedException ie) { break; } publish(100 * current / lengthOfTask); } return sleepDummy * lengthOfTask; } #Override protected void process(java.util.List<Integer> c) { model.setValueAt(c.get(c.size() - 1), key, 1); } #Override protected void done() { String text; int i = -1; if (isCancelled()) { text = "Cancelled"; } else { try { i = get(); text = (i >= 0) ? "Done" : "Disposed"; } catch (Exception ignore) { ignore.printStackTrace(); text = ignore.getMessage(); } } System.out.println(key + ":" + text + "(" + i + "ms)"); } }; model.addRow(new Object[]{str, 0}); worker.execute(); } public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { #Override public void run() { createAndShowGUI(); } }); } public static void createAndShowGUI() { JFrame frame = new JFrame(); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.getContentPane().add(new TableCellProgressBar().makeUI()); frame.setSize(320, 240); frame.setLocationRelativeTo(null); frame.setVisible(true); } } class ProgressRenderer extends DefaultTableCellRenderer { private final JProgressBar b = new JProgressBar(0, 100); public ProgressRenderer() { super(); setOpaque(true); b.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); } #Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Integer i = (Integer) value; String text = "Completed"; if (i < 0) { text = "Error"; } else if (i < 100) { b.setValue(i); return b; } super.getTableCellRendererComponent(table, text, isSelected, hasFocus, row, column); return this; } }
To my knowledge, a renderer applies to all rows in a given column. It seems to me that you would like to apply several renderers to the same column. Furthermore, it also seems that you are attempting to give the cell renderer state. I believe it will help to make the renderer stateless and have the renderer's method getTableCellRendererComponent() take care of setting the various (JProgressBar) properties based on values from the current row before the renderer paints the cell. In other words, you should only need to invoke tc.setCellRenderer() once for any given column, and then have your cell renderer to draw the column for any given row (based on, say, the underlying data model for that table).
autorefreshing jtable extracting collection data
I am a begginer with java programming. I am trying to make a JTable that reads collection set hashSet, and refreshes eachtime set is changed, also want to make autorefresh every 3 sec repaint on JPanel where table will be on (which works in original program). Also Class Mats changes Boolean take over time, i want to change color its writhing Mats.name in JTable depending on the value Mats.take in that mat in set which is the reason for autorefresh. This is a sample for table its remakeing table from scrach with each time new line is added throws java.lang.NullPointerException. I simply do not see why? Thx for the help in advance public class Table extends JFrame { private static JTable table; private static JButton addbuttin; private static TableModel model; private static Set<Mats> set = null; public static String colorString(String str, Mats a) { if (a.getTake().equals(new Boolean(true))) { return "<html><body><font color=red>" + str + "</font></body></html>"; } else { return "<html><body><font color=blue>" + str + "</font></body></html>"; } } public static void main(String[] argv) { JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); addbuttin = new JButton("Dodaj"); Mats a = new Mats(10, 10, 5, "WOOD", true); Mats b = new Mats(10, 12, 5, "WOOD", false); set = new HashSet<Mats>(); set.add(a); set.add(b); addbuttin.addActionListener(new ActionListener() { #Override public void actionPerformed(ActionEvent arg0) { Mats c = new Mats(10, 12, 6, "WOOD", true); set.add(c); } }); JPanel p = new JPanel(); model = new DefaultTableModel() { public boolean isCellEditable(int rowIndex, int vColIndex) { return false; } }; /**here is java.lang.NullPointerException*/ table.getModel().addTableModelListener(new TableModelListener() { #Override public void tableChanged(TableModelEvent e) { if (e.equals(TableModelEvent.INSERT)) { model = new DefaultTableModel(); addColumns(); adddata(set); } } }); addColumns(); adddata(set); table = new JTable(model); JScrollPane scrollPane = new JScrollPane(table, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); p.add(scrollPane, BorderLayout.CENTER); p.add(addbuttin, BorderLayout.EAST); p.setSize(500, 400); frame.add(p); frame.setSize(600, 600); frame.setVisible(true); } private static void addColumns() { ((DefaultTableModel) model).addColumn("NAME"); } private static void adddata(Set<Mats> set) { for (Iterator iterator = set.iterator(); iterator.hasNext();) { Mats mats = (Mats) iterator.next(); String n = colorString(mats.getName(), mats); ((DefaultTableModel) model).insertRow(model.getRowCount(), new Object[] { n }); } } } This is Class Mats simplified public class Mats implements Comparable<Mats> { private String name; private Boolean take; /** * */ public Mats() { } /** * * #param name * #param uzet */ public Mats(String name, boolean take) { this.name = name.toUpperCase(); this.take = take; taken(); } /** * * #param Material */ public Mats(Mats Material) { this.name = Material.getName().toUpperCase(); this.take = Material.getTake(); Material.taken(); } /** * #return the name */ public String getName() { return name; } /** * #param name * the name to set */ public void setName(String name) { this.name = name.toUpperCase(); } /** * #param take * the take to set */ public void setTake(Boolean take) { this.take = take; } /** * #return the take */ public Boolean getTake() { return take; } /** * * #return */ #Override public String toString() { return "Material: ".toUpperCase() + getName() + "\n"; } /** * * #param obj * #return */ #Override public boolean equals(Object obj) { Mats a = (Mats) obj; if (obj == null) { return false; } if (a.getName() == this.getName()) { return true; } return false; } /** * * #return */ #Override public int hashCode() { int hash = 3; hash = 83 * hash + this.getX(); hash = 83 * hash + this.getY(); hash = 83 * hash + (this.getName() != null ? this.getName().hashCode() : 0); return hash; } /** * */ public void taken() { MatRespawn s = new MatRespawn(1000, take); s.start(); } #Override public int compareTo(Mats o) { Integer a = this.getName(); if (a.compareTo(o.getName()) != 0) return a.compareTo(o.getName()); return 0; } } Class MatRespawn which changes Mats.take public class MatRespawn extends Thread { private int time; private Boolean taken; public MatRespawn(int time, Boolean take) { this.time = time; this.taken = take; } public int getTime() { return time; } public void setTime(int time) { this.time = time; } public Boolean getTaken() { return taken; } public void setTaken(Boolean taken) { this.taken = taken; } #Override public void run() { try { while (true) { if (taken.equals(new Boolean(true))) { sleep(time * 10); //JOptionPane.showMessageDialog(null, "dostupnost: " + taken); taken = false; } sleep(time); //JOptionPane.showMessageDialog(null, "dostupnost: " + taken); taken = true; } } catch (InterruptedException e) { e.printStackTrace(); } } }
At the time you call table.getModel(), you haven't yet called table = new JTable(model); That means that table is still null, and so you get an exception. Try moving the line that constructs table to before the line that accesses it.
Found what else was wrong private static TableModel model; later i made model = new DefaultTableModel() { public boolean isCellEditable(int rowIndex, int vColIndex) { return false; } }; Should have been private static DefaultTableModel model;
Displaying Large JLIST formatted as HTML
I have a java applet in which I have to display a large amount of items (dictionary entries). The user needs to be able to select individual items in the list, hence it is implemented as a JList. Generating the list was very quick until I decided to make the display more asthetically pleasing by formatting the individual items using HTML. Now the list looks pretty but it takes between 10 and 15 seconds to generate it each time the user accesses the dictionary (without formatting it occurs almost instantly). I suppose I could take the performance hit up front by generating the list when the user first enters the application and just hiding and unhiding the list as needed. But, I'm wondering if there is a better way. Perhaps a more efficient way to generate the list. Here is the section of code where the slow down occurrs (Between the display of C and D): DefaultListModel dictCodeModel = new DefaultListModel(); System.out.println("C"); while (e.hasMoreElements()) { String currentEntry = ""; DEntry dentry = (DEntry) e.nextElement(); if (!filter) dictCodeModel.addElement(dentry.theToken); // tokens have embedded html tags } System.out.println("D"); As you can see it is pretty simple. When "theToken" is formatted as HTML, I get a real performance hit. Any ideas of what I can do to get around this? Thanks,
What kind of HTML formatting are you using? If it's just some text styling (font, color), you can use a JLabel, set its properties accordingly and set it as ListCellRenderer for the JList.
The links above are a bit out of date so here's something more up to date. Simply using JTable is a huge improvement in speed on initial load but a little slow when you first start scrolling. And you have the new problem that the row height needs adjusting. This can be done inside of a custom renderer easy enough by implementing TableCellRenderer since the getTableCellRendererComponent method gives you access to the row, the table and the component. This will however fire a update of the table which will call the same code. If you code appropriately, this won't be a problem. Still, it's better practice to put it somewhere else. I added a listener to the JViewport and only updated the rows that are currently in view. The code I based this on is here Alternatively, you can use write a ListCellRenderer that returns a JPanel that looks like the HTML. If you only need a JTextArea then you'll need to set its width to ensure it's preferred height is set correctly like in this answer. Again, you have to update the row's width and it makes sense to do this based on the JViewport. If you're curious about the performance of both approaches, then a custom renderer returning a JPanel is faster than JLabels rendering HTML. Both are reasonably quick though even with lists with a few thousand items. As mentioned, they can be a little slow when you initially scroll. Finally, here's some code that lets you make a quick comparison yourself: import java.awt.*; import java.awt.event.*; import java.util.*; import java.util.Timer; import java.util.concurrent.ExecutionException; import javax.swing.*; import javax.swing.event.*; import javax.swing.table.*; public class JTableHtmlTest extends JFrame { protected static final long serialVersionUID = 1L; public static class Item { public int id; public String msg; } static class TableModel extends AbstractTableModel { private static final long serialVersionUID = JListTest.serialVersionUID; private Item[] items = new Item[] {}; public int getRowCount() { return items.length; } public int getColumnCount() { return 1; } public Object getValueAt(int rowIndex, int columnIndex) { return items[rowIndex]; } #Override public String getColumnName(int column) { return ""; } public void updateItems() { SwingWorker<Item[], Void> worker = new SwingWorker<Item[], Void>() { #Override protected Item[] doInBackground() throws Exception { final Item[] tempList = new Item[3000]; for (int i = 0; i < tempList.length; i++) { Item item = new Item(); item.id = (int) (Math.random() * 10000); item.msg = "This is the default message that has to be" + " long enough to wrap around a few times so that" + " we know things are working. It's rather tedious to write."; tempList[i] = item; } return tempList; } #Override protected void done() { try { items = get(); fireTableDataChanged(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }; worker.execute(); } } public static class TableRenderer implements TableCellRenderer { private static final String strColor = "#EDF5F4"; private static final Color strideColor = Color.decode(strColor); JLabel htmlLabel = new JLabel(); JPanel noHtmlPanel = new JPanel(); JLabel noHtmlLabel = new JLabel(); JTextArea noHTMLTextArea = new JTextArea(); Item toRender = null; boolean useHtml = false; public TableRenderer() { noHTMLTextArea.setWrapStyleWord(false); noHTMLTextArea.setLineWrap(true); noHTMLTextArea.setOpaque(false); Font defaultFont = noHtmlLabel.getFont(); Font boldFont = defaultFont.deriveFont(Font.BOLD); noHtmlLabel.setFont(boldFont); noHtmlLabel.setOpaque(false); noHtmlPanel.setLayout(new BorderLayout()); noHtmlPanel.add(noHtmlLabel, BorderLayout.NORTH); noHtmlPanel.add(noHTMLTextArea, BorderLayout.SOUTH); } public void setUseHtml(boolean useHtml) { this.useHtml = useHtml; } public Component getJlabelRenderer(JTable table, Item value, int row) { String colorString = ""; if (row % 2 == 0) { colorString = "background-color:" + strColor + ";"; } if (toRender != value) { toRender = value; htmlLabel.setText("<html><div style='padding:2px;" + "width:" + table.getWidth() + ";" + colorString + "color:black;'>" + "<div style='padding:2px;font-weight:500;'>" + "Item " + value.id + "</div>" + value.msg + "</div></html>"); } return htmlLabel; } public Component getNoHtmlRenderer(JTable table, Item value, int row) { if (toRender != value) { toRender = value; noHtmlLabel.setText("Item " + value.id); noHTMLTextArea.setText(value.msg); if (row % 2 == 0) { noHtmlPanel.setBackground(strideColor); noHtmlPanel.setOpaque(true); } else { noHtmlPanel.setOpaque(false); } } return noHtmlPanel; } public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { if (useHtml) { return getJlabelRenderer(table, (Item) value, row); } else { return getNoHtmlRenderer(table, (Item) value, row); } } } public JTableHtmlTest() { this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JPanel controlPanel = new JPanel(); JButton updaterControl = new JButton("Update 3000"); final JCheckBox useHtmlControl = new JCheckBox("Use HTML"); final TableModel model = new TableModel(); final JTable table = new JTable(model); final TableRenderer renderer = new TableRenderer(); JScrollPane scrollPane = new JScrollPane(table); final JLabel durationIndicator = new JLabel("0"); controlPanel.add(useHtmlControl, BorderLayout.WEST); controlPanel.add(updaterControl, BorderLayout.EAST); getContentPane().add(controlPanel, BorderLayout.PAGE_START); getContentPane().add(scrollPane, BorderLayout.CENTER); getContentPane().add(durationIndicator, BorderLayout.PAGE_END); table.setDefaultRenderer(Object.class, renderer); // Only update the JTable row heights when they are in view final JViewport viewport = scrollPane.getViewport(); viewport.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { Rectangle viewRect = viewport.getViewRect(); int first = table.rowAtPoint(new Point(0, viewRect.y)); if (first == -1) { return; } int last = table.rowAtPoint(new Point(0, viewRect.y + viewRect.height - 1)); if (last == -1) { last = model.getRowCount() - 1; } int column = 0; for (int row = first; row <= last; row++) { Component comp = table.prepareRenderer( table.getCellRenderer(row, column), row, column); int rowHeight = comp.getPreferredSize().height; table.setRowHeight(row, rowHeight); } } }); updaterControl.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { renderer.setUseHtml(useHtmlControl.isSelected()); model.updateItems(); } }); Timer counter = new Timer(); counter.schedule(new TimerTask() { #Override public void run() { String previousCounter = durationIndicator.getText(); final String newCounter = Integer.toString(Integer .parseInt(previousCounter) + 1); SwingUtilities.invokeLater(new Runnable() { public void run() { durationIndicator.setText(newCounter); setTitle(newCounter); } }); } }, 0, 100); } public static void main(String args[]) { EventQueue.invokeLater(new Runnable() { public void run() { try { JTableHtmlTest jlt = new JTableHtmlTest(); jlt.pack(); jlt.setSize(300, 300); jlt.setVisible(true); } catch (Exception e) { e.printStackTrace(); } } }); } }
Populate JTable from a Hashtable in Java
I have a function which gets a key from the user and generates a Hashtable (on a pattern specified by the key). After creating a Hashtable, I would like to populate a JTable so that each each column represents a key and every rows represents the values associated with the key. I tried everything but couldn't get this work. I'm not creating the table from within the constructor as I need to get input from the user.
See How to Use Tables: Creating a Table Model. The JTable constructor used by SimpleTableDemo creates its table model with code like this: new AbstractTableModel() { public String getColumnName(int col) { return columnNames[col].toString(); } public int getRowCount() { return rowData.length; } public int getColumnCount() { return columnNames.length; } public Object getValueAt(int row, int col) { return rowData[row][col]; } public boolean isCellEditable(int row, int col) { return true; } public void setValueAt(Object value, int row, int col) { rowData[row][col] = value; fireTableCellUpdated(row, col); } } You basically have to wrap your hashtable in the above manner. Here's an example. package eed3si9n.hashtabletable; import java.awt.BorderLayout; import java.util.Enumeration; import java.util.Hashtable; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.table.AbstractTableModel; import javax.swing.JButton; import java.awt.Dimension; public class MainForm extends JFrame { private static final long serialVersionUID = 1L; private JPanel jContentPane = null; // #jve:decl-index=0:visual-constraint="23,38" private JScrollPane m_scrollPane = null; private JTable m_table = null; private Hashtable<String, String> m_hash = null; private JButton m_btnAdd = null; /** * This is the default constructor */ public MainForm() { super(); initialize(); m_hash = new Hashtable<String, String>(); m_hash.put("Dog", "Bow"); } private void onButtonPressed() { m_hash.put("Cow", "Moo"); m_table.revalidate(); } /** * This method initializes this * * #return void */ private void initialize() { this.setSize(409, 290); this.setTitle("JFrame"); this.setContentPane(getJContentPane()); } /** * This method initializes jContentPane * * #return javax.swing.JPanel */ private JPanel getJContentPane() { if (jContentPane == null) { jContentPane = new JPanel(); jContentPane.setLayout(new BorderLayout()); jContentPane.setSize(new Dimension(500, 500)); jContentPane.setPreferredSize(new Dimension(500, 500)); jContentPane.add(getM_scrollPane(), BorderLayout.NORTH); jContentPane.add(getM_btnAdd(), BorderLayout.SOUTH); } return jContentPane; } /** * This method initializes m_scrollPane * * #return javax.swing.JScrollPane */ private JScrollPane getM_scrollPane() { if (m_scrollPane == null) { m_scrollPane = new JScrollPane(); m_scrollPane.setViewportView(getM_table()); } return m_scrollPane; } /** * This method initializes m_table * * #return javax.swing.JTable */ private JTable getM_table() { if (m_table == null) { m_table = new JTable(); m_table.setModel(new AbstractTableModel(){ private static final long serialVersionUID = 1L; public int getColumnCount() { return 2; } public int getRowCount() { return m_hash.size(); } public String getColumnName(int column) { if (column == 0) { return "Animal"; } else { return "Sound"; } } public Object getValueAt(int rowIndex, int columnIndex) { if (columnIndex == 0) { return getKey(rowIndex); } else { return m_hash.get(getKey(rowIndex)); } // if-else } private String getKey(int a_index) { String retval = ""; Enumeration<String> e = m_hash.keys(); for (int i = 0; i < a_index + 1; i++) { retval = e.nextElement(); } // for return retval; } }); } return m_table; } /** * This method initializes m_btnAdd * * #return javax.swing.JButton */ private JButton getM_btnAdd() { if (m_btnAdd == null) { m_btnAdd = new JButton(); m_btnAdd.setPreferredSize(new Dimension(34, 30)); m_btnAdd.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent e) { onButtonPressed(); } }); } return m_btnAdd; } public static void main(String[] args) { //Schedule a job for the event-dispatching thread: //creating and showing this application's GUI. javax.swing.SwingUtilities.invokeLater(new Runnable() { public void run() { MainForm frame = new MainForm(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(500, 500); frame.setVisible(true); } }); } } // #jve:decl-index=0:visual-constraint="10,10"
Firstly, avoid Hashtable, go straight for Map. In this case there two potential standard implementations you mights want: LinkedHashMap can retain the order that the entries were added; TreeMap, a SortedMap/NavigableMap, will sort the results (order of which can be determined by a Comparator. Alternatively you might want a form of Map that fire events or also provides a TableModel. If you want a one time conversion from the Map to table, then it's pretty straightforward. public static TableModel toTableModel(Map<?,?> map) { DefaultTableModel model = new DefaultTableModel( new Object[] { "Key", "Value" }, 0 ); for (Map.Entry<?,?> entry : map) { model.addRow(new Object[] { entry.getKey(), entry.getValue() }); } return model; } Then just create the JTable with this pre-populated model. (Disclaimer: I've not tested or so much as compiled this code.) To keep the Map and TableModel synchronized is more code. Generally it's best to avoid duplicating state whereever possible. Write a class that exposes itself as both a Map and a TableModel. You could go more disjoint by having a Map that fires events and a TableModel that adapts the Map (although note that Map does not have random access based on index, so you'll need to be iether clever or slow for large maps). Going the other way, a simpler approach would be to add the data straight to a DefaultTableModel and not using a Map at all.