1

I hope you can help me with a problem that has been bugging me for days now!

I've read a lot of SO answers and various examples from around the web and I've tried my best to read the DefaultTableModel source code to try and understand what's happening behind the scenes there but unfortunately I'm completely lacking the bigger picture still. I can't seem to find any examples, articles or videos that explain how or why a TableModel does what it does and thus how to effectively alter it to my needs.

I'm working on a project and I have need of displaying the data associated with each Object in a JTable. I have an ObjectCollection class which holds all the Objects in a folder inside an ArrayList. It is this collection that is passed to my custom TableModel to populate the JTable. The problem is however that I need the JTable to have a column (in this case column 0) containing checkboxes which the user can interact with. The state of the checkbox is not reflected in the Object or the ObjectCollection however. There is a separate method which is called once the user has made any selections which then searches the JTable for the checked rows and operates on the corresponding Object inside the ObjectCollection - thus neither the Objects themselves nor the collection need have any idea about the state of the checkbox. It's essentially as if the TableModel is holding data from two different sources - columns 1 to 4 from the Object in that row and the checked state of the cell in column 0 of that row. I hope that makes sense but if not hopefully my example code below will demonstarte it better than my words can.

The problem I'm having is twofold. Firstly, though I can get the checkboxes to display and I can register clicks on them I have no idea how to actually toggle the checked state of the checkbox (nothing happens when I click on the checkboxes). Secondly, assuming I can figure out how to toggle the checkbox state, I don't know how to use the getValue() method to return it, considering it isn't a member of the Object (again, see my example code below if I'm not being clear here).

I can't post the code (yet) from my real project for various reasons so I've created a fake example project for the purposes of this question. The principle is exactly the same and the problems I'm facing are too so if I can solve it here in this example I can solve it the same way in my real codebase. Anyway, here's the example code I've got so far...

A simple example Object

public class ExampleMovie {

    private final String title;
    private final String year;
    private final String director;
    private final double score;

    public ExampleMovie(String title, String year, String director, double score) {
        this.title = title;
        this.year = year;
        this.director = director;
        this.score = score;
    }

    public String getTitle() {
        return title;
    }

    public String getYear() {
        return year;
    }

    public String getDirector() {
        return director;
    }

    public double getScore() {
        return score;
    }
}

The ObjectCollection

public class ExampleMovieCollection {

    private List<ExampleMovie> allMovies;

    public ExampleMovieCollection() {
        allMovies = new ArrayList<>();
    }

    public void addMovie(ExampleMovie movie) {
        allMovies.add(movie);
    }

    public void removeMovie(ExampleMovie movie) {
        if (allMovies.contains(movie)) {
            allMovies.remove(movie);
        }
    }

    public List<ExampleMovie> getMovies() {
        return allMovies;
    }
}

The custom TableModel

public class ExampleMovieCollectionTableModel extends AbstractTableModel {

    private List<ExampleMovie> items;
    private String[] columnNames = {"Checked?", "Title", "Year", "IMDB Score", "Director"};

    public ExampleMovieCollectionTableModel(ExampleMovieCollection movieCollection) {
        this.items = new ArrayList<>(movieCollection.getMovies());
    }

    @Override
    public int getRowCount() {
        return  items.size();
    }

    @Override
    public int getColumnCount() {
        return columnNames.length;
    }

    @Override
    public String getColumnName(int columnIndex) {
        return columnNames[columnIndex];
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return columnIndex == 0; // Only the first column in the table should be editable
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        ExampleMovie item = items.get(rowIndex);
        switch (columnIndex) {
            case 0: // How do I return the checked state of the cell as it isn't a member of item?
            case 1: return item.getTitle();
            case 2: return item.getYear();
            case 3: return item.getScore();
            case 4: return item.getDirector();
            default: return null;
        }
    }

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        // How do I toggle the checked state of the cell when columnIndex == 0?
        System.out.println("(" + rowIndex + ", " + columnIndex + ") clicked. Value = " + aValue);
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
        if (columnIndex == 0) {
            return Boolean.class;
        }
        return String.class;
    }
}

