-6

My Java 8 applet connects to an SQL database and displays the result of a "select" statement in a JTable with two columns:

If the String in row 1/ColumnA isn't the same as the String in row 0/ColumnA, I want to give row 1 a grey background color (to mark the start of "new" data), the other rows should use the default white color.

Code for creating the table:

JTable myTable = new JTable();
myTable.setSelectionModel(new ToggleListSelectionModel()); //custom selection model
myTable.setModel(new DefaultTableModel(
    new Object[][] {
    },
    new String[] {
        "ColumnA", "ColumnB"
    }
));

Getting the data and filling the table:

Statement stat = connection.createStatement();
ResultSet rs = stat.executeQuery(someSelectStatement);
Object[] row = null;

while(rs.next()) {  
    int columns = rs.getMetaData().getColumnCount();    
    row = new Object[columns];

    for (int i=1; i<=columns; i++) {
        row[i - 1] = rs.getObject(i);
    }

    //TODO: Set color of row according to data in "ColumnA" of previous row
    ((DefaultTableModel) myTable.getModel()).insertRow(rs.getRow()-1,row); //Add new row with the ResultSet's content
    //Get String data of ColumnA for this specific row with e.g. "(String) row[0]"
}

rs.close();
stat.close();

From what I've found so far, I have to use a custom TableModel instead of the DefaultTableModel I set in the beginning but how do I use it? Everything I've found uses fixed checks, e.g.

if content in the cell is 'buy', set the background color to 'green'

(e.g. here or here) but in my case I don't know anything about the content when the table is created because it's filled/rows are added at runtime.

