0

I would like to highlight specific rows in a JTable whenever the contents of the a cell match with an input from the user. The following code is what I have that works thus far:

JTable table = new JTable(model) {
    public Component prepareRenderer(
            TableCellRenderer renderer, int row,
            int column) {
        Component c = super.prepareRenderer(renderer,
                row, column);
        if (!isRowSelected(row) ) {
            c.setBackground((hashMapcontainer
                    .containsKey(row)) ? Color.GREEN
                    : getBackground());
        }
        return c;
    }
    @Override
    public boolean isCellEditable(int row, int column) {
        return false;
    }
};

Notes: hashMapcontainer is a hashmap that is globally scoped within the source file.

Now this works to some extent however, I am adding this JTable to a JTabbedPane that is within a JFrame. JTables are dynamically created throughout the runtime of the program. However, the prepareRenderer method causes all the specific cells in all the created JTables to be highlighted.

How can I keep cells in all the JTables to keep their own specific highlighted cells rather than having all the JTables with the same exact highlighted cells in each?

Thanks in advance!

Sujay
  • 6,753
  • 2
  • 30
  • 49
David
  • 583
  • 1
  • 10
  • 27
  • sounds like the infamous DTCR color memory: http://stackoverflow.com/questions/9607670/how-do-i-correctly-use-custom-renderers-to-paint-specific-cells-in-a-jtable/9617446#9617446 – kleopatra Aug 09 '12 at 11:50

3 Answers3

4

The renderers are "rubber stamps". That basically means that they carry there previous settings over to the next cell.

What you need to do is provide a "default" behavior

if (!isRowSelected(row) ) {
    c.setBackground((hashMapcontainer
        .containsKey(row)) ? Color.GREEN
        : getBackground());
} else {

    // Define the default background color
    // Don't forget to take into the selection state

}

While I personally think that prepareRenderer in this case is probably a fair solution, you really should explore the possibly of providing a base line renderer. This is a lot of work to get right but has the advantage of been portable (if you change table implementations) as well as allowing other people the chance to define the highlight rules of a given cell, which you've basically just gone and overridden, IMHO.

I'd also suggest taking a look at JXTable as it has in built highlighting

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • If I wish to continue using this code, would the code snippet you've provided maintain the highlighted rows of the previous JTables while making the change to the new JTables of the JTabbedPane? – David Aug 08 '12 at 22:21
  • Well, given the fact that each table is it's own instances & the fact that you only change back to the "default" background when a row is not selected, I'd say it's a pretty good chance – MadProgrammer Aug 08 '12 at 22:47
  • Okay, the other problem I think you might be having is determining if the row actually belongs to the given table. Basically, from what I can determine, the global map is saying, in all the tables, highlight the same rows. Is this the functionality you are trying to achieve? – MadProgrammer Aug 08 '12 at 22:49
  • Yes, that is the problem I am having now. It highlights the same rows of the LAST JTable's highlighted rows. I just want that last table to make the changes while the previous JTables are kept the same. – David Aug 08 '12 at 23:14
  • I'd separate your selection model for each table so that they get there own list rws that should highlighted OR (& I don't like this idea), place a reference into you selection model of hich tables you want to highlight the specific rows – MadProgrammer Aug 08 '12 at 23:25
  • Model by the way is an instance of DefaultTableModel. I am not sure I follow what you're saying. If you don't mind, can you elaborate a bit further? – David Aug 09 '12 at 00:31
  • @David No, I'd use a separate model for your rows (as you mentioned the hashmap), but I would make one for each table. That way each table KNOWS exactly the rows it must highlight and not highlight the rows from everybody else. You could coordinate this via a centralised controller if you had to, but your the problem you're having is caused by this global model/hashmap and the inability for the individual rows to know which rows it should or shouldn't be highlighting – MadProgrammer Aug 09 '12 at 00:55
  • I do create a new DefaultTableModel for each JTable that is added to a globally scoped JTabbedPane. All of this is implemented within a button actionlistener. The hashmap is cleared of its values at the beginning of the actionlister body. I just can't put my finger on why all the JTables have the same rows highlighted. I apologize for not providing SSCCE. – David Aug 09 '12 at 01:31
  • Well, I'd suggest it's two things. First of all, when you change tabs, you need to update the table, usually a `TableChanged` event is the best choice for this, this should force the tables to update, so long as the "hashmap" you're using no longer contains any row information, hence the reason I suggest using a separate map for each table ;) – MadProgrammer Aug 09 '12 at 02:28
  • Is there an alternative way to "highlight" specific rows of a JTables that are dynamically added to a JTabbedPane at runtime? – David Aug 09 '12 at 13:41
  • You cold use a JXTable from the SwingX libraries, it has this feature built in, or you could, as I've been trying to suggest, allow each table to manage it's own highlight model – MadProgrammer Aug 09 '12 at 14:45
  • I will look into that thanks. I might not have the right Java environment for that. – David Aug 09 '12 at 16:57
3

Generally, overriding methods on the basic Swing classes is a bad idea. The recommended approach is to create a Jcomponent that implements TableCellRenderer and apply it to the table with setDefaultRenderer(). Note that by default JTable supplies 3 of these for Object, Number, and Boolean types. Typically, a renderer looks something like this:

