1

I am creating my first JTable that requires me to create a custom AbstractTableModel, TableCellEditor, and DefaultTableCellRenderer. Given that I have not needed to create these before, I've made some significant progress in getting my table to behave as desired.

However, I am getting overwhelmed with all the different methods I am overriding, and am spinning my wheels trying to figure out how to modify the ImageIcon of a particular cell. The cell must contain a JLabel, as it needs both an ImageIcon as well as a text string. I can already set the initial ImageIcon (although I am probably doing it incorrectly), but I can't set an updated ImageIcon. Nothing fails, but no change is made.

In a general sense, what is the best way to get and set an icon to a JLabel cell of a JTable, assuming all of these models, editors, and renderers have already been instantiated?

My model has already been defined to return JLabel.class for these cells, if you're wondering, and I also do a fireTableCellUpdated(row, col) once the change has supposedly been made. If I do a System.out.println(getIcon()) before and after the update, I can even see the source has changed.

Here is some of the code (updated with URL/ImageIcon fix in place):

class MonitorTable extends JTable {
   MonitorTableModel model = new MonitorTableModel(rows, columnNames);
   setModel(model);
   ...
   public void setIconAt(ImageIcon icon, int row, int col) {
      model.setIconAt(icon, row, col);
   } // End setIconAt(ImageIcon, int, int)
   ...

   class MonitorTableModel extends AbstractTableModel {
      ...
      public void setIconAt(ImageIcon icon, int row, int col) {
         StatusTableCellRenderer cell =
            (StatusTableCellRenderer)getColumnModel().getColumn(col).getCellRenderer().
            getTableCellRendererComponent(myTableObject, null, false, false, row, col);

         System.out.println(cell.getIcon()); // Shows initial icon source
         cell.setIcon(icon);
         fireTableCellUpdated(row, col);     // Should update the table
         System.out.println(cell.getIcon()); // Shows new icon source
         System.out.println("Cell updated");
      } // End setIconAt(ImageIcon, int, int)
   } // End class MonitorTableModel

   public class StatusTableCellRenderer extends DefaultTableCellRenderer {
      public Component getTableCellRendererComponent(JTable table, Object value,
         boolean isSelected, boolean hasFocus, int row, int col) {

         setIcon(imgGray);
         setText((String)value);
         return this;
      } // End getTableCellRendererComponent(JTable, Object, boolean, boolean, int, int)
   } // End class StatusTableCellRenderer
} // End class MonitorTable
mKorbel
  • 109,525
  • 20
  • 134
  • 319
D.N.
  • 2,160
  • 18
  • 26

3 Answers3

3

My model has already been defined to return JLabel.class for these cells,

But according to the code in your renderer you expect a String value in these cells:

setText((String)value); 

I don't like your setIcon() method. I would not pass in the URL. I would pass in the Icon. Maybe you have a problem that the icon has not been read into memory at the time the cell is rendered.

what is the best way to get and set an icon to a JLabel cell of a JTable,

You should not store a JLable in the TableModel. It is expensive to store Swing components in the model, that is why Swing components use renderers. Instead you store a custom Object like "LabelInfo" which contains two properties, the text and the Icon. Then your custom renderer will extend the default renderer and invoke super.getTableCellRendererComponent(). You can then access your object and rest the text/icon properties of the renderer. You should not be creating objects in the renderer.

Now when you want to change something in the model you can do:

LabelInfo info = (LabelInfo)table.getValueAt(row, column);
info.setIcon(...);
table.setValueAt(info, row, column);

Thats all you need. There is not custom code to repaint the cell or anything because that is already built intothe setValueAt(...) method. of your table model.

Edit: a simple example for using a custom Object in the TableModel.

1) to add the object to the model you do something like:

LabelInfo info = new LabelInfo("some Text", yourIcon);
table.setValueAt(info, row, column);

2) the code for your custom renderer would be:

