JTable memory leak when setting individual row heights - java

I need to show multi-line content in a JTable. The actual content is a collection of objects maintained in a custom model, which extends DefaultTableModel and generates cell content on the fly by overriding getValueAt().
In order to have multi-line content, I have implemented a custom TableCellRenderer:
private class MultiLineCellRenderer extends JTextArea implements TableCellRenderer {
public MultiLineCellRenderer() {
setLineWrap(true);
setWrapStyleWord(true);
setOpaque(true);
setBorder(new EmptyBorder(-1, 2, -1, 2));
setRows(1);
}
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
String text = value == null ? "" : value.toString();
if (!getText().equals(text)) {
setText(text);
int newHeight = table.getRowHeight() * getLineCount();
if (table.getRowHeight(row) != newHeight)
table.setRowHeight(row, newHeight);
}
if (isSelected) {
setForeground(table.getSelectionForeground());
setBackground(table.getSelectionBackground());
} else {
setForeground(table.getForeground());
setBackground(table.getBackground());
}
return this;
}
}
Now if I populate the table with a few hundred rows (column count is 2), I see the AWT worker thread starting to max out one CPU core. At the same time, memory consuption goes up from ~100 MB to ten times that amount and further. That happens even if the application is not actually doing anything (no data loaded in the background, no user interaction) and stops only when I clear the collection from which the table gets its content.
By commenting out select sections of code, I have identified these lines as the culprit:
int newHeight = table.getRowHeight() * getLineCount();
if (table.getRowHeight(row) != newHeight)
table.setRowHeight(row, newHeight);
If I comment out this section, all table rows have the same height (1 row of text), but memory consumption stays around ~100 MB.
If I replace these lines with a single call to table.setRowHeight(row, 32), i.e. with a fixed value, memory consumption starts going up again indefinitely.
The following modification works, at the expense of all rows having the same height:
int newHeight = getRowHeight() * getLineCount();
if (table.getRowHeight() < newHeight)
table.setRowHeight(newHeight);
Bottom line: it seems setting individual row heights in a JTable creates a massive memory leak. Am I doing something wrong, or have I encountered an actual bug? In the latter case, are there any known fixes/workarounds?

Setting the row height triggers a redraw, which in turn triggers another call to the renderer. Therefore, it is important to set the row height only if it is different from the current one, in order to avoid an endles loop. This is what happens when you call setRowHeight() unconditionally, even with a fixed value.
A second issue is that each row comprises two cells, which may have different heights. The code above will set the row height to match the cell being rendered right now. When the other cell of that row gets rendered and has a different height, the row height gets changed again. That will trigger a redraw, also of the first column in the row. Since that will result in another height change, there’s the infine loop again.
Proof: the following code fixes this:
int newHeight = table.getRowHeight() * getLineCount();
if (table.getRowHeight(row) < newHeight)
table.setRowHeight(row, newHeight);
Now the row height will only increase, but never decrease, thus breaking the infinite loop. Side effect: if the cell contents change and now occupy fewer rows than before, the row height will not change to reflect this.
Bottom line: rendering a JTable with multiline cells is non-trivial, and SO has quite a few buggy examples. The only working example I found (thanks to another SO post) is at https://www.javaspecialists.eu/archive/Issue106.html.
Their solution is to store cell heights internally in the renderer (though this could also be done in the table model, whichever works best for your implementation). When calculating the height of a cell, store it, then get the maximum height of any cell in the row and use that. Also be sure to set the row height only if it differs from the current one.
That has fixed the memory leak/processor consumption issue, in addition to finally giving me a working example of how to calculate cell height properly.

Related

Add border BETWEEN cells in JTable

I'm trying to display a JTable as a grid, with lines between the cells. I've only been able to add borders within individual cells, though, which never looks correct; if I add full borders, I get a bunch of disconnected boxes, which looks ugly and wrong. Using MatteBorders (as the below code) looks a little better, but results in gaps where border lines don't quite meet.
public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
Component stamp = super.prepareRenderer(renderer, row, column);
int top = 1;
int left = 1;
int bottom = row == 7 ? 1 : 0; //Grid is always 8x8, this ensures the bottom/right will have full borders.
int right = column == 7 ? 1 : 0;
MatteBorder border = new MatteBorder(top, left, bottom, right, Color.BLACK);
if (stamp instanceof JComponent) {
((JComponent) stamp).setBorder(border);
}
return stamp;
}
I feel like there must be some way to do this properly, so that I just get grid lines between cell elements. What am I missing? If nothing else, is there a way to get MatteBorder to stretch across the gaps, or to push a normal border out slightly further so that the borders of adjacent cells overlap?
EDIT: Got it working with setShowGrid(true) and setGridColor(Color.BLACK).
Use JTable.setShowGrid(true) to show default border or use setShowHorizontalLines(boolean showHorizontalLines) or setShowVerticalLines(boolean showVerticalLines) to show only horizontal or vertical lines