The main() method and GUI creation code etc.

public class ExampleForm extends JFrame {

    private JPanel rootPanel;
    private JTable exampleTable;
    private JScrollPane tableScrollPane;
    private ExampleMovieCollection movieCollection;
    private ExampleMovieCollectionTableModel tableModel;

    public ExampleForm() {
        movieCollection = new ExampleMovieCollection();
        movieCollection.addMovie(new ExampleMovie("The Shawshank Redemption", "1994", "Frank Darabont", 9.3));
        movieCollection.addMovie(new ExampleMovie("The Godfather", "1972", "Francis Ford Coppola", 9.2));
        movieCollection.addMovie(new ExampleMovie("The Godfather: Part II", "1974","Francis Ford Coppola", 9.1));
        movieCollection.addMovie(new ExampleMovie("The Dark Knight", "2008", "Christopher Nolan", 9.0));
        movieCollection.addMovie(new ExampleMovie("Schindler's List", "1993", "Steven Spielberg", 8.9));
        createExampleTable();
    }

    private static void createAndShowGui() {
        JFrame frame = new JFrame("TableModel Example");
        frame.setContentPane(new ExampleForm().rootPanel);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setLocationRelativeTo(null); // Center on primary monitor
        frame.setVisible(true);
    }

    private void createExampleTable() {
        tableModel = new ExampleMovieCollectionTableModel(movieCollection);
        exampleTable = new JTable(tableModel);
        exampleTable.setPreferredScrollableViewportSize(exampleTable.getPreferredSize());
        exampleTable.changeSelection(0, 0, false, false);
        exampleTable.setAutoCreateRowSorter(true);
        tableScrollPane.setViewportView(exampleTable);
    }

    public static void main(String[] args) {
        createAndShowGui();
    }
}

The above works perfectly, except for not being able to set or retrieve the checkbox state in column 0. I can confirm the setValueAt() method is being called when I click on the checkboxes with the System.out.println statement but I have no idea how to actually toggle the checked state and unfortunately the documentation hasn't been any help and none of the answers/tutorials I've found cover this specific issue.

Hopefully I've provided enough information but if not please let me know and I'll edit the question accoringly.

Edit for clarity: In the example code it is trivial to add a boolean to the ExampleMovie class and have this value reflected in column 0 of the table, but unfortunately this isn't an option in the actual codebase I'm working with so I guess my specific question is: is there a way to get and set/toggle the checkbox value in column 0 without there being a corresponding data member in the ExampleMovie class? If that's not possible, or is incredibly non-trivial to achieve, can anyone point me toward an alternative method of displaying the data that might do the trick?

Second edit: Ignore me. I actually can relatively easily add a new data member to the class I'm working with. I've market Gilbert Le Blanc's answer as correct. I guess this was a duplicate question all along then. Sorry about that! Thanks for the help, I really appreciate it.

