We have fairly complicated model in a JTable. In new development I noticed that GUI does not refresh when I call fireTableChanged(...) for individual cells.
So, my question is:
What do I put into TableModelEvent - model row id or view row id?
Looking into JTable code (I have jdk1.8.0_202):
public class JTable extends JComponent implements TableModelListener, Scrollable,
...
public void tableChanged(TableModelEvent e) {
...
int modelColumn = e.getColumn();
int start = e.getFirstRow();
int end = e.getLastRow();
Rectangle dirtyRegion;
if (modelColumn == TableModelEvent.ALL_COLUMNS) {
// 1 or more rows changed
dirtyRegion = new Rectangle(0, start * getRowHeight(),
getColumnModel().getTotalColumnWidth(), 0);
}
else {
// A cell or column of cells has changed.
// Unlike the rest of the methods in the JTable, the TableModelEvent
// uses the coordinate system of the model instead of the view.
// This is the only place in the JTable where this "reverse mapping"
// is used.
int column = convertColumnIndexToView(modelColumn);
dirtyRegion = getCellRect(start, column, false);
}
I see that in order to calculate dirty region, it converts column index, but does not do the same for row.
What does "reverse mapping" comment mean?
Looks like a bug in Swing to me.
What do you think?
UPDATE
My code is simple:
model.fireTableChanged(new TableModelEvent(model, rowNumber, rowNumber, columnNumber));
GUI does NOT refresh the cell.
UPDATE2
The issue is in my model which is too complicated to post it here. :(
I cannot blame JTable. It is designed this way. The only possible addition to it is RowSorter, and in there it does correct conversion:
private void repaintSortedRows(ModelChange change) {
...
int modelIndex = change.startModelIndex;
while (modelIndex <= change.endModelIndex) {
int viewIndex = convertRowIndexToView(modelIndex++);
if (viewIndex != -1) {
Rectangle dirty = getCellRect(viewIndex, columnViewIndex,
false);
int x = dirty.x;
int w = dirty.width;
if (eventColumn == TableModelEvent.ALL_COLUMNS) {
x = 0;
w = getWidth();
}
repaint(x, dirty.y, w, dirty.height);
}
}
}
Thanks everybody. Sorry for disturbance.
My code is simple:
model.fireTableChanged(new TableModelEvent(model, rowNumber, rowNumber, columnNumber));
That is not how you change data in a JTable. You should NOT be invoking that method directly. It is the responsibility of the TableModel to invoke that method when data is changed.
The point of using a TableModelListener is to listen for changes in the TableModel. You only need to implement the listener if you want to do special processing AFTER the data has changed as I demonstrated in the link I provided in my comment.
If you have data in an existing cell and you want to change its value then can do something like:
model.setValueAt("new value", 0, 0);
If you want to add a new row of data you use:
model.addRow(...);
The point is all changes should be done via the TableModel.
Note the JTable also has a convenience setValueAt(...) method which will invoke the model for you.
Related
I'm currently writing an application where I present to the user amongst other things links to websites in a JTable. I already set up my JTable correctly to open up the corresponding website upon clicking the regarding cell. However I struggle with correctly formatting the cell so that users know they actually have the possibility of clicking the cell for instantly opening the website.
Hence what I want to achieve is to have the colour of the link being blue at least and even better also underlined. I searched through different articles on SO regarding this but couldn't quite grasp how the things explained there work together - despite I'm not entirely sure if these things would have even be what I'm actually looking for.
The way I fill my table is the following:
String[][] rowData = new String[entries.size() + 1][entries.get(0).length + 1];
rowData[0] = columnNames;
int i = 1;
Iterator<String[]> iterator = entries.iterator();
while (iterator.hasNext()) {
rowData[i] = iterator.next();
i++;
}
tblEntries = new JTable(rowData, columnNames);
entries in this case is an ArrayList that is passed over by the database handler and - as the name suggests - contains all entries for the table. After reading the ArrayList into the respective Array I initialize the table as seen in the last row. Now all the links are actually stored in all rows > 0 and the 4th column.
My first approach was doing something like this:
for (int j = 0; j < entries.size(); j++) {
for (int j2 = 0; j2 < entries.get(0).length; j2++) {
tblEntries.editCellAt(row, column, e);
}
}
where e should be an event that checks wheter the conditions for a link are satisfied or not and execute the formatting accordingly. However I don't really now what kind of event is needed to pass it to the function.
An other approach I saw in a different SO article was to use the prepareRenderer method to specify the conditions for rendering the content correctly. However apparently this seems to be only possible for own implementations of a JTable which I'd like to avoid as tblEntries.prepareRenderer() and applying a new TableCellRenderer or DefaultTableCellRenderer doesn't give me the function that I need to override according to above mentioned SO article.
So, what would be the best and most convenient way to tackle this problem down? Thanks in advance for your any adivce and help.
SOLUTION:
For anyone facing a similar problem I'll put my solution here. As suggested by #camickr the best solution is a custom DefaultTreeCellRenderer the problem in this specific scenario however is that it will also render the specific table-header (which obviously doesn't contain any links) in the link format. Hence I searched a bit further and found this website where I found a working code for customising where the renderer should be applied.
In the end I came up with this code:
String[][] rowData = new String[entries.size() + 1][entries.get(0).length + 1];
rowData[0] = columnNames;
int i = 1;
Iterator<String[]> iterator = entries.iterator();
while (iterator.hasNext()) {
rowData[i] = iterator.next();
i++;
}
tblEntries = new JTable(rowData, columnNames) {
#Override
public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
Component c = super.prepareRenderer(renderer, row, column);
if (row > 0 && column == 4) {
c = super.prepareRenderer(new LinkRenderer(), row, column);
}
return c;
}
};
For reference for the LinkRenderer see accepted answer below.
what I want to achieve is to have the colour of the link being blue at least and even better also underlined.
This is controlled by the renderer. The default renderer for the JTable is a JLabel.
You can easily create a custom renderer to display the text in blue:
DefaultTableCellRenderer renderer = new DefaultTableCellRenderer();
renderer.setForeground( Color.BLUE );
table.getColumnModel().getColumn(3).setCellRenderer( renderer );
Unfortunately underlining the text will be more difficult. Underlining text in a component can be achieved by setting a property of the Font which is easy enough to do for a JLabel:
JLabel label = new JLabel("Underlined label");
Font font = label.getFont();
Map<TextAttribute, Object> map = new HashMap<TextAttribute, Object>();
map.put(TextAttribute.FONT, font);
map.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
font = Font.getFont(map);
label.setFont(font);
However, you can't just set the Font for the renderer because when each cell is rendered the default renderer will reset the Font to be the Font used by the table.
So if you want to implement a custom renderer with a custom Font, you need to extend the DefaultTableCellRenderer and override the getTableCellRendererComponent(….) method. The code might be something like:
class LinkRenderer extends DefaultTableCellRenderer
{
private Font underlineFont;
public LinkRenderer()
{
super();
setForeground( Color.BLUE );
underlineFont = .getFont();
Map<TextAttribute, Object> map = new HashMap<TextAttribute, Object>();
map.put(TextAttribute.FONT, underlineFont);
map.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
underLinefont = Font.getFont(map);
}
#Override
public Component getTableCellRendererComponent(
JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
{
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
setFont( underlineFont );
return this;
}
}
Read the section from the Swing tutorial on Renderers and Editors for more information.
So the other approach is to NOT use a custom renderer but instead you can add HTML to the table model. A JLabel can display simple HTML.
So the text you add to the model would be something like:
String text = "<html><u><font color=blue>the link goes here</font></ul></html>";
I have a jtable.
Some of the cells contain very long strings and trying to scroll left and right through it is difficult. My question is whether it is possible to show a row from a JTable in a pop-up eg showDialog type box (ie where the selected row is organised as a column).
Even a link to a tutorial would do.
I have scoured the internet but I don't think I'm really using the correct keywords as I get a lot of right-click options.
If this is not possible are there any other suggestions for how to do this?
As shown here, the JOptionPane factory methods will display the Object passed in the message parameter. If that message is a one column JTable, you can recycle any custom renderers and editors that were applied to the original table.
In outline,
Add a ListSelectionListener to your table and get the selectedRow.
Iterate through the table's model and construct a newModel whose rows are the columns of the selectedRow.
Create a JTable newTable = new JTable(newModel).
Apply any non-default renderers and editors.
Pass a new JScrollPane(newTable) as the message parameter to your chosen JOptionPane method.
Starting from this example, the following listener displays the dialog pictured.
table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
#Override
public void valueChanged(ListSelectionEvent e) {
int selectedRow = table.convertRowIndexToModel(table.getSelectedRow());
if (selectedRow > -1) {
DefaultTableModel newModel = new DefaultTableModel();
String rowName = "Row: " + selectedRow;
newModel.setColumnIdentifiers(new Object[]{rowName});
for (int i = 0; i < model.getColumnCount(); i++) {
newModel.addRow(new Object[]{model.getValueAt(selectedRow, i)});
}
JTable newTable = new JTable(newModel) {
#Override
public Dimension getPreferredScrollableViewportSize() {
return new Dimension(140, 240);
}
};
// Apply any custom renderers and editors
JOptionPane.showMessageDialog(f, new JScrollPane(newTable),
rowName, JOptionPane.PLAIN_MESSAGE);
}
}
});
I want to show all the values in the row, each in their cel, organised vertically- that's what I meant by 'in a column'.
That should be in the question, not in the comment.
There is no default functionality for this but you can do it yourself.
You could create a JPanel (maybe using a GridBagLayout), with two labels in a row to represent the data in a column of the selected row of the table.
For the data in the first label you would use the getColumnName(...) method of the TableModel.
For the data in the second label you would use the getValueAt(...) method of the TableModel.
Another option is to simply display a tool tip for the cell. See the section from the Swing tutorial on Specifying ToolTips For Cells for more information.
You may use the following ListSelectionListener:
final JTable dialogTable =new JTable();
table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
#Override
public void valueChanged(ListSelectionEvent event) {
int selectedRow = table.getSelectedRow();
if (selectedRow > -1) {
int columnCount = table.getModel().getColumnCount();
Object[] column = new Object[]{"Row "+(selectedRow+1)};
Object[][] data = new Object[columnCount][1];
for (int i = 0; i < columnCount; i++) {
Object obj = table.getModel().getValueAt(selectedRow, i);
data[i][0] = obj;
}
dialogTable.setModel(new DefaultTableModel(data, column));
JOptionPane.showMessageDialog(null, new JScrollPane(dialogTable));
}
}
});
This is going to show a message dialog which contains a JTable with data that is derived from the selected row. Hope this helps you.
I have window that shows log events in table.
For example, user is reading text in some row of table. When new logs come, they are added in the beginning of the table, and the row, the user was reading moves down. I have a requirement to prevent JScrollPane to scroll when new rows are added at the beginning of the table. I tried diffrent things but nothing helped. Can someone advice me how to implement this?
Thanks in advance!
Tricky task :-) The moving out happens because by default the scroll position is not adjusted in any way: adding rows above simply keeps the vertical scroll position which then points to a different row as before.
What's needed:
keep track of the visibleRect/last row before the insert
listen to model changes of type insert
calculate the new visible rect such that the old last row is scrolled back into the view
trigger the scroll
It's tricky because we need to listen to model changes. That listener is a neighbour of the modelListener registered internally by the table which updates itself on changes. So we need to be sure to act after the internal changes are done and at the same time use information before the internal update.
A dirty solution - depending on the usual sequence of swing listener notification which is last-added-first-notified (beware: DONT in production code! It will break easily, f.i. when changing the LAF) - is to gather the before-state when notified and adjust the scroll position wrapped in an invokeLater:
final DefaultTableModel model = new DefaultTableModel(20, 2);
for (int i = 0; i < model.getRowCount(); i++) {
model.setValueAt(i, i, 0);
}
final JTable table = new JTable(model);
Action action = new AbstractAction("add row at 0") {
#Override
public void actionPerformed(ActionEvent e) {
model.insertRow(0, new Object[] {});
}
};
JXFrame frame = wrapWithScrollingInFrame(table, "modify scroll");
TableModelListener l = new TableModelListener() {
#Override
public void tableChanged(TableModelEvent e) {
if (!TableUtilities.isInsert(e)) return;
final Rectangle r = table.getVisibleRect();
final int lastRow = table.rowAtPoint(
new Point(0, r.y + r.height - 5));
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
// assuming
// a) the insert was exactly 1 row
// b) was _before_ the old last row in view-coordinates
Rectangle cell = table.getCellRect(lastRow + 1, 0, true);
cell.translate(0, -r.height + cell.height);
cell.height = r.height;
table.scrollRectToVisible(cell);
}
});
}
};
model.addTableModelListener(l);
The appropriate way of getting the before-state cleanly depends on your exact context. You can keep track of the first/last row during the lifetime of the table, updating them in (one or all)
a ListSelectionListener to the row selectionModel (if the row the user is reading is always selected)
a ChangeListener to the vertical scrollBar
a ComponentListener to scrollPane's size changes
I am trying to display the data of hidden column as tooltip. Hiding is working perfectly using the following code:
JTable table = new JTable(model){
//Implement table cell tool tips.
public String getToolTipText(MouseEvent e) {
String tip = null;
java.awt.Point p = e.getPoint();
int rowIndex = rowAtPoint(p);
int colIndex = columnAtPoint(p);
int realColumnIndex = convertColumnIndexToModel(colIndex);
try {
tip = getValueAt(rowIndex, 8).toString();
} catch (RuntimeException e1) {
//catch null pointer exception if mouse is over an empty line
}
return tip;
}
};
TableColumnModel tcm = table.getColumnModel();
TableColumn tc;
for(int i = 1; i <= 7; i++){
tc = tcm.getColumn(8);
tcm.removeColumn(tc);
}
But the tooltip is not showing the data of hidden column (getValue function is not returning value). So do hiding the column delete the data as well ?
You do not need to for loop as you do not use the i variable ;-)
The removeColumn on the JTable does not remove the data from the model, as clearly stated in the javadoc
Removes aColumn from this JTable's array of columns. Note: this method does not remove the column of data from the model; it just removes the TableColumn that was responsible for displaying it.
There is no mention in the javadoc for the same method on the TableColumnModel, but I would assume it works the same way, but you can always give it a try to call it on the JTable instead
The real problem in your code is the use of getValueAt, which uses the row and column index of the table, and not of the model
Note: The column is specified in the table view's display order, and not in the TableModel's column order. This is an important distinction because as the user rearranges the columns in the table, the column at a given index in the view will change. Meanwhile the user's actions never affect the model's column ordering.
And since you removed that column, it simply does not exists for the table. Call the getValue method on the model instead, and do not forget to convert the row index
I have a JTable with a custom Cell Renderer for multi-line cells. Everything is ok, the JTable is painted ok in the screen and I am very happy with it, but ast night when I tried to simply print it, I came up with a very strange issue. Using:
table.print(PrintMode.FIT_WIDTH, new MessageFormat("..."), new MessageFormat("..."));
I saw that the table did not print entirely. Then using another class made from a colleague for printing JTables I had the same result:
The table (with multi-line cells) needed 22 pages to print. The printed document (which I only viewed in xps format since I do not own a printer) had also 22 pages. But up to page 16 everything was printed as expected and after that only the borders and the column headers of the table were printed.
Strangely (to me) enough, when I tried to print the table using another cell renderer that does not allow for multi line cells, the table needed exactly 16 pages and was printed entirely, albeit the cropping in the lengthy cell values.
I searched all over the net but I had no luck. Does anybody know why could this be happening? Is there a solution?
Update:
My cell renderer is the following:
public class MultiLineTableCellRenderer extends JTextPane implements TableCellRenderer {
private List<List<Integer>> rowColHeight = new ArrayList<List<Integer>>();
public MultiLineTableCellRenderer() {
setOpaque(true);
}
public Component getTableCellRendererComponent(
JTable table, Object value, boolean isSelected, boolean hasFocus,
int row, int column) {
String s = (String)value;
if (s.equals("<περιοδάριθμος>")) {
setForeground(Color.blue);
}
else if(s.equals("<παραγραφάριθμος>")) {
setForeground(Color.red);
}
else {
setForeground(Color.black);
}
setBackground(new Color(224, 255, 255));
if (isSelected) {
setBackground(Color.GREEN);
}
setFont(table.getFont());
setFont(new Font("Tahoma", Font.PLAIN, 10));
if (hasFocus) {
setBorder(UIManager.getBorder("Table.focusCellHighlightBorder"));
if (table.isCellEditable(row, column)) {
setForeground(UIManager.getColor("Table.focusCellForeground"));
setBackground(UIManager.getColor("Table.focusCellBackground"));
}
} else {
setBorder(new EmptyBorder(1, 2, 1, 2));
}
if (value != null) {
setText(value.toString());
} else {
setText("");
}
adjustRowHeight(table, row, column);
SimpleAttributeSet bSet = new SimpleAttributeSet();
StyleConstants.setAlignment(bSet, StyleConstants.ALIGN_CENTER);
StyleConstants.setFontFamily(bSet, "Tahoma");
StyleConstants.setFontSize(bSet, 11);
StyledDocument doc = getStyledDocument();
doc.setParagraphAttributes(0, 100, bSet, true);
return this;
}
private void adjustRowHeight(JTable table, int row, int column) {
int cWidth = table.getTableHeader().getColumnModel().getColumn(column).getWidth();
setSize(new Dimension(cWidth, 1000));
int prefH = getPreferredSize().height;
while (rowColHeight.size() <= row) {
rowColHeight.add(new ArrayList<Integer>(column));
}
List<Integer> colHeights = rowColHeight.get(row);
while (colHeights.size() <= column) {
colHeights.add(0);
}
colHeights.set(column, prefH);
int maxH = prefH;
for (Integer colHeight : colHeights) {
if (colHeight > maxH) {
maxH = colHeight;
}
}
if (table.getRowHeight(row) != maxH) {
table.setRowHeight(row, maxH);
}
}
}
Furthermore, if you test the following very simple example you will notice that something is terribly wrong with the printing, but I really can't find what!
public static void main(String[] args) throws PrinterException {
DefaultTableModel model = new DefaultTableModel();
model.addColumn("col1");
model.addColumn("col2");
model.addColumn("col3");
int i = 0;
for (i = 1; i <= 400; i++) {
String a = "" + i;
model.addRow(new Object[]{a, "2", "3"});
}
JTable tab = new JTable(model);
tab.print();
}
I believe you are having the same problem that I had when I asked this question:
Truncated JTable print output
I found a solution to my problem, and I believe it may help you as well.
The answer is here:
Truncated JTable print output
To summarize my answer:
If your TableCellRenderer is the only place in your code where you are setting rows to their correct height, then you are going to run into trouble caused by an optimization inside JTable: JTable only invokes TableCellRenderers for cells that have been (or are about to be) displayed.
If not all of your cells have been displayed on-screen, then not all of your renderers have been invoked, and so not all of your rows have been set to the desired height. With your rows not being their correct height, your JTable overall height is incorrect. After all, part of determining the overall JTable height is accounting for the height of each of that table's rows. If the JTable overall height isn't correct, this causes the print to truncate, since the JTable overall height is a parameter that is considered in the print layout logic.
An easy (but perhaps not squeaky clean) way to fix this is to visit all of your cell renderers manually before printing. See my linked answer for an example of doing this. I actually chose to do the renderer visitation immediately after populating my table with data, because this fixes some buggy behavior with the JTable's scrollbar extents (in addition to fixing the printing.)
The reason the table looks and works OK on-screen even when printing is broken, is because as you scroll around in the table, the various renderers are invoked as new cells come on screen, and the renderers set the appropriate row height for the newly visible rows, and various dimensions are then are recalculated on the fly, and everything works out OK in the end as you interact with the table. (Although you may notice that the scrollbar "extent" changes as you scroll around, which it really shouldn't normally do.)
Strange thing is that behavior is not deterministic.
Such behavior always makes me suspect incorrect synchronization.
It's not clear how your TableCellRenderer works, but you might try HTML, which is supported in many Swing components.
Another useful exercise is to prepare an sscce that reproduces the problem in minature. A small, complete example might expose the problem. It would also allow others to test your approach on different platforms.
This answer is probably too late for the one who asked this question, but for everybody with a similar problem, here is my solution;
I had exactly the same problem, I have my own TableCellRenderer to handle multi-line Strings which works flawless for showing the table but makes the printing of the table unreliable.
My solutions consists of 2 parts;
Part 1: I have created my own TableModel, in the getValueAt() I 'copied' a part of the StringCellRenderer logic, I make it recalculate and set the height of the table row in case af a multi-line String AND return the String as HTML with 'breaks' instead of line-separators.
Part 2: Before invoking the table.print() I call the getValueAt() for all cells (a for-loop over the columns with an inner for loop over the rows invoking the getValueAt()), this has to be done 'manually' because the print functionality doesn't invoke all getValueAt's (I have found reasons on different fora regarding this issue regarding the execution of the TableCellRenderers).
This way the clipping of the table is done like it is supposed to, only complete rows are printed per page and it devides the rows over severall pages if required with a table header at each page.