I also found this answer but the author of the question reads the data, then changes the model and only then fills the table, while I fill the table row by row (directly after reading the row's content).

My question: I know how to compare the content of a cell to content of the cell in the previous row but how do I set the background color of the row at runtime?

Edit:

Here's some MRE code for filling the table. Please note: If you post a suggestion about how to accomplish what I want to do, keep in mind that I'm working with a database and a ResultSet (see code above), not pre-defined data (see code below)!

JTable myTable = new JTable();
myTable.setSelectionModel(new ToggleListSelectionModel()); //custom selection model
myTable.setModel(new DefaultTableModel(
    new Object[][] {
        {"1000", 123},
        {"1000", 234}, 
        {"1001", 123},
        {"1002", 123},
        {"1002", 234},
        {"1002", 345},
        {"1003", 123},
        {"1003", 234}
    },
    new String[] {
        "ColumnA", "ColumnB"
    }
));

Result:

enter image description here

Desired result (grey background for every new "Column A" value):

enter image description here

Alternative result (alternate marking all rows of a group):

enter image description here

Neph
  • 1,823
  • 2
  • 31
  • 69
  • (1-) *keep in mind that I'm working with a database and a ResultSet (see code above), not pre-defined data (see code below)!* - where the data comes from is completely irrelevant. A renderer works on the data in the TableModel. – camickr Sep 19 '19 at 14:08
  • Setting the data directly changes how the table is set up in code. Not using the right code for the setup you're using means that you won't see the result because there will simply be no rows the background of which can be changed (if the code even compiles). So unless you want an empty table without data but with rows/columns that you set in Eclipse's WindowBuilder, to write working code it is important to know how the table is filled with data. – Neph Sep 19 '19 at 14:55
  • To test how a renderer works it is irrelevant how the data gets added to the TableModel of the JTable. The data can come from a database, a file, be hardcoded or you can write a method to add the data dynamically using the `addRow(...)` method to simulate adding data from a database. It just doesn't matter. All you need is a JTable with a TableModel that contains data and the custom renderer. It is that simple. – camickr Sep 19 '19 at 15:05
  • I don't want to test rendering, I want to get it to work and use it. On one hand you want full code (which includes filling the table with data) to test it yourself, on the other you don't want full code but would rather ignore where the data comes from. Pick one. – Neph Sep 19 '19 at 15:34
  • In case you care, you still have several issues with the apparent solution you are using: 1) Your table has String and Integer data, but you are rendering the Integers as Strings, Typically the Integers should be displayed right justified in the column. This is solved by overriding the `getColujmnClass(...)` method of the JTable or TableModel. But now you will have rendering problems because you only provide a custom renderer for the String data. 2) By default the columns of a JTable can be reorder by the user which will cause rendering problems. – camickr Sep 20 '19 at 04:04
  • 1) Tbh, I don't mind that everything's left-aligned. The column names hint at what type the content is (not that anyone who uses the app cares) and the arrays/objects I use to work with the data internally use parsed types anyway (I parse the data looping through it, see comment in my question). No rendering problems so far, even with a lot of rows but feel to edit your answer. 2) I disabled reordering of columns anyway (`mytable.getTableHeader().setReorderingAllowed(false)`) but even before there were no problems with it, so not sure what rendering problems you're talking about. – Neph Sep 24 '19 at 11:48
  • *I don't mind left-aligned.* - Industry standard is to display numbers right aligned. Look at the file explorer on your system, or how spreadsheets align numeric data. If you want left alignment then add the data as a String to reduce confusion. *disabled reordering* - yes that will solve the problem but remove flexibility for users. The problem is your highlighting is based on the data in column one. If you switch the order the highlighting changes. May not be a problem now, but learning proper techniques avoids problems in the future. – camickr Sep 24 '19 at 14:03
  • 1) As I said, it's easy to see the type of an entry looking at the column title - if the type even mattered (which it doesn't in the UI). If the alignment is important, feel free to add that info with a suggestion on how to change it to your answer. 2) The order of the columns is important and a lot of them don't make sense/the table is confusing if the order changes, so in the end it would do more harm than add flexibility. – Neph Sep 24 '19 at 14:49
  • I am trying to be helpful and provide background for how the rendering process works so you can use that knowledge on future applications. If you don't care about learning then tell me and I won't waste my time. The type matters. Maybe in the future you want to display a "check box" or an "image" or a "date". A JTable chooses the appropriate renderer based on the Class of data in each column. – camickr Sep 24 '19 at 15:25
  • *feel free to add that info with a suggestion* - my original answer in in the provided link and the answer provided here already shows a more complete solution to handle the issues mentioned. All you had to do was copy/paste/compile and test. That is why you post an [mre]. To make it easy for people to see exactly what happens when you test the code. – camickr Sep 24 '19 at 15:27
  • I see that you override `getColumnClass` but you don't set a specific renderer according to the class and also didn't write any comments about what's happening in your code. I don't want to display check boxes or images, only numbers and Strings but there's no guaranty that a single column will always have the same type of data (numbers and Strings are mixed), so checking for columns (not single cells) wouldn't work. – Neph Sep 26 '19 at 08:36
  • *also didn't write any comments about what's happening in your code.* - That was all explained in the original blog link I provided you. The expectation is that when someone helps, you take the time to read the blog and understand the suggestion/code provided.If you have questions you ask them. *you don't set a specific renderer according to the class* - that is the benefit of this approach. You write the rendering logic one. *numbers and Strings are mixed* - unusual for a JTable. But then you might use an approach like: https://stackoverflow.com/a/33218124/131872 – camickr Sep 26 '19 at 14:06

3 Answers3

2
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
    @Override
    public Component getTableCellRendererComponent(JTable table,
                                  Object value,
                                  boolean isSelected,
                                  boolean hasFocus,
                                  int row,
                                  int column) {
        Component comp = super.getTableCellRendererComponent(table,
                value, isSelected, hasFocus, row, column);
        if(!isSelected) { //Important check, see comment below!
            boolean levelBreak = row == 0;
            if (!levelBreak) {
                Object prior = table.getValueAt(row - 1, 0);
                Object current = table.getValueAt(row, 0);
                levelBreak = !prior.equals(current);
            }
            comp.setBackground(levelBreak ? Color.BLUE : Color.WHITE);
        }
        return comp;
    }
});

As the renderer / renderer component is reused for all table cells, the background must be set for all cases.

In general the JTable's TabelModel is better for getting a value instead of JTable's getValueAt, but evidently you neither sort the rows nor rearrange the columns.