iText cell widths seem to behave inconsistently

I have a function which I've been using for a good while in java which calculates cell widths for a pdftable from a given array of string values.
It works very well, and I recently wrote a version of the function in c# and it doesn't give the expected result (i.e text is wrapped to multiple lines) - both java and c# code shown below any help much appreciated
This is the Java version;
float[] CalculateCellWidths(String[] CellHeaders, Font CellFont)
{
float[] CellWidths = new float[CellHeaders.length];
for (int i = 0; i < CellHeaders.length; i++)
{
CellWidths[i] = CellFont.getCalculatedBaseFont(true).getWidthPoint(CellHeaders[i], CellFont.getCalculatedSize());
}
return CellWidths;
}
This is the C# version;
float[] CalculateCellWidths(String[] CellHeaders, iTextSharp.text.Font CellFont)
{
float[] CellWidths = new float[CellHeaders.Length];
for (int i = 0; i < CellHeaders.Length; i++)
{
CellWidths[i] = CellFont.GetCalculatedBaseFont(true).GetWidthPoint(CellHeaders[i], CellFont.CalculatedSize);
}
return CellWidths;
}
I see from your comment that the problem has been solved, but that you don't know why, so here's a small explanation about LockWidth.
There are two ways to define the width of a table. Either the table takes a percentage of the available width. This is a relative width that depends on page size and page margins. Historically, this percentage is 80% and the table is centered. You can change this width percentage with the setWidthPercentage() method. If you use a method to set the widths of the invidual columns, those widths will be treated as relative widths, for instance { 10, 10, 20 } means that you have a table with 3 columns where the first two columns take a quarter of the available width and the third column takes half.
You can also set the absolute width of a table. You can set the total width with the setTotalWidth() method. You can also define the absolute widths of the columns. However, these absolute widths are ignored in favor of the default width percentage as long as you don't "lock" the width. This is what happens if you use setLockedWidth(true) (Java) or table.LockedWidth = true (C#).
Based on your comment, I think the problem was caused by locking the width of the columns so that absolute values were used instead of relative values.

Apache POI autosizing a percent formatted cell

I'm having some issues with autosizing cells. After my program populates and formats each accordingly, it autosizes the cells to make it look visually acceptable. The issue is, when the cell is formatted as percentage, it auto-sizes the cell without taking in consideration the fact that there's a '%' character, so we end up getting #### on the cells until you expand the cell. Is there a way to autosize it WHILE taking in consideration that extra '%' character?
EDIT:
So this is what happens, the left VAR has been autosized correctly for whatever reasons, but the VAR on the right hasn't.
EDIT2:
I noticed that this ONLY happens when the cell value is 0.00% So all the values in the column for VAR that has #### were 0s and some of the values in the left column for VAR were non-zeros.
While this may not be an ideal solution, one possibility is to grab the size of the string you are inserting into the cell (ie. "50.00%" = size of 6), and set the cell width based manually based on that.
I'm afraid this is not supported in Apache POI, as it disregards the custom cell formatting when trying to calculate column width (it only takes font characteristics and rotation into consideration, but no DataFormat).
As a workaround you can try to write your custom autoSizeColumn method as a modification of the one currently implemented, i.e.:
public void autoSizeColumn(Sheet sheet, int column, int plusMinusChars) {
double width = SheetUtil.getColumnWidth(sheet, column, false);
if (width != -1) {
width += plusMinusChars;
width *= 256;
int maxColumnWidth = 255*256; // The maximum column width for an individual cell is 255 characters
if (width > maxColumnWidth) {
width = maxColumnWidth;
}
sheet.setColumnWidth(column, (int)(width));
}
}
Then you can call it with an extra '%' character to be printed out:
//4th column autosized + 1 character in width to accomodate for '%'
autoSizeColumn(sheet, 3, 1);

Using itext for printing at absolute positions

