Functionality of Swing ListCellRenderer - java

A quick question about the example code in the JavaDoc for javax.swing.ListCellRenderer:
I'm a little surprised, that in the example, the ListCellRenderer is implemented by a class that extends JLabel and that the getListCellRendererComponent(...)-method simply returns this. It looks like there is only one instance of a JLabel around then, even for a list containing more than one element.
Usually, I would then expect that when the setText(...) method is called inside getListCellRendererComponent(...) for the second item in the list, it changes the label of the already existing first item in the list. Or, actually, it probably shouldn't even be possible for the list to use the same JLabel-instance twice (or more times), once for each item in the list.
Now, I've come up with two possible ways to resolve this and was wondering which one (if any) is actually happening:
Does JList somehow create new instances of the provided ListCellRenderer for each list item?
Or does it use the component returned by getListCellRendererComponent(...) only to invoke its paint(...) method on the list's canvas rather than actually adding this component to some panel?

When the JList renders itself it asks the ListModel for the elements it should display.
For each element it calls the javax.swing.ListCellRenderer to provide a render component. Then it paints the component. That's all. A render component is not bound to an element's state that it renders.
The javadoc of ListCellRenderer says:
Identifies components that can be used as "rubber stamps"
to paint the cells in a JList.
So your second assumption is right.
A look at javax.swing.plaf.BasicListUI shows it:
protected void paintCell(Graphics g, int row, Rectangle rowBounds,
ListCellRenderer cellRenderer, ListModel dataModel,
ListSelectionModel selModel, int leadIndex) {
Object value = dataModel.getElementAt(row);
boolean cellHasFocus = list.hasFocus() && (row == leadIndex);
boolean isSelected = selModel.isSelectedIndex(row);
Component rendererComponent = cellRenderer
.getListCellRendererComponent(list, value, row, isSelected,
cellHasFocus);
int cx = rowBounds.x;
int cy = rowBounds.y;
int cw = rowBounds.width;
int ch = rowBounds.height;
if (isFileList) {
// Shrink renderer to preferred size. This is mostly used on Windows
// where selection is only shown around the file name, instead of
// across the whole list cell.
int w = Math
.min(cw, rendererComponent.getPreferredSize().width + 4);
if (!isLeftToRight) {
cx += (cw - w);
}
cw = w;
}
rendererPane.paintComponent(g, rendererComponent, list, cx, cy, cw, ch,
true);
}

Related

How to change JTable color cells at several different locations?

I have a JTable object which displays the content of an Excel table. Once another Excel table is loaded, the differences have to be displayed (so some cells will change its background color, blue for example). This is the structure of my table.
And this is my code:
tblGSM.setDefaultRenderer(Object.class, new CustomTableRenderer(diffs));
CustomTableRenderer.java
public class CustomTableRenderer extends DefaultTableCellRenderer {
private Vector<Diff> diffs;
public PersoTableRenderer(Vector<Diff> diffs){
this.diffs = diffs;
}
#Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Component c = null;
for (int x = 0; x < diffs.size(); x++){
Diff d = diffs.elementAt(x);
c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus,
d.getRow(), d.getColumn());
c.setBackground(Color.BLUE);
}
return c;
}}
Diff.java
/*
A class to store the difference of corresponding cells
*/
public class Diff {
private int row, col;
public Diff(int row, int col){
this.row = row;
this.col = col;
}
public Diff(){
this(0,0);
}
public int getRow(){
return row;
}
public int getColumn(){
return col;
}
}
My question is diffs is populated correctly, yet the cells colors which are should be changed are not. Turns out all cells in column 1, 2, ,3, and 7 are changed. What it the solution, then?
From the documentation for DefaultTableCellRenderer (emphasis mine):
However JTable employs a unique mechanism for rendering its cells and therefore requires some slightly modified behavior from its cell renderer. The table class defines a single cell renderer and uses it as a as a rubber-stamp for rendering all cells in the table; it renders the first cell, changes the contents of that cell renderer, shifts the origin to the new location, re-draws it, and so on.
So as you can see, super.getTableCellRendererComponent() may return the same component for multiple cells, and thus your approach will not work.
Note that getTableCellRendererComponent is called once per cell as it is rendering, so in addition to the above caveat, your general approach of setting all renderer components in the table to blue when retrieving a single cell's component is not correct.
Instead you will want to only modify the background color of the component being requested, like (pseudo-code):
c = super.getTableCellRendererComponent(..., row, column)
// also don't forget to translate to model coords
model_row = table.convertRowIndexToModel(row)
model_column = table.convertColumnIndexToModel(column)
if diffs contains model_row,model_column:
c.setBackground(blue)
else
c.setBackground(table.getBackground()) // don't forget to reset
return c
Noting that you also have to reset the background color to its default if its not a "diff" cell, since as the docs state, the components are shared among multiple cells.
By the way, rather than storing the Vector<Diff> in your renderer, you really ought to be using a proper TableModel for this, and then querying the model for information. With a sanely implemented model this will also give you constant-time lookups of whether or not a cell should be blue, rather than having to search through the entire list of Diffs.
PS: Don't forget to translate your renderer/view coordinates to model coordinates when working with your Diffs, assuming they are in model coordinates. View coordinates may not agree with model coordinates if e.g. the table is sorted or the user has rearranged the columns. I've shown this in the above pseudo code.
Here is a complete example showing the use of a table model and per-cell custom backgrounds. You can sort the table and rearrange its columns.