public class MyRenderer extends JLable, implements TableCellRenderer{
    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, 
         boolean isSelected, boolean hasFocus, int row, int column) {
    // Set up default state here 
    c.setBackground(Color.white);
    c.setForeground(Color.black);
    // ...
    if (!isRowSelected(row) ) {
        c.setBackground((hashMapcontainer
                .containsKey(row)) ? Color.GREEN
                : getBackground());
    }
    return c;
}

This gives you a reusable component, rather than needing to extend JTable every place you create it. As for the same cells selecting in all tables, that is due to isRowSelected and hashMapContainer accessing global state instead of per-instance state. All JComponents have getClientProperty and putClientProperty. These allow you to attach your own state object to a JTable. Then your isRowSelected becomes isRowSelected(table, row), which simply calls:

MyObject myObj = (MyObject)table.getClientProperty("MySelectionProperty");
myObj.isRowSelected(row);

Like wise the hashMapContainer can also be retrieved from the table:

MyHashContainer myHash = (MyHash)table.getClientProperty("MyHashContainer");

Update:

This is pretty much the same for a dynamically generated table. Table creation will look something like this:

JTable t = new JTable();
// other typical table setup, t.setModel(...); etc
t.setDefaultRenderer(String.class, myRenderer);
t.putClientProperty("MySelectionProperty", new MyObject());
t.putClientProperty("MyHashContainer", new MyHashContainer());

It's worth noting that as long as the renderer carries no state there is no need to create an instance per table. I'll usually create one and use it for all my tables.

Here's an update to the renderer above that does not use global state, rather looks to the table for properties:

public class MyRenderer extends JLable, implements TableCellRenderer{
    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, 
        boolean isSelected, boolean hasFocus, int row, int column) {

        // Pull hashMapContainer from the per-table client properties
        MyHashContainer hashMapcontainer = (MyHashContainer)table.getClientProperty("MyHashContainer");

        // Set defaults as above            

        if (!isRowSelected(table, row) ) {
            // Same as above
        }
        return c;
    }
    // Private method to check for row selection
    private boolean isRowSelected(JTable t, int row) {
        int[] selectedRows = table.getSelectedRows();
        for (int i = 0; i < selectedRows.length; i++) {
            if (selectedRows[i] == row) {
                return true;
            }
         }
         return false;
    }
}
Devon_C_Miller
  • 16,248
  • 3
  • 45
  • 71
0

I wanted my JTable to display updated rows using a highlight colour, to make it easy to see which rows are being updated. This was how I got it done. The solution is implemented as a wrapper around the conventional TableCellRenderer. Use it like:

RowHighlighter highlighter = new RowHighlighter(table);
table.setDefaultRenderer(Date.class, highlighter.wrap(new DefaultTableCellRenderer()));

STEPS and PERIOD allow configuration (should be a parameter, I guess) of how the highlight fades to the background. In this example, it will fade through 50 steps, over 2 seconds, to the cell's background colour. This also preserves the background colour of the real renderer, including if the row is currently selected.

import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.swing.JTable;
import javax.swing.Timer;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableCellRenderer;

class RowHighlighter implements TableModelListener, ActionListener {
    
    private static int STEPS = 50;
    private static int PERIOD = 2000;
    
    private Map<Integer, Integer> rowHighlights = new HashMap<>();
    private JTable table;
    
    public RowHighlighter(JTable table) {
        this.table = table;
        table.getModel().addTableModelListener(this);
        Timer timer = new Timer(PERIOD/STEPS, this);
        timer.start();
    }
    
    @Override
    public void tableChanged(TableModelEvent e) {
        int first = e.getFirstRow();
        int last = e.getLastRow();
        for (int i=first; i<=last; i++) {
            rowHighlights.put(i, STEPS);
        }
    }
    
    @Override
    public void actionPerformed(ActionEvent e) {
        if (rowHighlights.size() == 0)
            return;
        Iterator<Map.Entry<Integer, Integer>> it = rowHighlights.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<Integer, Integer> entry = it.next();
            int v = entry.getValue();
            if (v > 1) {
                entry.setValue(v - 1);
            } else {
                it.remove();
            }
        }
        table.repaint();
    }
    
    public TableCellRenderer wrap(TableCellRenderer delegate) {
        return new HighlightingTableCellRenderer(rowHighlights, delegate);
    }
    
    private static class HighlightingTableCellRenderer implements TableCellRenderer {

        private Map<Integer, Integer> rowMap;
        private TableCellRenderer delegate;
        
        public HighlightingTableCellRenderer(Map<Integer, Integer> rowMap, TableCellRenderer delegate) {
            this.rowMap = rowMap;
            this.delegate = delegate;
        }
        
        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
                int row, int column) {
            Component c = delegate.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
            Integer v = rowMap.get(row);
            if (v != null) {
                Color background = c.getBackground();
                Color highlight = getColor(v, Color.PINK, background);
                c.setBackground(highlight);
            }
            return c;
        }
        
        private Color getColor(int n, Color from, Color to) {
            float p = ((float)n)/STEPS;
            
            return new Color((int)(from.getRed() * p + to.getRed() * (1-p)),
                    (int)(from.getGreen() * p + to.getGreen() * (1-p)),
                    (int)(from.getBlue() * p + to.getBlue() * (1-p)));
        }
        
    }

}
Rogan Dawes
  • 350
  • 2
  • 9