JTable with multi-line cell renderer prints very weird - java

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.

Related

How to change a cell in a jTable programmatically?

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>";

JTable does not refresh on fireTableChanged

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.

JEditorPane.getPreferredSize not always working in Java 9?

This question is about different behaviour of JEditorPane in Java 8 and Java 9. I’d like to know if others have experienced the same, whether it could be a bug in Java 9, and if possible have your input to how to handle it in Java 9.
Context: In our (age-old) code base we are using a subclass of JTable, and for rendering multi-line HTML in one of the columns we are using a subclass of JEditorPane. We are using JEditorPane.getPreferredSize() for determining the height of the content and using it for setting the height of the table row. It’s been working well for many years. It doesn’t work in Java 9; the rows are displayed just 10 pixels high. Seen both on Windows and Mac.
I should like to show you two code examples. If the first and shorter one suffices for you, feel free to skip the second and longer one.
MCVE:
JEditorPane pane = new JEditorPane("text/html", "");
pane.setText("<html>One line</html>");
System.out.println(pane.getPreferredSize());
pane.setText("<html>Line one<br />Line 2<br />Third line<br />Line four</html>");
System.out.println(pane.getPreferredSize());
Output in Java 8 (1.8.0_131):
java.awt.Dimension[width=48,height=15]
java.awt.Dimension[width=57,height=60]
And on Java 9 (jdk-9.0.4):
java.awt.Dimension[width=49,height=15]
java.awt.Dimension[width=58,height=0]
In Java 9 the first time I set the text, the preferred height reflects it. Every subsequent time it doesn’t.
I have searched to see if I could find any information on a bug that might account for this, did not find anything relevant.
Question: Is this intended (change of) behaviour? Is it a bug??
Longer example
public class TestJEditorPaneAsRenderer extends JFrame {
public TestJEditorPaneAsRenderer() {
super("Test JEditorPane");
MyRenderer renderer = new MyRenderer();
String html2 = "<html>one/2<br />two/2</html>";
String html4 = "<html>one of four<br />two of four<br />"
+ "three of four<br />four of four</html>";
JTable table = new JTable(new String[][] { { html2 }, { html4 } },
new String[] { "Dummy col title" }) {
#Override
public TableCellRenderer getDefaultRenderer(Class<?> colType) {
return renderer;
}
};
add(table);
setSize(100, 150);
setDefaultCloseOperation(EXIT_ON_CLOSE);
}
public static void main(String[] args) {
System.out.println(System.getProperty("java.version"));
new TestJEditorPaneAsRenderer().setVisible(true);
}
}
class MyRenderer extends JEditorPane implements TableCellRenderer {
public MyRenderer() {
super("text/html", "");
}
#Override
public Component getTableCellRendererComponent(
JTable table, Object value, boolean selected, boolean hasFocus, int row, int col) {
setText(value.toString());
Dimension preferredSize = getPreferredSize();
System.out.format("Row %d preferred size %s%n",row, preferredSize);
if (preferredSize.height > 0 && table.getRowHeight(row) != preferredSize.height) {
table.setRowHeight(row, preferredSize.height);
}
return this;
}
}
Result on Java 8 is as expected:
Output from Java 8:
1.8.0_131
Row 0 preferred size java.awt.Dimension[width=32,height=30]
Row 1 preferred size java.awt.Dimension[width=72,height=60]
Row 0 preferred size java.awt.Dimension[width=32,height=30]
Row 1 preferred size java.awt.Dimension[width=72,height=60]
On Java 9 the second row is not shown high enough so most of the lines are hidden:
Output from Java 9:
9.0.4
Row 0 preferred size java.awt.Dimension[width=33,height=30]
Row 1 preferred size java.awt.Dimension[width=73,height=0]
Row 0 preferred size java.awt.Dimension[width=33,height=0]
Row 1 preferred size java.awt.Dimension[width=73,height=0]
A possible fix is if I create a new renderer component each time by changing the body of getDefaultRenderer() to:
return new MyRenderer();
Now the table looks good as on Java 8. If necessary I suppose we could live with a similar fix in our production code, but it seems quite a waste. Especially if it’s only necessary until the behaviour change is reverted in a coming Java version.
I have faced similar problem, but with JLabel. My solution was to set the size of the JLabel which seems to force the component to recalculate its preferred size.
Try this:
JEditorPane pane = new JEditorPane("text/html", "");
pane.setText("<html>One line</html>");
System.out.println(pane.getPreferredSize());
pane.setText("<html>Line one<br />Line 2<br />Third line<br />Line four</html>");
// get width from initial preferred size, height should be much larger than necessary
pane.setSize(61, 1000);
System.out.println(pane.getPreferredSize());
In this casse the output is
java.awt.Dimension[width=53,height=25]
java.awt.Dimension[width=61,height=82]
Note: my fonts are different, that is why I get different dimensions.
Edit: I changed your getTableCellRendererComponent in the longer example like this and it is working for me. I am using jdk9.0.4, 64 bit.
#Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean selected, boolean hasFocus, int row, int col) {
setText(value.toString());
Dimension preferredSize = getPreferredSize();
setSize(new Dimension(preferredSize.width, 1000));
preferredSize = getPreferredSize();
System.out.format("Row %d preferred size %s%n", row, preferredSize);
if (preferredSize.height > 0 && table.getRowHeight(row) != preferredSize.height) {
table.setRowHeight(row, preferredSize.height);
}
return this;
}
Based on previous suggestion and digging in JDK sources I'm suggesting a little bit simpler solution:
setSize(new Dimention(0, 0))
Based on BasicTextUI.getPreferredSize implementation it forces to recalculate root view size
} else if (d.width == 0 && d.height == 0) {
// Probably haven't been layed out yet, force some sort of
// initial sizing.
rootView.setSize(Integer.MAX_VALUE, Integer.MAX_VALUE);
}