Object in JComboBox in JTable is not associated with same object in combo list

my problem is that when I put Object to table in cell with CellEditor set to work as JComboBox and it's fine, but when click on the cell i got list with Objects, but selected one is not that which were in cell before, but just first on the list. Is there simple way to fix it?
public void setValueAt(Object value, int row, int col) {
data.get(row).values.set(col, (Device) value);
fireTableCellUpdated(row, col);
}
and
for(int i = 0; i < deviceTable.getModel().getColumnCount(); i++){
ExtendedAbstractTableModel model = (ExtendedAbstractTableModel) deviceTable.getModel();
JComboBox<Device> combo = new JComboBox<Device>();
for(Device value : model.columnsCombo.get(i)){
combo.addItem(value);
}
TableColumn columnModel = deviceTable.getColumnModel().getColumn(i);
columnModel.setCellEditor(new DefaultCellEditor(combo));
}
As shown in this example, DefaultCellEditor handles this for you. You're adding multiple instances in a loop; a single instance can handle the entire column. DefaultCellEditor works by overriding setValue() in the nested EditorDelegate. It's not clear how you've defeated this feature, but understanding the default may guide your search.
public void setValue(Object value) {
comboBox.setSelectedItem(value);
}
Finally I found what was wrong. I didn't override equal method in my class, and that is why these components couldnt recognise same item. Anyway, thank you all.

Any way to cause a refresh of selectedItemReminder in JComboBox

I have a JComboBox with a custom model which extends DefaultComboBoxModel.
When I want to add an item to my combo box I add it to the model and repaint the JComboBox. However, this is leaving the internal field:
selectedItemReminder
unchanged. What should I be doing instead.
I'm not sure I understand what it is you are trying to achieve, but I might be temptered to modify the method to read more like...
private void setChildren(Collection<BoundedArea> children) {
int oldSize = getSize();
// Notify the listeners that the all the values have begin removed
fireIntervalRemoved(this, 0, oldSize - 1);
this.children.clear();
for (BoundedArea boundedArea : children) {
if (boundedArea.getBoundedAreaType() == childType) {
this.children.add(boundedArea);
}
}
int size = getSize();
// Notify the listeners that a bunch of new values have begin added...
fireIntervalAdded(this, 0, size - 1);
setSelectedItem(null);
}
The other issue I can see is you seem to be thinking that the list is 1 based, it's not, it's 0 based, that is, the first element is 0
Updated based on changes to the question
From what I can understand, intervalAdded and contentsChanged of the JComboBox check to see if the selected value in the combo box model has changed, if it has, it calls selectedItemChanged which fires appropriate events to signal the change of the selected item...
I would, when you change the model, set the currently selected item value to something like null BEFORE you fire any event notifications...
So, using the previous example, I would do something more like...
private void setChildren(Collection<BoundedArea> children) {
setSelectedItem(null);
int oldSize = getSize();
// Notify the listeners that the all the values have begin removed
fireIntervalRemoved(this, 0, oldSize - 1);
this.children.clear();
for (BoundedArea boundedArea : children) {
if (boundedArea.getBoundedAreaType() == childType) {
this.children.add(boundedArea);
}
}
int size = getSize();
// Notify the listeners that a bunch of new values have begin added...
fireIntervalAdded(this, 0, size - 1);
}