For a possible older installed renderer

class MyCellRenderer extends DefaultTableCellRenderer {

    private final TableCellRenderer old;

    MyCellRenderer(TableCellRenderer old) {
        this.old = old;
    }

    @Override
    public Component getTableCellRendererComponent(JTable table,
                                  Object value,
                                  boolean isSelected,
                                  boolean hasFocus,
                                  int row,
                                  int column) {
        boolean levelBreak = row == 0;
        if (!levelBreak) {
            Object prior = table.getValueAt(row - 1, 0);
            Object current = table.getValueAt(row, 0);
            levelBreak = !prior.equals(current);
        }
        Component comp;
        if (old != null) {
            comp = old.getTableCellRendererComponent(table,
                value, isSelected, hasFocus, row, column);
        } else {
            comp = super.getTableCellRendererComponent(table,
                value, isSelected, hasFocus, row, column);
        }
        comp.setBackground(levelBreak ? Color.BLUE : Color.WHITE);
        return comp;
    }
}

table.setDefaultRenderer(Object.class, new MyCellRenderer(table.getDefaultRenderer(Object.class));
Neph
  • 1,823
  • 2
  • 31
  • 69
Joop Eggen
  • 107,315
  • 7
  • 83
  • 138
  • Thanks, this worked! You got 2 small mistakes though: `Color.TEAL` doesn't exist in "normal" Java and the comma (`Color.TEAL, Color.WHITE`) should be a colon. ;) `JTable's TabelModel is better for getting a value instead of JTable's getValueAt` - is it a performance thing or what's the difference? – Neph Sep 18 '19 at 15:28
  • I just noticed that this messes with the `SelectionModel`: I have it set to a custom "multi-select" (only full rows) but after adding your code, I can only select single cells, which also only adds a border instead of changing the color as usual. – Neph Sep 18 '19 at 15:35
  • There exist JTable.convertRowIndexToModel and ~ToView. I am since some time more concentrated on JavaFX/OpenJFX so the exact details I would have to read again. – Joop Eggen Sep 18 '19 at 15:35
  • Maybe there already is a custom cell renderer added. You could get the renderer and wrap it in your renderer. – Joop Eggen Sep 18 '19 at 15:38
  • Not that I'm aware of. I'm using [this](https://stackoverflow.com/a/39548102/2016165) "ToggleListSelectionModel", which lets you drag your mouse to select rows instead of having to click on all of them/pressing shift. Other than that I only set the width of the columns (via `setPreferredWidth`) and use `myTable.setDefaultEditor(Object.class, null)` to disable editing. – Neph Sep 18 '19 at 15:40
  • Sorry, there are parameters isSelected and hasFocus that might be used in setting the background color (for extended styling), A _model_ should not occupy itself with the _view_, so I do not know. Something done to the JTable. – Joop Eggen Sep 18 '19 at 15:51
  • Thanks for the other code, I edited your post to fix the class/function names. It seems to work the same way as the first code you posted, unfortunately the selection is broken with this one too. – Neph Sep 19 '19 at 13:14
  • I think I found the problem: You have to check if the current row is selected (`if(!table.isRowSelected(row))`) before you set the background color. Without this check the "this row is selected" background color is overwritten by the color set through your code. That's why clicking into a cell only added a border to this single cell in my app, instead of marking the row - it's simply the default behavior to mark a cell you click into with a border, additional to giving it the same "selected" background color the other cellls in the same row get. Is it okay with you if I add it to your code? – Neph Sep 19 '19 at 15:55
  • Sorry, I overlooked that one. Yes, `isSelected` works too. I only tested your first code with it but it'll most likely work with your second code block too. – Neph Sep 19 '19 at 16:13
  • 1
    I added the check to your code - only to your first one though because I didn't test it with the alternative you posted. – Neph Sep 24 '19 at 10:01
1

You need to create and use a custom TableCellRenderer

Basically, implement the one method in this class:

getTableCellRendererComponent(JTable table,
                                      Object value,
                                      boolean isSelected,
                                      boolean hasFocus,
                                      int row,
                                      int column)

I think the easiest way to do this is to create a new class that extends DefaultTableCellRendererComponent, like

class myRenderer extends DefaultTableCellRendererComponent {
    public Component getTableCellRendererComponent(JTable table,
                                          Object value,
                                          boolean isSelected,
                                          boolean hasFocus,
                                          int row,
                                          int column) {
      Component c = super.getTableCellRendererComponent(...);
    }
}

Component c is a JLabel. Just call setBackground (based on the row) and you should be set

ControlAltDel
  • 33,923
  • 10
  • 53
  • 80
  • I know that I can set the color of a `component` but how do I apply this to my rable, so I can set the color of each row directly after I added it? – Neph Sep 18 '19 at 14:22
0

I don't know anything about the content when the table is created because it's filled/rows are added at runtime.

You don't need to know anything about the data in advance. The rendering of a JTable is only done AFTER the data has been added to the TableModel.

The Table Row Rendering suggestion you linked to in your question is the approach you should be using. It shows how to do the rendering dynamically based on the data in the model.

Everything I've found uses fixed checks, e.g. "if content in the cell is 'buy', set the background color to 'green'"

Well you also have a fixed check:

  1. You want to get the data for the current row
  2. then get the data for the previous row.
  3. Then you compare both values and do the required highlighting.

All you are doing is comparing two values. Whether one value is hard coded is retrieve from the previous row makes no difference for the comparison.

From what I've found so far, I have to use a custom TableModel instead of the DefaultTableModel

There is no reason you can't use the DefaultTableModel. In fact the code from the Table Row Rendering uses the DefualtTableModel. The model used is irrelevant. All the model does is store the data. The rendering process then uses the data from the model.

So my suggestion is to:

  1. start with the working demo code from the Table Row Rendering link.
  2. change the hardcoded data to be similar to your real data.
  3. then add you custom rendering logic

Get the basics working first. Then worry about adding the data dynamically from the database. Simplify the problem and solve one step at a time.

Simple MRE using all the suggestions from above:

import java.awt.*;
import javax.swing.*;
import javax.swing.table.*;

public class SSCCE extends JPanel
{
    public SSCCE()
    {
        JTable myTable = new JTable()
        {
            @Override
            public Class getColumnClass(int column)
            {
                return getValueAt(0, column).getClass();
            }

            @Override
            public Component prepareRenderer(TableCellRenderer renderer, int row, int column)
            {
                Component c = super.prepareRenderer(renderer, row, column);

                if (!isRowSelected(row))
                {
                    if (row == 0)
                        c.setBackground( Color.BLUE );
                    else
                    {
                        Object previous = getModel().getValueAt(row - 1, 0);
                        Object current = getModel().getValueAt(row, 0);
                        c.setBackground( current.equals(previous) ? Color.WHITE : Color.BLUE );
                    }
                }

                return c;
            }
        };

        myTable.setModel(new DefaultTableModel(
            new Object[][] {
                {"1000", 123},
                {"1000", 234},
                {"1001", 123},
                {"1002", 123},
                {"1002", 234},
                {"1002", 345},
                {"1003", 123},
                {"1003", 234}
            },
            new String[] {
                "ColumnA", "ColumnB"
            }
        ));

        setLayout( new BorderLayout() );
        add(new JScrollPane(myTable));
    }

    private static void createAndShowGUI()
    {
        JFrame frame = new JFrame("SSCCE");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new SSCCE());
        frame.pack();
//        frame.setLocationByPlatform( true );
        frame.setVisible( true );
    }

    public static void main(String[] args) throws Exception
    {
        java.awt.EventQueue.invokeLater( () -> createAndShowGUI() );
/*
        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                createAndShowGUI();
            }
        });
*/
    }
}
camickr
  • 321,443
  • 19
  • 166
  • 288
  • This code doesn't work if there are empty `null` cells. Adding a null-check for `getColumnClass` (`return null` if the value is null) and one in `prepareRenderer` (`return null` if the renderer is null) of course results in the null cells not getting the blue background color. – Neph Oct 29 '19 at 15:08