class LabelInfoRenderer extends DefaultTableCellRenderer
{
    @Override
    public Component getTableCellRendererComponent(
        JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
    {
        super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);

        LableInfo info = (LabelInfo)value;
        setIcon( info.getIcon() );

        return this;
    }
}
camickr
  • 321,443
  • 19
  • 166
  • 288
  • Your first point makes some sense, but it's working already. I can set and update the text of the `JLabel`, and set the initial icon. Maybe I'm just missing something there as well. Second, you are completely right about passing the icon instead of the URL, and I had planned on fixing that once I got this to work. Third, I am not trying to store anything in the table, I just want to have the two properties as you described. What would I extend, implement, or instantiate to have the LabelInfo object as you outline, which makes sense to me? – D.N. Jan 19 '11 at 21:29
  • The LabelInfo oject is just a simple class with two getters/setters for the text and icon properties. You can then add a toString() method to return the text so that the default renderer will display that value. Then in the custom renderer you just get the icon from this class and add it to the renderer. – camickr Jan 20 '11 at 02:05
  • That's an interesting concept, but a road of complexity I don't want to drive down. The `DefaultTableCellRenderer` has almost all the capabilities of my needs - the ability to set a `String` and an `ImageIcon`. The issue was when/how I set the icon. Once I moved the icon setting logic inside my extension of that class (instead of trying to call it from outside the class), it worked correctly. But thank you for your efforts, you challenged me to find the appropriate answer for my task! – D.N. Jan 20 '11 at 14:25
  • 1
    It is not complex. It is simple and straight forward and it is the way models and renderers are designed to be used. Somehow you need to store a separate icon for each row of the table. Logically this icon should be stored with the text for the same cell. This is proper OO design and is used in many places in Java, not just for tables and models and renferers. You did not find the appropriate answer for your task. You found a work around that will be hard to maintain and will not help you with other JTable or OO design issues in the future. – camickr Jan 20 '11 at 16:49
  • Why store a calculation when it can be done programmatically? The icon state is a calculation. To store both an explicit and derived value is counterproductive and wasteful. This happens all the time in database design. If absolute speed is your goal, it may be beneficial, but that is not my intent. – D.N. Jan 20 '11 at 18:56
  • That is fine but that is NOT what your posted code does, which I guess is why your question (and solution) is so confusing. Your original code had a setIconAt(...) method, which implies you are attempting to store the state of the icon in the model and not calculating the state when the cell is rendered. – camickr Jan 20 '11 at 21:51
  • You are correct in that my original code did not do that. That is why I gave such a hearty explanation in my answer as to what I did, complete with a code block showing how the value was set. You incorrectly assumed that my attempt to use a `setIconAt()` method was evidence that I was trying to store it in the model. All I stated is that I was attempting to "update" or "set" the icon. Perhaps my naming convention wasn't the best choice, but not everyone will follow correct convention all of the time. As you notice, my solution shows my evolution of understanding, and no longer includes it. – D.N. Jan 20 '11 at 21:59
0

Call the fireTableDataChanged from your model.

Try call the repaint method from JLabel too.

The better way to do it, is implement a CellRenderer, with returns a JPanel and make the draw method in the paintComponent. You can load a BufferedImage instead of a ImageIcon and use it to draw in the JPanel.

Try change you Renderer to:

public class StatusTableCellRenderer extends DefaultTableCellRenderer {
     public Component getTableCellRendererComponent(JTable table, Object value,
        boolean isSelected, boolean hasFocus, int row, int col) {
        JLabel comp = new JLabel(new ImageIcon(imgGray));
        comp.setText((String)value);
        return comp;
     } // End getTableCellRendererComponent(JTable, Object, boolean, boolean, int, int)
  }

You are doing some mess, this is not the right way to work with the TableModel, you should return the URL for the image in the getValueAt(int row, int col), after it, register the CellRenderer to correspond to cells with URL.class. The renderer is automatically called from the JTable, you don't need to extends JTable either, you have only to implement the Renderer and the Model. The setIconAt should only call the setValueAt and put your URL at the column, the renderer take care of the rest.

