I've done JTable for simple schedule with visits.
It contains custom AbstractTableModel which shows three columns shown below.
The problem is that it is possible to initializate Table and to get desired look - but after data change there is no change in appearance of Table. Each button click takes data from database and sets fields in columns TYPE and STATE - depending on given hour and date of reservation.
What is more I'am able to insert new row at the end of table but cannot make visible existing value update.
I already read few similar topics but nothing helps in my case.
Thanks in advance for every suggestions.
[UPDATED] Working code illustrating the problem:
import java.awt.EventQueue;
import java.util.Vector;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;
import javax.swing.JButton;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
public class ModelTest {
private JFrame frame;
private JTable tablePendingVisits;
private PendingVisitModel pendingVisitModel;
private JScrollPane scrollPanePendingVisits;
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
ModelTest window = new ModelTest();
window.frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
public ModelTest() {
frame = new JFrame();
frame.setBounds(100, 100, 450, 407);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().setLayout(null);
JButton btnChangeValue = new JButton("Change value at 9:00");
btnChangeValue.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
refreshTableModel();
}
});
btnChangeValue.setBounds(63, 308, 305, 23);
frame.getContentPane().add(btnChangeValue);
tablePendingVisits = new JTable();
scrollPanePendingVisits = new JScrollPane();
pendingVisitModel = new PendingVisitModel();
tablePendingVisits.setModel(pendingVisitModel);
scrollPanePendingVisits.setBounds(63, 36, 305, 246);
scrollPanePendingVisits.setViewportView(tablePendingVisits);
frame.getContentPane().add(scrollPanePendingVisits);
}
public void refreshTableModel(){
String[] sampleString = {"9:00", "Bobby", "Tables"};
// search for row with 9:00 and replace values in given columns
for (int i = 0; i < pendingVisitModel.getRowCount(); i++) {
if( sampleString[0].equals(pendingVisitModel.getValueAt(i, 0)) ) { // Change row values when both hours are equal
pendingVisitModel.setValueAt(sampleString[1], i, 1); // Change at type column
pendingVisitModel.setValueAt(sampleString[2], i, 2); // Change at status column
}
}
}
}
// Custom TableModel
class PendingVisitModel extends AbstractTableModel {
private static final long serialVersionUID = 1L;
private String[] columnNames = {"HOUR", "TYPE", "STATE"};
private Vector<String[]> data = new Vector<String[]>();
public PendingVisitModel() {
for(int i = 8; i<15; i++) {
data.add(new String[]{i+":00", "-", "Free"} );
data.add(new String[]{i+":15", "-", "Free"} );
data.add(new String[]{i+":30", "-", "Free"} );
data.add(new String[]{i+":45", "-", "Free"} );
}
}
public int getColumnCount() {
return columnNames.length;
}
public int getRowCount() {
return data.size();
}
public String getColumnName(int col) {
return columnNames[col];
}
public String getValueAt(int row, int col) {
String[] temp = data.get(row);
if(temp.length > 0 && col < 3)
return temp[col];
else
return null;
}
public void setValueAt(String[] value, int row, int col) {
String[] temp = data.get(row);
temp[col] = value[col];
data.set(row, temp);
fireTableRowsUpdated(row, row);
}
public void insertRow(String[] value) {
data.add(value);
fireTableRowsInserted(data.size(), data.size());
}
public void clearRows(){
data.clear();
fireTableDataChanged();
}
public void removeAllEntry(){
data.clear();
fireTableDataChanged();
}
public boolean isCellEditable(int row, int col) {
return false;
}
#Override
public Class<String> getColumnClass(int colNum) {
return String.class;
}
}
PendingVisitModel pendingVisitModel = new PendingVisitModel();
It looks to me like the pendingVisitModel is defined as a local variable. This is the model you add to the table.
The refreshTableModel() method is referencing a pendingVisitModelvariable, but I would guess this is an instance variable that is NOT used by the table.
Get rid of local instance of your pendingVisitModel.
fireTableDataChanged();
Also don't keep using firTableDataChanged in all your TableModel methods. The API provides other methods that are more appropriate for different events.
Edit:
public void setValueAt(String[] value, int row, int col) {
You did not override the setValueAt(...) method. The value parameter is an Object not a String array.
Whenever you override a method of a class you should use the #Override annotation before the method. This way the compiler will give you an error if you override a method incorrectly (saving hours of frustration...).
#Override
public void setValueAt(Object value, int row, int col) {
Related
I want to sort a JTable like this:
That is: (EDIT)
If I click the "Name" column to sort, "Tom", "Polazzo" and "Anna" must be sorted alphabetically, and rows with the same names must stay together("grouped" by the name), and each name should be shown only once, the rest cells must be blank.
If I click the "Duration" or "Book #" column, I want all rows sorted ascending/descending by values of duration/book number, but same as in point 1), rows with the same "Name" must stay together, that is, stay grouped, and only the first row in every group is shown, and the rest "Name" stay blank.
The data in the table model's vector are collected from parsing a XML file. The rows with same "Name" are under the same node in the hierarchy tree.
I think there're two ways to do this:
a) When collecting the data and construct the rows, under the same "Name" node, give the cell at column 0 the "Name" value, and leave the rest of rows "" in the same column. But, I don't know how to construct the comparator of column "Name", to ensure the first row always being the top in sorting. (It cannot be the biggest and the smallest when we override compare() method, can it?)
b) Every time we click the table header to sort, make the renderer repaint the table the way we want: comparing the value in the first line of each group, and if it's the same as the last line's column 0's value, don't paint this cell, until we reach another different value. In that way, we don't mess with comparators/sorters, and it turns into a renderer problem. That's what I kind of achieved in the SSCCE below, but I am half way there and I need some tips.
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.List;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.Comparator;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.RowSorter;
import javax.swing.RowSorter.SortKey;
import javax.swing.event.RowSorterEvent;
import javax.swing.event.RowSorterListener;
import javax.swing.SortOrder;
import javax.swing.SwingUtilities;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
import com.WindThunderStudio.JHeaderToolTip.JHeaderToolTip;
import net.miginfocom.swing.MigLayout;
public class RowGroupInTable extends JFrame {
public RowGroupInTable() {
begin();
}
private void begin() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new BorderLayout());
DefaultTableModel model = new DefaultTableModel();
Object[][] data = new Object[][] {{"Tom", "17", "Book1"},
{"Tom", 23, "Book2"},
{"Tom", 25, "Book3"},
{"Polazzo", 41, "Book1"},
{"Polazzo", 45, "Book2"},
{"Polazzo", 12, "Book3"},
{"Anna", 1, "Book3"},
{"Anna", 33, "Book5"}};
String[] titles = new String[] {"Name", "Last job duration", "Book #"};
JTable table = new JTable(data, titles);
table.setFillsViewportHeight(true);
table.setAutoCreateRowSorter(false);
TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel());
ArrayList<SortKey> sortKeys = new ArrayList<RowSorter.SortKey>();
sortKeys.add(new SortKey(2, SortOrder.ASCENDING));
sortKeys.add(new SortKey(1, SortOrder.ASCENDING));
// sorter.setSortKeys(sortKeys);
sorter.setSortable(0, true);
sorter.setSortable(1, false);
sorter.setSortable(2, true);
table.setRowSorter(sorter);
table.setDefaultRenderer(Object.class, new MyRenderer(table.getDefaultRenderer(Object.class)));
JTableHeader header = table.getTableHeader();
header.addMouseListener(new MouseListener() {
#Override
public void mouseReleased(MouseEvent e) {
// TODO Auto-generated method stub
}
#Override
public void mousePressed(MouseEvent e) {
// TODO Auto-generated method stub
}
#Override
public void mouseExited(MouseEvent e) {
// TODO Auto-generated method stub
}
#Override
public void mouseEntered(MouseEvent e) {
// TODO Auto-generated method stub
}
#Override
public void mouseClicked(MouseEvent e) {
int col = ((JTableHeader)(e.getComponent())).getColumnModel().getColumnIndexAtX(e.getX());
}
});
JScrollPane sp = new JScrollPane(table);
sp.setBounds(0, 0, 200, 200);
add(sp, BorderLayout.CENTER);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
RowGroupInTable frame = new RowGroupInTable();
}
});
}
private class MyRenderer implements TableCellRenderer {
TableCellRenderer def;
public MyRenderer() {
// TODO Auto-generated constructor stub
}
public MyRenderer(TableCellRenderer defaultRend) {
this();
this.def = defaultRend;
}
#Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
int row, int column) {
int rowCount = table.getModel().getRowCount();
Component orig = (def).getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if (column == 0) {
if (row == 0) {
return orig;
} else if (row > 0 && row < rowCount) {
if (table.getModel().getValueAt(row-1, column).equals(value)) {
return new JLabel("");
} else {
return orig;
}
}
}
return orig;
}
}
}
each name should be shown only once, the rest cells must be blank.
If I understand your requirement you might be able to use table.getValueAt(...) instead of table.getModel().getValueAt(...):
import java.awt.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.table.*;
public class RowGroupInTableTest {
private JComponent makeUI() {
String[] titles = new String[] {"Name", "Last job duration", "Book #"};
DefaultTableModel model = new DefaultTableModel(null, titles) {
#Override public Class<?> getColumnClass(int column) {
return MyData.class;
}
};
addMyData(model, new MyData("Tom", 17, "Book1"));
addMyData(model, new MyData("Tom", 23, "Book2"));
addMyData(model, new MyData("Tom", 25, "Book3"));
addMyData(model, new MyData("Polazzo", 41, "Book1"));
addMyData(model, new MyData("Polazzo", 45, "Book2"));
addMyData(model, new MyData("Polazzo", 12, "Book3"));
addMyData(model, new MyData("Anna", 1, "Book3"));
addMyData(model, new MyData("Anna", 33, "Book5"));
JTable table = new JTable(model);
table.setFillsViewportHeight(true);
table.setDefaultRenderer(MyData.class, new MyRenderer());
TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel());
Comparator<MyData> c = Comparator.comparing(MyData::getName);
sorter.setComparator(0, c);
sorter.setComparator(1, c.thenComparing(Comparator.comparingInt(MyData::getDuration)));
sorter.setComparator(2, c.thenComparing(Comparator.comparing(MyData::getBook)));
table.setRowSorter(sorter);
return new JScrollPane(table);
}
private static void addMyData(DefaultTableModel model, MyData data) {
//Omission work...
model.addRow(Collections.nCopies(3, data).toArray());
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame f = new JFrame();
f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
f.getContentPane().add(new RowGroupInTableTest().makeUI());
f.setSize(320, 240);
f.setLocationRelativeTo(null);
f.setVisible(true);
});
}
}
class MyData {
private final String name;
private final int duration;
private final String book;
protected MyData(String name, int duration, String book) {
this.name = name;
this.duration = duration;
this.book = book;
}
public String getName() {
return name;
}
public int getDuration() {
return duration;
}
public String getBook() {
return book;
}
}
class MyRenderer implements TableCellRenderer {
TableCellRenderer def = new DefaultTableCellRenderer();
#Override public Component getTableCellRendererComponent(
JTable table, Object value, boolean isSelected, boolean hasFocus,
int row, int column) {
JLabel orig = (JLabel) def.getTableCellRendererComponent(
table, value, isSelected, hasFocus, row, column);
orig.setHorizontalAlignment(SwingConstants.LEFT);
MyData data = (MyData) value;
switch (table.convertColumnIndexToModel(column)) {
case 0:
String str = data.getName();
if (row > 0) {
//if (table.getModel().getValueAt(row-1, column).equals(value)) {
//Since it compares with the value of the previous line on the display,
//table.getModel() is not needed
MyData prev = (MyData) table.getValueAt(row - 1, column);
if (Objects.equals(prev.getName(), str)) {
str = " ";
}
}
orig.setText(str);
break;
case 1:
orig.setHorizontalAlignment(SwingConstants.RIGHT);
orig.setText("" + data.getDuration());
break;
case 2:
orig.setText(data.getBook());
break;
default:
break;
}
return orig;
}
}
edit
Now if I only use Java 7, is there some "old" way to do this? Just setting the comparators in the Java 7 way?
You would need to impliment Comparator:
TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel());
//Comparator<MyData> c = Comparator.comparing(MyData::getName);
//sorter.setComparator(0, c);
//sorter.setComparator(1, c.thenComparing(Comparator.comparingInt(MyData::getDuration)));
//sorter.setComparator(2, c.thenComparing(Comparator.comparing(MyData::getBook)));
sorter.setComparator(0, new MyDataGroupComparator(0));
sorter.setComparator(1, new MyDataGroupComparator(1));
sorter.setComparator(2, new MyDataGroupComparator(2));
table.setRowSorter(sorter);
class MyDataGroupComparator implements Comparator<MyData> {
private final int column;
protected MyDataGroupComparator(int column) {
this.column = column;
}
#Override public int compare(MyData a, MyData b) {
if (a == null && b == null) {
return 0;
} else if (a != null && b == null) {
return -1;
} else if (a == null && b != null) {
return 1;
} else {
int v = a.getName().compareTo(b.getName());
if (v == 0) {
switch (column) {
case 2:
return a.getBook().compareTo(b.getBook());
case 1:
return a.getDuration() - b.getDuration();
case 0:
default:
return v;
}
}
return v;
}
}
}
when I change table.getModel().getValueAt() to table.getValueAt() I cannot get my original example to work. Why?
Works fine for me(only the cell under Anna is blank):
I would like to populate a JTable during runtime with many rows (lets say 10000). But all my attempts are very poor and inefficient.
Starting point is the addData method which gets a List of Objects representing a row. I tried to fill the table via a SwingWorker but this only works for small data for me.
Another attempt was setting the data directly without using any kind of thread, but this is also very slow, at least the UI isn't blocked like its the case with the SwingWorker.
So how do you do this is general? The table should be filled row by row or chunkwise but not all by one and the vertical scrollbar should be scrollable meanwhile.
My TableModel:
public class MyTableModel extends AbstractTableModel {
/**
*
*/
private static final long serialVersionUID = 1L;
String[] columnNames;
public Map<Long, ErrorMessage> data = new LinkedHashMap<Long, ErrorMessage>();
public MyTableModel(String[] header) {
columnNames = header;
}
public String getColumnName(int col) {
return columnNames[col].toString();
}
#Override
public int getColumnCount() {
return columnNames.length;
}
#Override
public int getRowCount() {
return data.size();
}
#Override
public Object getValueAt(int row, int col) {
.
.
return value;
}
public void addRow(long id, MyDataObject o) {
data.put(id, m);
fireTableRowsInserted(0,nqm_messages.size()-1);
}
}
SwingWorker implementation:
class TableSwingWorker extends SwingWorker<MyTableModel, MyDataObject> {
private final MyTableModel tableModel;
List<MyDataObject> messages;
public TableSwingWorker(MyTableModel tableModel, List<MyDataObject> dataList) {
this.tableModel = tableModel;
this.messages = new LinkedList<MyDataObject>(mm);
}
#Override
protected MyTableModel doInBackground() throws Exception {
for(MyDataObject s : messages) {
publish(s);
}
return tableModel;
}
#Override
protected void process(List<MyDataObject> chunks) {
for(MyDataObject row : chunks){
Long l = Long.parseLong(row.getId());
tableModel.addRow(l, row);
}
}
}
Add Objects to JTable:
public void addData(List<MyDataObject> o) {
MyTableModel m = (MyTableModel)table.getModel();
(new TableSwingWorker(m,o)).execute();
//for(int i=0; i < mm.size();i++) {
// long l = Long.parseLong(mm.get(i).getId());
// m.addRow(l, mm.get(i));
//}
}
So, a number of things have being identified from the comments...
You need to correctly fire the row inserted method, indicating only those rows that have being added and where they have being updated. This very important, as the the table has being optimised for speed
You should provide batch add method for your table model, allowing you to more easily add multiple rows in a single or as few steps as possible
You should have the SwingWorker periodically sleep or yield, to allow it time to publish the results.
So, in this example, I'm adding 1, 000, 000 rows. In my test it took slightly under 1 second...
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
public class TestTableLoad01 {
public static void main(String[] args) {
new TestTableLoad01();
}
public TestTableLoad01() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
MyTableModel model = new MyTableModel();
JTable table = new JTable(model);
table.setDefaultRenderer(Date.class, new TimeCellRenderer());
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new JScrollPane(table));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
TableSwingWorker worker = new TableSwingWorker(model);
worker.execute();
}
});
}
public class TimeCellRenderer extends DefaultTableCellRenderer {
private DateFormat df;
public TimeCellRenderer() {
df = new SimpleDateFormat("HH:mm:ss");
}
#Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
if (value instanceof Date) {
value = df.format(value);
}
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
return this;
}
}
public class MyTableModel extends AbstractTableModel {
private String[] columnNames = new String[]{"Date", "Row"};
private List<RowData> data;
public MyTableModel() {
data = new ArrayList<>(25);
}
#Override
public Class<?> getColumnClass(int columnIndex) {
return columnIndex == 0 ? Date.class : Integer.class;
}
#Override
public String getColumnName(int col) {
return columnNames[col];
}
#Override
public int getColumnCount() {
return columnNames.length;
}
#Override
public int getRowCount() {
return data.size();
}
#Override
public Object getValueAt(int row, int col) {
RowData value = data.get(row);
return col == 0 ? value.getDate() : value.getRow();
}
public void addRow(RowData value) {
int rowCount = getRowCount();
data.add(value);
fireTableRowsInserted(rowCount, rowCount);
}
public void addRows(RowData... value) {
addRows(Arrays.asList(value));
}
private void addRows(List<RowData> rows) {
int rowCount = getRowCount();
data.addAll(rows);
fireTableRowsInserted(rowCount, getRowCount() - 1);
}
}
public class RowData {
private Date date;
private int row;
public RowData(int row) {
this.date = new Date();
this.row = row;
}
public Date getDate() {
return date;
}
public int getRow() {
return row;
}
}
public class TableSwingWorker extends SwingWorker<MyTableModel, RowData> {
private final MyTableModel tableModel;
public TableSwingWorker(MyTableModel tableModel) {
this.tableModel = tableModel;
}
#Override
protected MyTableModel doInBackground() throws Exception {
// This is a deliberate pause to allow the UI time to render
Thread.sleep(2000);
System.out.println("Start polulating");
for (int index = 0; index < 1000000; index++) {
RowData data = new RowData(index);
publish(data);
Thread.yield();
}
return tableModel;
}
#Override
protected void process(List<RowData> chunks) {
System.out.println("Adding " + chunks.size() + " rows");
tableModel.addRows(chunks);
}
}
}
I have a JTable populated with a custom DataModel (pasted below) and when I call the populate() method, it appears to populate the table with duplicate data - each row is filled with the same value over and over again. However, on closer inspection (by simply println()ing the 'data' field), the data model isn't at fault - it holds correct data, in the format I expect. What gives?
import java.util.ArrayList;
import javax.swing.table.AbstractTableModel;
#SuppressWarnings("serial") // we don't expect this app to ever use serialized classes. EVER.
public class CollectionDataModel extends AbstractTableModel {
private ArrayList<ArrayList<String>> data;
public CollectionDataModel() {
data = new ArrayList<ArrayList<String>>();
}
#Override
public int getColumnCount() {
if(data.isEmpty()) return 0;
return data.get(0).size();
}
#Override
public int getRowCount() {
return data.size();
}
#Override
public Object getValueAt(int rowIndex, int columnIndex) {
if(rowIndex > getRowCount()) return null;
if(columnIndex > getColumnCount()) return null;
return data.get(rowIndex).get(columnIndex);
}
public void populate(Collection c) {
data.clear();
for(Item i : c.getItems()) {
ArrayList<String> row = new ArrayList<String>();
for(Property p : i.getProperties().values()) {
row.add(p.toString());
}
data.add(row);
}
fireTableDataChanged();
}
}
Here's a complete example that may prove helpful. As the sample Map is unmodifiable, I refer you to #mKorbel's example on how to override isCellEditable() and setValueAt().
import java.awt.EventQueue;
import java.awt.GridLayout;
import java.util.Map;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;
/** #see https://stackoverflow.com/questions/9132987 */
public class EnvTableTest extends JPanel {
public EnvTableTest() {
this.setLayout(new GridLayout());
this.add(new JScrollPane(new JTable(new EnvDataModel())));
}
private static class EnvDataModel extends AbstractTableModel {
private Map<String, String> data = System.getenv();
private String[] keys;
public EnvDataModel() {
keys = data.keySet().toArray(new String[data.size()]);
}
#Override
public String getColumnName(int col) {
if (col == 0) {
return "Key";
} else {
return "Value";
}
}
#Override
public int getColumnCount() {
return 2;
}
#Override
public int getRowCount() {
return data.size();
}
#Override
public Object getValueAt(int row, int col) {
if (col == 0) {
return keys[row];
} else {
return data.get(keys[row]);
}
}
}
private void display() {
JFrame f = new JFrame("EnvTableTest");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(this);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
new EnvTableTest().display();
}
});
}
}
You could try to make the changes of populate more atomic.
public void populate(Collection c) {
ArrayList<ArrayList<String>> data2 = new ArrayList<ArrayList<String>>();
for(Item i : c.getItems()) {
ArrayList<String> row = new ArrayList<String>();
for(Property p : i.getProperties().values()) {
row.add(p.toString());
}
data2.add(row);
}
data = data2;
fireTableDataChanged();
}
I am guessing that populate is called again before a prior populate call finished. And probably c is changed during its iteration.
1) your TableModel is un_completed, I miss there lots or required methods for JTable's life_cycle, starting with TableHeader etc.
2) since there are lots of AbstactTableModels based on HashMap, I'd suggest to return arrays type implemented in API directly
Vector<Vector<Object or String>> data;
String[][] or Object[][]
instead of
ArrayList<ArrayList<String>> data;
simple explanations is that XxxList returs column and Vector or String[] returns Row
3) I'd suggest to use DefaultTableModel directly then you'll never need to solve duplicates or missed column/row
I would like to populate a JTable during runtime with many rows (lets say 10000). But all my attempts are very poor and inefficient.
Starting point is the addData method which gets a List of Objects representing a row. I tried to fill the table via a SwingWorker but this only works for small data for me.
Another attempt was setting the data directly without using any kind of thread, but this is also very slow, at least the UI isn't blocked like its the case with the SwingWorker.
So how do you do this is general? The table should be filled row by row or chunkwise but not all by one and the vertical scrollbar should be scrollable meanwhile.
My TableModel:
public class MyTableModel extends AbstractTableModel {
/**
*
*/
private static final long serialVersionUID = 1L;
String[] columnNames;
public Map<Long, ErrorMessage> data = new LinkedHashMap<Long, ErrorMessage>();
public MyTableModel(String[] header) {
columnNames = header;
}
public String getColumnName(int col) {
return columnNames[col].toString();
}
#Override
public int getColumnCount() {
return columnNames.length;
}
#Override
public int getRowCount() {
return data.size();
}
#Override
public Object getValueAt(int row, int col) {
.
.
return value;
}
public void addRow(long id, MyDataObject o) {
data.put(id, m);
fireTableRowsInserted(0,nqm_messages.size()-1);
}
}
SwingWorker implementation:
class TableSwingWorker extends SwingWorker<MyTableModel, MyDataObject> {
private final MyTableModel tableModel;
List<MyDataObject> messages;
public TableSwingWorker(MyTableModel tableModel, List<MyDataObject> dataList) {
this.tableModel = tableModel;
this.messages = new LinkedList<MyDataObject>(mm);
}
#Override
protected MyTableModel doInBackground() throws Exception {
for(MyDataObject s : messages) {
publish(s);
}
return tableModel;
}
#Override
protected void process(List<MyDataObject> chunks) {
for(MyDataObject row : chunks){
Long l = Long.parseLong(row.getId());
tableModel.addRow(l, row);
}
}
}
Add Objects to JTable:
public void addData(List<MyDataObject> o) {
MyTableModel m = (MyTableModel)table.getModel();
(new TableSwingWorker(m,o)).execute();
//for(int i=0; i < mm.size();i++) {
// long l = Long.parseLong(mm.get(i).getId());
// m.addRow(l, mm.get(i));
//}
}
So, a number of things have being identified from the comments...
You need to correctly fire the row inserted method, indicating only those rows that have being added and where they have being updated. This very important, as the the table has being optimised for speed
You should provide batch add method for your table model, allowing you to more easily add multiple rows in a single or as few steps as possible
You should have the SwingWorker periodically sleep or yield, to allow it time to publish the results.
So, in this example, I'm adding 1, 000, 000 rows. In my test it took slightly under 1 second...
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
public class TestTableLoad01 {
public static void main(String[] args) {
new TestTableLoad01();
}
public TestTableLoad01() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
MyTableModel model = new MyTableModel();
JTable table = new JTable(model);
table.setDefaultRenderer(Date.class, new TimeCellRenderer());
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new JScrollPane(table));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
TableSwingWorker worker = new TableSwingWorker(model);
worker.execute();
}
});
}
public class TimeCellRenderer extends DefaultTableCellRenderer {
private DateFormat df;
public TimeCellRenderer() {
df = new SimpleDateFormat("HH:mm:ss");
}
#Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
if (value instanceof Date) {
value = df.format(value);
}
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
return this;
}
}
public class MyTableModel extends AbstractTableModel {
private String[] columnNames = new String[]{"Date", "Row"};
private List<RowData> data;
public MyTableModel() {
data = new ArrayList<>(25);
}
#Override
public Class<?> getColumnClass(int columnIndex) {
return columnIndex == 0 ? Date.class : Integer.class;
}
#Override
public String getColumnName(int col) {
return columnNames[col];
}
#Override
public int getColumnCount() {
return columnNames.length;
}
#Override
public int getRowCount() {
return data.size();
}
#Override
public Object getValueAt(int row, int col) {
RowData value = data.get(row);
return col == 0 ? value.getDate() : value.getRow();
}
public void addRow(RowData value) {
int rowCount = getRowCount();
data.add(value);
fireTableRowsInserted(rowCount, rowCount);
}
public void addRows(RowData... value) {
addRows(Arrays.asList(value));
}
private void addRows(List<RowData> rows) {
int rowCount = getRowCount();
data.addAll(rows);
fireTableRowsInserted(rowCount, getRowCount() - 1);
}
}
public class RowData {
private Date date;
private int row;
public RowData(int row) {
this.date = new Date();
this.row = row;
}
public Date getDate() {
return date;
}
public int getRow() {
return row;
}
}
public class TableSwingWorker extends SwingWorker<MyTableModel, RowData> {
private final MyTableModel tableModel;
public TableSwingWorker(MyTableModel tableModel) {
this.tableModel = tableModel;
}
#Override
protected MyTableModel doInBackground() throws Exception {
// This is a deliberate pause to allow the UI time to render
Thread.sleep(2000);
System.out.println("Start polulating");
for (int index = 0; index < 1000000; index++) {
RowData data = new RowData(index);
publish(data);
Thread.yield();
}
return tableModel;
}
#Override
protected void process(List<RowData> chunks) {
System.out.println("Adding " + chunks.size() + " rows");
tableModel.addRows(chunks);
}
}
}
I have a JTable using AbstractTableModel where I have a JCheckBox in the first column for selecting rows. Now, I need to get the selected rows from the table which are checked. Right now, I am sequentially traversing from first row to the last row and getting all the rows that are selected like the following,
List<Integer> selectedRows = new ArrayList<Integer>();
for(int i = 0; i < table.getRowCount(); i++) {
if((Boolean) table.getValuAt(i, 0)) {
selectedRows.add(i);
}
}
The problem here is, I need to traverse all the rows when ever I need to get the selected rows. Right now I am having 10 to 20 rows. But in future I will get around 5000 rows. My question is, if there are 5000 rows and if the user selects only 5000nd (last record) row then I need to traverse all the 5000 rows to get the selected row. Which I think is not a good approach.
One approach which I want to implement is, to add a listener to the JCheckBox column, such that when ever there is a change (SELECTED/DESELECTED) then I need to update my array of the selected rows in the listener class. In this listener class when ever user selectes a JCheckBox I need to call table.getSelectedRow(..) and I need to store if that JCheckBox is selected.
Are there any better approaches ?
In the example below, the TableModel updates a Set<Integer> checked in the implementation of setValueAt(). The model of an adjacent JList listens to the table's model and displays the currently selected row numbers. The example assumes that the number of selected rows is small compared to the number of rows. Note the use of TreeSet, whose iterator retains the natural order of the elements.
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridLayout;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.swing.BorderFactory;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
/** #see http://stackoverflow.com/a/13919878/230513 */
public class CheckTable {
private static final CheckModel model = new CheckModel(5000);
private static final JTable table = new JTable(model) {
#Override
public Dimension getPreferredScrollableViewportSize() {
return new Dimension(150, 300);
}
};
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame f = new JFrame("CheckTable");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setLayout(new GridLayout(1, 0));
f.add(new JScrollPane(table));
f.add(new DisplayPanel(model));
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
});
}
private static class DisplayPanel extends JPanel {
private DefaultListModel dlm = new DefaultListModel();
private JList list = new JList(dlm);
public DisplayPanel(final CheckModel model) {
super(new GridLayout());
this.setBorder(BorderFactory.createTitledBorder("Checked"));
this.add(new JScrollPane(list));
model.addTableModelListener(new TableModelListener() {
#Override
public void tableChanged(TableModelEvent e) {
dlm.removeAllElements();
for (Integer integer : model.checked) {
dlm.addElement(integer);
}
}
});
}
}
private static class CheckModel extends AbstractTableModel {
private final int rows;
private List<Boolean> rowList;
private Set<Integer> checked = new TreeSet<Integer>();
public CheckModel(int rows) {
this.rows = rows;
rowList = new ArrayList<Boolean>(rows);
for (int i = 0; i < rows; i++) {
rowList.add(Boolean.FALSE);
}
}
#Override
public int getRowCount() {
return rows;
}
#Override
public int getColumnCount() {
return 2;
}
#Override
public String getColumnName(int col) {
return "Column " + col;
}
#Override
public Object getValueAt(int row, int col) {
if (col == 0) {
return row;
} else {
return rowList.get(row);
}
}
#Override
public void setValueAt(Object aValue, int row, int col) {
boolean b = (Boolean) aValue;
rowList.set(row, b);
if (b) {
checked.add(row);
} else {
checked.remove(row);
}
fireTableRowsUpdated(row, row);
}
#Override
public Class<?> getColumnClass(int col) {
return getValueAt(0, col).getClass();
}
#Override
public boolean isCellEditable(int row, int col) {
return col == 1;
}
}
}
I agree with kleopatra. When you create a subclass of the AbstractTableModel, you'll override the setValue( Object value, int rowIndex, int colIndex ). In your overridden method, you just check if the column is the one with your check box, and if so, update the internal data structure appropriately. You can also add a method getCheckedRows() that returns a List< Integer > with the rows in which the check boxes have been selected.