Format (spread) text in DefaultListCellRenderer

I have a list of expences that looks like this :
My List
I would like the elements of the list to appear as if formatted with \t :
PRODUCT NAME PRICE DATE
PRODUCT NAME PRICE DATE
ect. I created DefaultListCellRenderer but I don't know how to implement this formatting.. If it's not doable, than at least how do I center the elements?
My DefaultListCellRenderer looks like this at the moment :
private class MyListRenderer extends DefaultListCellRenderer {
private static final long serialVersionUID = 1L;
public Component getListCellRendererComponent( JList<?> list,
Object value, int index, boolean isSelected,
boolean cellHasFocus )
{
Component c = super.getListCellRendererComponent( list, value, index,
isSelected, cellHasFocus );
Color czerwony = new Color(205, 16, 26);
setForeground(czerwony);
return(this);
}
}
Shoot, I'll make my comment an answer: use a JTable to display tabular data since it excels at this and was built for this. Otherwise if you want to create your own kludge of a Table via a JList, you'll be forced to use mono-spaced fonts and code that can easily break if one item of data exceeds the expected width of that column.
If you have restrictions on why you can't or are not allowed to use this, then please by all means share this with us.

Remove row being edited from JTable

I have a JTable and a button next to it that calls deleteSelectedRows(), which does exactly what it sounds like:
public void deleteSelectedRows() {
int[] selected = jTable.getSelectedRows();
for(int i = selected.length - 1; i >= 0; i--) {
model.removeRow(selected[i]);
}
if(model.getRowCount() < 1) {
addEmptyRow();
}
}
But if a cell was in the act of being edited when it (and/or cells above it) were deleted, the edited cell stayed while the rest left, like this:
And then trying to exit out of the editing threw an ArrayIndexOutOfBoundsException since row 5 was trying to be accessed and there was only one row left in the table.
I then tried all sorts of fun and games with jTable.getEditingRow(). At first, adding an if(selected[i] != editing) before the removal seemed to work, but then removing rows above the edited cell caused problems.
Then I tried this:
public void deleteSelectedRows() {
int[] selected = jTable.getSelectedRows();
int editing = jTable.getEditingRow();
for(int s : selected) { //PS: Is there a better way of doing a linear search?
if(s == editing) {
return;
}
}
for(int i = selected.length - 1; i >= 0; i--) {
model.removeRow(selected[i]);
}
if(model.getRowCount() < 1) {
addEmptyRow();
}
}
But that doesn't delete anything, ever. Judging from printlns I sprinkled around, the last cell to be highlighted (that has the special border seen here on spam) is considered part of the editing row, and thus triggers my early return.
So I don't really care whether the solution involves fixing the original problem--that of the wacky results when a cell being edited is deleted--or this new problem--that of getEditingRow() not behaving as I expected, it's just that I need at least one of those to happen. That said, I would be interested to hear both solutions just out of academic curiosity. Thanks in advance.
Try to include the following lines before removing any rows from your model:
if (table.isEditing()) {
table.getCellEditor().stopCellEditing();
}
As Howard stated, it is necessary to stop the cell editing before modifying the model. But it is also necessary to check if the cell is actually being modified to avoid null pointer exceptions.
This is because the getCellEditor() method will return null if the table isn't being edited at the moment:
if (myTable.isEditing()) // Only if it's is being edited
myTable.getCellEditor().stopCellEditing();
...
there are cases where the cell editor may refuse to stop editing,
that can happen i.e. if you are using some complex editor that is waiting for user input on a dialog. In that case you should add an extra check:
if (myTable.isEditing())
if (!myTable.getCellEditor().stopCellEditing()) {
// If your update is user-generated:
JOptionPane.showMessageDialog(null, "Please complete cell edition first.");
// Either way return without doing the update.
return;
}
In your code, you are trying to delete only the rows that are not being edited, but that would also throw an ArrayOutOfBounds Exception when the cell editor stops editing. The best is to stop it before the refresh.
Finally, there seems to be also a property you can set in your table:
table.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
as explained here.
Whilst stopping any and all cells from editing before applying any changes works, it's a bit like using a sledge hammer to crack a nut. What happens, for example, if the cell that is editing is not the one being deleted? This is the next problem you'll encounter. For that reason and others there is a better way.
Firstly, use the framework to do the heavy lifting for you. Attach a TableModelListener to your table model table.getModel().addTableModelListener()... then in your listeners implementation catch the delete event and process as follows:
/**
* Implements {#link TableModelListener}. This fine grain notification tells listeners
* the exact range of cells, rows, or columns that changed.
*
* #param e the event, containing the location of the changed model.
*/
#Override
public void tableChanged(TableModelEvent e) {
if (TableModelEvent.DELETE == e.getType()) {
// If the cell or cells beng edited are within the range of the cells that have
// been been changed, as declared in the table event, then editing must either
// be cancelled or stopped.
if (table.isEditing()) {
TableCellEditor editor = table.getDefaultEditor(ViewHolder.class);
if (editor != null) {
// the coordinate of the cell being edited.
int editingColumn = table.getEditingColumn();
int editingRow = table.getEditingRow();
// the inclusive coordinates of the cells that have changed.
int changedColumn = e.getColumn();
int firstRowChanged = e.getFirstRow();
int lastRowChanged = e.getLastRow();
// true, if the cell being edited is in the range of cells changed
boolean editingCellInRangeOfChangedCells =
(TableModelEvent.ALL_COLUMNS == changedColumn ||
changedColumn == editingColumn) &&
editingRow >= firstRowChanged &&
editingRow <= lastRowChanged;
if (editingCellInRangeOfChangedCells) {
editor.cancelCellEditing();
}
}
}
}
}
In the example above I've assigned my own editor as the default editor for the table table.setDefaultRenderer(ViewHolder.class, new Renderer()); table.setDefaultEditor(ViewHolder.class, new Editor());.
Additionally instead of using a specific view I use a ViewHolder. The reason for this is to make the table generic in terms of the views it displays. Here is the generic ViewHolder.class:
/**
* Holds the view in a table cell. It is used by both the {#link Renderer}
* and {#link Editor} as a generic wrapper for the view.
*/
public static abstract class ViewHolder {
private static final String TAG = "ViewHolder" + ": ";
// the position (index) of the model data in the model list
protected final int position;
// the model
protected Object model;
// the view to be rendered
protected final Component view;
// the views controller
protected final Object controller;
/**
* #param view the view to be rendered
* #param position the position (index) of the data
*/
public ViewHolder(int position,
Object model,
Component view,
Object controller) {
this.position = position;
if (view == null) {
throw new IllegalArgumentException("item view may not be null");
}
if (model == null) {
throw new IllegalArgumentException("model may not be null");
}
this.controller = controller;
this.model = model;
this.view = view;
}
Now, each time your renderer or editor is called, construct a ViewHolder class and pass in your view / controller / position etc, and you're done.
The important thing to note here is that you do not have to catch the delete or change event before it happens. You should, in fact, catch it after the model changes. Why? Well after a change you know what has changed, because the TableModelListener tells you, helping you determine as to what to do next.

Categories