Marcos Vasconcelos
  • 18,136
  • 30
  • 106
  • 167
  • I am already firing `fireTableCellUpdated(row, col)`. I'll update my question with a bit more. – D.N. Jan 19 '11 at 20:17
  • I tried, no change. `fireTableCellUpdated(row, col)` should take care of that anyway. No wonder I never messed with JTables before! Argh! – D.N. Jan 19 '11 at 20:23
  • I already extend a `DefaultTableCellRenderer`, but it returns `this` instead of an instance of a `JLabel` or `JPanel`, as I've read it throws away the object after drawing for performance reasons. You're forcing me to actually post relevant code, damn you! :) – D.N. Jan 19 '11 at 20:32
  • I actually expected that to work, barring performance issues, but still no luck! To test your theory, I also had to change `MonitorTableModel.setIconAt()` to use `JLabel` instead of `StatusTableCellRenderer`, as they were incompatible. But that whole line seems messy, and may be the root issue... – D.N. Jan 19 '11 at 20:51
  • But I want the cell to have both an `ImageIcon` and text value, which is why I leave it as a `JLabel`. I just want to update the icon part of the cell, whereas the text is updated separately. I am extending the JTable for my own reasons (there's a lot more code I didn't post) – D.N. Jan 19 '11 at 21:00
  • So, you should do the JPanel solution, adding a JLabel with the image at left and text at right. But consider about working correctly, you TableModel should not be responsible to how render, the JTable does. – Marcos Vasconcelos Jan 19 '11 at 21:02
  • I'll have to chew on that one...the initial setup works, and sticking in a `JPanel` with a `JLabel` inside seems to defeat the purpose of a cell renderer in the first place. I just can't seem to update the image contents afterward - updating the text contents is not a problem. – D.N. Jan 19 '11 at 21:08
  • Last try, invalidate() then repaint() – Marcos Vasconcelos Jan 19 '11 at 22:37
  • No need, it was an issue with `getTableCellRendererComponent` (see my own answer). I don't have to do anything other than `fireTableCellUpdated(row, col)`, which makes me feel a little bit more "sane." Thanks again! – D.N. Jan 19 '11 at 22:45
0

I fixed this by changing setIcon(imgGray) to if (getIcon() == null) setIcon(imgGray);.

The issue is my getTableCellRendererComponent method was setting the icon to imgGray every time. Apparently my setIconAt method, which calls getTableCellRendererComponent, was being overridden, even though the "new" icon value was processed after the "old" value was (re)set.

I ended up removing all my setIcon methods and moved the relevant logic into my StatusTableCellRenderer class. That way I pass the value of the cell and let the renderer do the icon setting based on that value. It makes more sense this way, and works beautifully. I have confirmed that initial setting and all subsequent updates are performing as expected.

The logic of setting the icon is pretty simple -- set the predefined icon based on certain predefined threshold values.

double val;
if (getIcon() == null) setIcon(imgGray);       // Initialize
if ((value == null) || (value == "")) {
   val = 0;
} else {
   val = Double.parseDouble(value.toString());
} // End if

if (val <= THRESHOLD1) {
   setIcon(icon1);
} else if (val <= THRESHOLD2) {
   setIcon(icon2);
...
} // End if
setText(value.toString());

I was very concerned about suggestions to make brand new objects to use, when the default JLabel was exactly what I needed. It was both unnecessary and a potential performance hit to the JTable. Thank you all for your insight and assistance. This was driving me batty!

D.N.
  • 2,160
  • 18
  • 26
  • Your setIconAt(...) method should never invoke getTableCellRenderer(...). This is not a proper solution and that is why you are still having problems. The same icon is displayed for every cell because you always assign it to be the imgGray icons, which isn't even defined in the code you posted. If you want a different icon for every cell, then you need to store the Icon in the model. I gave you a simple solution earlier. – camickr Jan 20 '11 at 02:12