JTable Cell Font? (Java)

The program I am creating should work like Microsoft Excel, except in JAVA. It should also support cell formatting (Which is my problem). I have the code for detecting which cell is clicked, and what font to use working properly - I just can not figure out how to apply the Font to the cell! Google gave me CellRenderers, but it seems that cell renderers format the cell only when a condition is true. I want it to format with the specified Font it when it is called!
Can someone please help me, I am really confused!!!
I have already looked at the Java Tutorials.
My apologies if this question has been asked before!
this is what you are looking for,, this code snippet changes the font of all columns in a jTable..
I'm sure a slight modification should get your scenario covered.
for (int i = 0; i < jTable1.getColumnCount(); i ++) {
TableColumn col = jTable1.getColumnModel().getColumn(i);
col.setCellEditor(new MyTableCellEditor());
}
public class MyTableCellEditor extends AbstractCellEditor implements TableCellEditor {
JComponent component = new JTextField();
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int rowIndex, int vColIndex) {
((JTextField)component).setText((String)value);
((JTextField)component).setFont(new java.awt.Font("Arial Unicode MS", 0, 12));
return component;
}
}
This will change the font for all cells in the table - even when new columns or rows are added:
JTable table;
......
Object dce = table.getDefaultEditor(Object.class);
if(dce instanceof DefaultCellEditor) {
((DefaultCellEditor) dce).getComponent().setFont([your font]);
}

Auto adjust the height of rows in a JTable

In a JTable, how can I make some rows automatically increase height to show the complete multiline text inside? This is how it is displayed at the moment:
I do not want to set the height for all rows, but only for the ones which have multiline text.
The only way to know the row height for sure is to render each cell to determine the rendered height. After your table is populated with data you can do:
private void updateRowHeights()
{
for (int row = 0; row < table.getRowCount(); row++)
{
int rowHeight = table.getRowHeight();
for (int column = 0; column < table.getColumnCount(); column++)
{
Component comp = table.prepareRenderer(table.getCellRenderer(row, column), row, column);
rowHeight = Math.max(rowHeight, comp.getPreferredSize().height);
}
table.setRowHeight(row, rowHeight);
}
}
If only the first column can contain multiple line you can optimize the above code for that column only.
Camickr's solution did not work for me at all. My data model was dynamic though - it changed all the time. I guess the mentioned solution works for static data, like coming from an array.
I had JPanel for cell renderer component and it's preferred size wasn't set correctly after using prepareRenderer(...). The size was set correctly after the containing window was already visible and did repaint (2 times in fact after some unspecified, though short time). How could I call updateRowHeights() method shown above then and where would I do this? If I called it in (overriden) Table.paint() it obviously caused infinite repaints. It took me 2 days. Literally. The solution that works for me is this one (this is the cell renderer I used for my column):
public class GlasscubesMessagesTableCellRenderer extends MyJPanelComponent implements TableCellRenderer {
#Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
int row, int column) {
//this method updates GUI components of my JPanel based on the model data
updateData(value);
//this sets the component's width to the column width (therwise my JPanel would not properly fill the width - I am not sure if you want this)
setSize(table.getColumnModel().getColumn(column).getWidth(), (int) getPreferredSize().getHeight());
//I used to have revalidate() call here, but it has proven redundant
int height = getHeight();
// the only thing that prevents infinite cell repaints is this
// condition
if (table.getRowHeight(row) != height){
table.setRowHeight(row, height);
}
return this;
}
}
You must iterate over each row, get the bounding box for each element and adjust the height accordingly. There is no code support for this in the standard JTable (see this article for a solution for Java ... 1.3.1 =8*O).

Categories

Resources