Thanks.

  • I don't want to close this as a duplicate unless I'm 100% sure but I think [this](https://stackoverflow.com/questions/4526779/multiple-row-selection-in-jtable/4528604#4528604) is what you want. If it is I or someone else will close it as such. – David Kroukamp Dec 11 '20 at 20:17
  • 2
    Also you extend the `JFrame` class which you shouldn't then you create a new instance of the `JFrame` class - `frame` and add your extended `JFrame`s root panel to the new `JFrame` just reading that is convoluted. Don't extend the `JFrame` class, create a `JPanel` instance with a `BorderLayout` and simply add your components to the `JPanel` and then add the `JPanel` to the `frame` instance. Also all swing components should be created on the [EDT](https://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html) – David Kroukamp Dec 11 '20 at 20:29
  • I may be failing to see something obvious because of my lack of understanding so if this is indeed a duplicate please accept my apologies. I've taken a look at that link though and I can't see where they're overriding setValueAt(). They're just calling the super class' implementation, aren't they? Again - I might be missing something so apologes if so. I've taken your suggestions re JPane etc on board so thank you for that. As for creating on the EDT - I thought anything in the scope of the ExampleForm class _is_ executed on the EDT? That's how IntelliJ sets things up by default anyway? – DoTheDonkeyKonga Dec 11 '20 at 21:17

1 Answers1

0

The code you provided abends. I changed the name of the main class from ExampleForm to ExampleMovieGUI.

Exception in thread "main" java.lang.NullPointerException
    at com.ggl.testing.ExampleMovieGUI.createExampleTable(ExampleMovieGUI.java:45)
    at com.ggl.testing.ExampleMovieGUI.<init>(ExampleMovieGUI.java:27)
    at com.ggl.testing.ExampleMovieGUI.createAndShowGui(ExampleMovieGUI.java:32)
    at com.ggl.testing.ExampleMovieGUI.main(ExampleMovieGUI.java:49)

After fixing all the abends, I added a boolean to the ExampleMovie class. Here's what the GUI looks like.

TableModel GUI

I made all the classes inner classes so I could post the complete runnable code as one block.

import java.awt.BorderLayout;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.AbstractTableModel;

public class ExampleMovieGUI {
    
    public static void main(String[] args) {
        ExampleMovieGUI gui = new ExampleMovieGUI();
        
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame("TableModel Example");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                
                frame.add(gui.createExampleTable(), BorderLayout.CENTER);
                
                frame.pack();
                frame.setLocationRelativeTo(null); // Center on primary monitor
                frame.setVisible(true);
            }
        });
    }

    private JTable exampleTable;
    private JScrollPane tableScrollPane;
    private ExampleMovieCollection movieCollection;
    private ExampleMovieCollectionTableModel tableModel;

    public ExampleMovieGUI() {
        movieCollection = new ExampleMovieCollection();
        movieCollection.addMovie(new ExampleMovie(
                "The Shawshank Redemption", "1994", "Frank Darabont", 9.3));
        movieCollection.addMovie(new ExampleMovie(
                "The Godfather", "1972", "Francis Ford Coppola", 9.2));
        movieCollection.addMovie(new ExampleMovie(
                "The Godfather: Part II", "1974","Francis Ford Coppola", 9.1));
        movieCollection.addMovie(new ExampleMovie(
                "The Dark Knight", "2008", "Christopher Nolan", 9.0));
        movieCollection.addMovie(new ExampleMovie(
                "Schindler's List", "1993", "Steven Spielberg", 8.9));
    }

    private JPanel createExampleTable() {
        JPanel panel = new JPanel(new BorderLayout());
        
        tableModel = new ExampleMovieCollectionTableModel(movieCollection);
        exampleTable = new JTable(tableModel);
        exampleTable.setPreferredScrollableViewportSize(
                exampleTable.getPreferredSize());
        exampleTable.changeSelection(0, 0, false, false);
        exampleTable.setAutoCreateRowSorter(true);
        tableScrollPane = new JScrollPane(exampleTable);
       
        panel.add(tableScrollPane, BorderLayout.CENTER);
        
        return panel;
    }
    
    public class ExampleMovieCollectionTableModel extends AbstractTableModel {

        private static final long serialVersionUID = 1L;
        
        private List<ExampleMovie> items;
        private String[] columnNames = {"Checked?", "Title", "Year", 
                "IMDB Score", "Director"};

        public ExampleMovieCollectionTableModel(
                ExampleMovieCollection movieCollection) {
            this.items = new ArrayList<>(movieCollection.getMovies());
        }

        @Override
        public int getRowCount() {
            return  items.size();
        }

        @Override
        public int getColumnCount() {
            return columnNames.length;
        }

        @Override
        public String getColumnName(int columnIndex) {
            return columnNames[columnIndex];
        }

        @Override
        public boolean isCellEditable(int rowIndex, int columnIndex) {
            // Only the first column in the table should be editable
            return columnIndex == 0;
        }

        @Override
        public Object getValueAt(int rowIndex, int columnIndex) {
            ExampleMovie item = items.get(rowIndex);
            switch (columnIndex) {
                case 0: return item.isSelected();
                case 1: return item.getTitle();
                case 2: return item.getYear();
                case 3: return item.getScore();
                case 4: return item.getDirector();
                default: return null;
            }
        }

        @Override
        public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
            // How do I toggle the checked state of the cell when columnIndex == 0?
            System.out.println("(" + rowIndex + ", " + columnIndex + 
                    ") clicked. Value = " + aValue);
            if (columnIndex == 0) {
                ExampleMovie item = items.get(rowIndex);
                item.setSelected(Boolean.valueOf(aValue.toString()));
            }
        }

        @Override
        public Class<?> getColumnClass(int columnIndex) {
            if (columnIndex == 0) {
                return Boolean.class;
            } else if (columnIndex == 3) {
                return Double.class;
            }
            return String.class;
        }
    }
    
    public class ExampleMovieCollection {

        private List<ExampleMovie> allMovies;

        public ExampleMovieCollection() {
            allMovies = new ArrayList<>();
        }

        public void addMovie(ExampleMovie movie) {
            allMovies.add(movie);
        }

        public void removeMovie(ExampleMovie movie) {
            if (allMovies.contains(movie)) {
                allMovies.remove(movie);
            }
        }

        public List<ExampleMovie> getMovies() {
            return allMovies;
        }
    }
    
    public class ExampleMovie {
        
        private boolean selected;

        private final String title;
        private final String year;
        private final String director;
        private final double score;

        public ExampleMovie(String title, String year, 
                String director, double score) {
            this.title = title;
            this.year = year;
            this.director = director;
            this.score = score;
            this.selected = false;
        }

        public String getTitle() {
            return title;
        }

        public String getYear() {
            return year;
        }

        public String getDirector() {
            return director;
        }

        public double getScore() {
            return score;
        }

        public boolean isSelected() {
            return selected;
        }

        public void setSelected(boolean selected) {
            this.selected = selected;
        }
        
    }
    
}
Gilbert Le Blanc
  • 50,182
  • 6
  • 67
  • 111
  • Thank you for your answer. I'm sorry - I should have been more explicit in my question. In the example I gave it's easy enough to add a boolean to the ExampleMovie class but in the real codebase I'm working with that's not an option unfortunately. That's the main problem I'm having. Is there way to get and set/toggle the boolean in column 0 _without_ it being a member of ExampleMovie? Sorry for not making my question clear. I'll try to edit it now to fix that. Also, what do you mean by the code 'abends'? – DoTheDonkeyKonga Dec 12 '20 at 00:16
  • @DoTheDonkeyKonga: You have to save the booleans somewhere. Even if you can't modify database tables, why can't you modify a Java class? – Gilbert Le Blanc Dec 12 '20 at 08:38
  • You are corect. I originally said that as I'm not the maintainer of the class I'm working with... buuuuuut I can of course just extend the class and add the required boolean. I think I was just blinkered with the way I was trying to implement what I wanted and as such couldn't see the obvious. My bad! Sorry to waste everyone's time. I've learned a lot re working with Java Swing from your answer however and maybe this question will help others in the future so all is not lost. Thank you for your input :) – DoTheDonkeyKonga Dec 12 '20 at 11:22
  • @DoTheDonkeyKonga: No problem. You don't even have to extend the class. You can create your own class with a boolean and an instance of the original class. I think that's called a decorator. I'm glad my example helped you. Good luck with your future Swing applications. – Gilbert Le Blanc Dec 12 '20 at 12:28