What is the recommended way of printing a text document as a pdf using absolute positioning ?
I am having a table that I have to print. I am also having the data type lengths and starting positions of the columns.
Since the existing table was a character based, there was no problem in its positioning. But even after using a monotype font (Courier, 10) I am not able to properly position the data and last column(s) of each row erroneously skip to the next line.
In order to present my data as close as the character one, I divided the page into different columns(based on its page size) and then add the contents at the desired place. I am adding chunks of data into the paragraph.
paragraph.add(new Chunk(new VerticalPositionMark(), columnNo*ptUnit, false));
I have tried to tweak the page size, font size and margin lengths, but the data is not properly displayed. Have you encountered any such problems ? please do share your thoughts.
Have you tried ColumnText
When i want to write a paragraph and I do know the amount of lines...I do a cycle incrementing (even it says incrementing and is minus is because the pdf is from "south" to "north" (0 - height) the y in a proportion of the fontsize, something like this
//_valueArray is my string[]
//fontSize is the value of the Size of the font...
//1.5 it's just a magic number :) that give me the space line that i need
//cbLocal is the PdfContentByte of the pdf
for (i = 0; i < _valueArray.Length; i++)
{
var p = new Phrase(_valueArray[i], font);
ColumnText.ShowTextAligned(cbLocal, align, p, x, y, 0);
if (i + 1 != _valueArray.Length)
{
y = y - (fontSize*1.5f);
}
}

Java JTable setting Column Width

I have a JTable in which I set the column size as follows:
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
table.getColumnModel().getColumn(0).setPreferredWidth(27);
table.getColumnModel().getColumn(1).setPreferredWidth(120);
table.getColumnModel().getColumn(2).setPreferredWidth(100);
table.getColumnModel().getColumn(3).setPreferredWidth(90);
table.getColumnModel().getColumn(4).setPreferredWidth(90);
table.getColumnModel().getColumn(6).setPreferredWidth(120);
table.getColumnModel().getColumn(7).setPreferredWidth(100);
table.getColumnModel().getColumn(8).setPreferredWidth(95);
table.getColumnModel().getColumn(9).setPreferredWidth(40);
table.getColumnModel().getColumn(10).setPreferredWidth(400);
This works fine, but when the table is maximized, I get empty space to the right of the last column. Is it possible to make the last column resize to the end of the window when resized?
I found AUTO_RESIZE_LAST_COLUMN property in docs but it does not work.
Edit: JTable is in a JScrollPane its prefered size is set.
What happens if you call setMinWidth(400) on the last column instead of setPreferredWidth(400)?
In the JavaDoc for JTable, read the docs for doLayout() very carefully. Here are some choice bits:
When the method is called as a result of the resizing of an enclosing window, the
resizingColumn is null. This means that resizing has taken place "outside" the JTable
and the change - or "delta" - should be distributed to all of the columns regardless of
this JTable's automatic resize mode.
This might be why AUTO_RESIZE_LAST_COLUMN didn't help you.
Note: When a JTable makes adjustments to the widths of the columns it respects their
minimum and maximum values absolutely.
This says that you might want to set Min == Max for all but the last columns, then set Min = Preferred on the last column and either not set Max or set a very large value for Max.
With JTable.AUTO_RESIZE_OFF, the table will not change the size of any of the columns for you, so it will take your preferred setting. If it is your goal to have the columns default to your preferred size, except to have the last column fill the rest of the pane, You have the option of using the JTable.AUTO_RESIZE_LAST_COLUMN autoResizeMode, but it might be most effective when used with TableColumn.setMaxWidth() instead of TableColumn.setPreferredWidth() for all but the last column.
Once you are satisfied that AUTO_RESIZE_LAST_COLUMN does in fact work, you can experiment with a combination of TableColumn.setMaxWidth() and TableColumn.setMinWidth()
JTable.AUTO_RESIZE_LAST_COLUMN is defined as "During all resize operations, apply adjustments to the last column only" which means you have to set the autoresizemode at the end of your code, otherwise setPreferredWidth() won't affect anything!
So in your case this would be the correct way:
table.getColumnModel().getColumn(0).setPreferredWidth(27);
table.getColumnModel().getColumn(1).setPreferredWidth(120);
table.getColumnModel().getColumn(2).setPreferredWidth(100);
table.getColumnModel().getColumn(3).setPreferredWidth(90);
table.getColumnModel().getColumn(4).setPreferredWidth(90);
table.getColumnModel().getColumn(6).setPreferredWidth(120);
table.getColumnModel().getColumn(7).setPreferredWidth(100);
table.getColumnModel().getColumn(8).setPreferredWidth(95);
table.getColumnModel().getColumn(9).setPreferredWidth(40);
table.getColumnModel().getColumn(10).setPreferredWidth(400);
table.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
Use this method
public static void setColumnWidths(JTable table, int... widths) {
TableColumnModel columnModel = table.getColumnModel();
for (int i = 0; i < widths.length; i++) {
if (i < columnModel.getColumnCount()) {
columnModel.getColumn(i).setMaxWidth(widths[i]);
}
else break;
}
}
Or extend the JTable class:
public class Table extends JTable {
public void setColumnWidths(int... widths) {
for (int i = 0; i < widths.length; i++) {
if (i < columnModel.getColumnCount()) {
columnModel.getColumn(i).setMaxWidth(widths[i]);
}
else break;
}
}
}
And then
table.setColumnWidths(30, 150, 100, 100);
Reading the remark of Kleopatra (her 2nd time she suggested to have a look at javax.swing.JXTable, and now I Am sorry I didn't have a look the first time :) )
I suggest you follow the link
I had this solution for the same problem: (but I suggest you follow the link above)
On resize the table, scale the table column widths to the current table total width.
to do this I use a global array of ints for the (relative) column widths):
private int[] columnWidths=null;
I use this function to set the table column widths:
public void setColumnWidths(int[] widths){
int nrCols=table.getModel().getColumnCount();
if(nrCols==0||widths==null){
return;
}
this.columnWidths=widths.clone();
//current width of the table:
int totalWidth=table.getWidth();
int totalWidthRequested=0;
int nrRequestedWidths=columnWidths.length;
int defaultWidth=(int)Math.floor((double)totalWidth/(double)nrCols);
for(int col=0;col<nrCols;col++){
int width = 0;
if(columnWidths.length>col){
width=columnWidths[col];
}
totalWidthRequested+=width;
}
//Note: for the not defined columns: use the defaultWidth
if(nrRequestedWidths<nrCols){
log.fine("Setting column widths: nr of columns do not match column widths requested");
totalWidthRequested+=((nrCols-nrRequestedWidths)*defaultWidth);
}
//calculate the scale for the column width
double factor=(double)totalWidth/(double)totalWidthRequested;
for(int col=0;col<nrCols;col++){
int width = defaultWidth;
if(columnWidths.length>col){
//scale the requested width to the current table width
width=(int)Math.floor(factor*(double)columnWidths[col]);
}
table.getColumnModel().getColumn(col).setPreferredWidth(width);
table.getColumnModel().getColumn(col).setWidth(width);
}
}
When setting the data I call:
setColumnWidths(this.columnWidths);
and on changing I call the ComponentListener set to the parent of the table (in my case the JScrollPane that is the container of my table):
public void componentResized(ComponentEvent componentEvent) {
this.setColumnWidths(this.columnWidths);
}
note that the JTable table is also global:
private JTable table;
And here I set the listener:
scrollPane=new JScrollPane(table);
scrollPane.addComponentListener(this);
This code is worked for me without setAutoResizeModes.
TableColumnModel columnModel = jTable1.getColumnModel();
columnModel.getColumn(1).setPreferredWidth(170);
columnModel.getColumn(1).setMaxWidth(170);
columnModel.getColumn(2).setPreferredWidth(150);
columnModel.getColumn(2).setMaxWidth(150);
columnModel.getColumn(3).setPreferredWidth(40);
columnModel.getColumn(3).setMaxWidth(40);
fireTableStructureChanged();
will default the resize behavior ! If this method is called somewhere in your code AFTER you did set the column resize properties all your settings will be reset. This side effect can happen indirectly. F.e. as a consequence of the linked data model being changed in a way this method is called, after properties are set.
No need for the option, just make the preferred width of the last column the maximum and it will take all the extra space.
table.getColumnModel().getColumn(0).setPreferredWidth(27);
table.getColumnModel().getColumn(1).setPreferredWidth(120);
table.getColumnModel().getColumn(2).setPreferredWidth(100);
table.getColumnModel().getColumn(3).setPreferredWidth(90);
table.getColumnModel().getColumn(4).setPreferredWidth(90);
table.getColumnModel().getColumn(6).setPreferredWidth(120);
table.getColumnModel().getColumn(7).setPreferredWidth(100);
table.getColumnModel().getColumn(8).setPreferredWidth(95);
table.getColumnModel().getColumn(9).setPreferredWidth(40);
table.getColumnModel().getColumn(10).setPreferredWidth(Integer.MAX_INT);
Use this code. It worked for me. I considered for 3 columns. Change the loop value for your code.
TableColumn column = null;
for (int i = 0; i < 3; i++) {
column = table.getColumnModel().getColumn(i);
if (i == 0)
column.setMaxWidth(10);
if (i == 2)
column.setMaxWidth(50);
}

Categories