2

I want to implement a component that serves as a list of options, that a user can choose to select or not.
Visually I thought that it would be best presented in a UI (if there is a better approach please tell me) as follows:
enter image description here

Anyway, I thought that this could be implemented via a JTable (single column) and using a JCheckBox as a cell editor.
I tried it but did not work.
Example of code:

public class ListRenderer extends JFrame {
    
    JCheckBox checkbox = new JCheckBox("Test");
    DefaultCellEditor dce1 = new DefaultCellEditor(checkbox);
    
    public ListRenderer(){          
        
        Object[][] data =  {   {"Test"} };
        String[] columnNames = {"Options"};
        
        DefaultTableModel model = new DefaultTableModel(data,columnNames);
        
        JTable table = new JTable(model){
            
            public TableCellEditor getCellEditor(int row, int column)            
            {               
                return dce1;                
            }
            
        };
        JScrollPane scrollPane = new JScrollPane( table );        
        getContentPane().add( scrollPane );
    }

What happens is that when the frame appears I see the "Test" in the table but it does not appear like a checkbox (as in the example image).
If I click on the cell, it turns into a checkbox (click button on the left and not right) but the text changes to show either true or false! It does not keep showing "Test"
More over the text depends on whether I keep pressing on the cell or not.
If I change the JCheckBox to a JComboBox the behavior is correct as far as I can tell.
Can anyone please tell me what am I doing wrong here?
Thanks!

Community
  • 1
  • 1
Cratylus
  • 52,998
  • 69
  • 209
  • 339
  • "_Can anyone please tell me what am I doing wrong here?_" - you didn't read snoracles swing tutorial. If you had, you would know the respective responsibilities of renderer/editor and which methods to use to plug-in custom renderers/editors. Hint: overriding JTable is *wrong* – kleopatra Jul 09 '11 at 10:21
  • @Kleopatra:Ok.Why is overiding wrong here? – Cratylus Jul 09 '11 at 12:29
  • for several reasons ... try to find at least two (hint: one is hinted to in the comment above "methods to use" :-) – kleopatra Jul 09 '11 at 12:57

2 Answers2

2

To be rendered as a JCheckBox by default, the table's model must return Boolean.class as the type for that column. If you are using DefaultTableModel, you will have to override getColumnClass() accordingly. Here's a related example.

Addendum: Note in the example that the editor's private instance of ValueRenderer can apply ItemEvent directly, instead of via setValueAt().

Addendum: The example has been updated to reflect the correct model-view workflow.

setValueAt() is called anyway. Verified it via debugging

If you step into setValueAt(), you'll see that "This empty implementation is provided so users don't have to implement this method if their data model is not editable."

trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • @Trashgod:One question for your example in the link.I do not understand how `ValueRenderer` and `ValueEditor` are related.I mean `ValueEditor` has its reference of `ValueRenderer` and both are set in `JTable` by `table.setDefaultRenderer(Value.class, new ValueRenderer());` `table.setDefaultEditor(Value.class, new ValueEditor());` – Cratylus Jul 09 '11 at 08:12
  • 2
    There's more details in [Concepts: Editors and Renderers](http://download.oracle.com/javase/tutorial/uiswing/components/table.html#editrender). – trashgod Jul 09 '11 at 08:16
  • @Trashgod:I understand the concepts.My question is related specifically to the example code.The `JTable` is added a new instance of `ValueRenderer` as an defaultrenderer and a a new instance of `ValueEditor` as a default editor and the code in `ValueEditor` has its own reference to a `ValueRenderer` unrelated to the one we set on the `JTable`.May be I am missunderstanding something here? – Cratylus Jul 09 '11 at 08:30
  • You have it right. The one in the editor is only used during editing. In that way, the `ItemEvent` can be applied directly, instead of via `setValueAt()`. – trashgod Jul 09 '11 at 08:38
  • @user384706 "_I understand the concepts_" - personally, I doubt it: your code definitely doesn't look like you do. – kleopatra Jul 09 '11 at 10:26
  • @Trashgod:`setValueAt()` is called anyway.Verified it via debugging – Cratylus Jul 09 '11 at 21:53
  • @Trashgod:I have stepped into `setValueAt` and it is not empty.It calls `getModel().setValueAt(aValue, convertRowIndexToModel(row), convertColumnIndexToModel(column));` which calls `Vector rowVector = (Vector)dataVector.elementAt(row); rowVector.setElementAt(aValue, column); fireTableCellUpdated(row, column);`.I am not sure how the example actually works. The `getElement` uses the `List` but the `setElement` uses the model's `dataVector`.Oops.I changed to extend `DefaultTableModel` instead of `AbstractTableModel`. Is this wrong? – Cratylus Jul 10 '11 at 07:57
  • @user384706 (DefaultTableModel) what's value returns (Object returns from JTable by defalut String value) model.getValueAt(row, col);, better would be update your post with code that shows what you are tried :-) – mKorbel Jul 10 '11 at 09:39
  • @mKorbel:The code is the same with the http://sites.google.com/site/drjohnbmatthews/table trashgod has refered to.Difference is that i extended `DefaultTableModel` instead of `AbstractTableModel` for the `DataModel` – Cratylus Jul 10 '11 at 09:47
  • @user384706 as your personal debuger, see my Edit – mKorbel Jul 10 '11 at 10:24
  • removed my upvote again - incorrect model implementation/usage (my bad I didn't spot it before, sorry, was focused on the fireEditingStopped ;-): a) all modifications _must_ happen through the setValueAt b) if the model contains objects which are mutable then those objects must implement some kind of change notification _and_ the model must listen to it and fire tableEvents as appropriate c) never-ever let the editor change the "live" value - its job is to present the value for change, notify when ready and keep the change handy - applying it to a model is the job of the editorListener – kleopatra Jul 10 '11 at 11:34
  • @kleopatra:Sorry you are too advanced for me :) Do you mean that the example in http://sites.google.com/site/drjohnbmatthews/table is also wrong? – Cratylus Jul 10 '11 at 13:37
  • @kleopatra is correct, the [example](http://sites.google.com/site/drjohnbmatthews/table) violates the separation of model and view, and it should be improved. This answer is otherwise correct, as you are using `DefaultTableModel`, which does the right thing. – trashgod Jul 10 '11 at 15:22
  • John, it's not only the model, it's also the mutation of the "real" value (of the current Value instance) by the editor (my fault again, should have been more clear;): that's an incorrect workflow. To see the effect, show the same model in another table with your custom renderer and see what happens if you edit it in the first. darn forum - can't add readable code in a comment .. and don't want to answer, because it _is_ a comment to your example :-) – kleopatra Jul 10 '11 at 16:36
  • @kleopatra: You are absolutely correct; I appreciate your diligence in this. I think I've updated the [example](https://sites.google.com/site/drjohnbmatthews/table) to reflect the correct flow—editor's listener updates model, model fires update event, table view is updated, editing stops. Corrections welcome. – trashgod Jul 10 '11 at 17:21
  • appreciate somebody listening :-) On we go: the "editorListener" I meant (a bit unclear, sorry) is the listener to the CellEditor, not the one listening to the editing component (which is a cellEditor internal affair). As a general rule, the editor must not mutate _any_ of the pararmeters passed into the getXXEditingComp - the example still does by updating the table's model. Not needed, btw, would the model fire in setValue as it should :-) Also, itemChanged is tricky as it can get notified too often (once iniatial setSelected, once by click) – kleopatra Jul 11 '11 at 09:55
  • @kleopatra: Thank you for your guidance; I think I see what you mean. I've updated the [example](https://sites.google.com/site/drjohnbmatthews/table) to reflect my understanding—the editor's component recieves an `ItemEvent` and invokes `fireEditingStopped()`, the table's implemnetation of `CellEditorListener` invokes `editingStopped()`, which feeds `getCellEditorValue()` to `setValueAt()`. – trashgod Jul 11 '11 at 15:53
  • nearly perfect - once you implement the model to fire on setValueAt :-) – kleopatra Jul 11 '11 at 16:36
  • hach .. you found that blog that never happened :-) – kleopatra Jul 12 '11 at 09:28
  • @kleopatra: Sorry if I was impertinent; I'll delete the comment containing the link if you like. – trashgod Jul 12 '11 at 09:41
  • @kleopatra let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/1353/discussion-between-trashgod-and-kleopatra) – trashgod Jul 12 '11 at 09:41
  • don't worry, you weren't :-) I have no problem with you publishing the link - simply forgot that I had the intention to blog some years ago. And astonished that this intention survived the migration of java.net when so much really useful stuff didn't. Hmm, can't load the chat ... will try again later (actually no time right now, sorry) – kleopatra Jul 12 '11 at 10:18
2

here you can find answer why (JButtons JComponents) JCheckBox and JRadioButton ... are little bit different for Rendering in the TableCell as another JComponents (and you have to play with JTable row Selection because I changed Opacity)

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

public class CheckboxInCellTest {

    private String[] columnNames = {"Boolean(Default)", "Boolean"};
    private Object[][] data = {{false, true}, {true, false}, {false, true}};
    private DefaultTableModel model = new DefaultTableModel(data, columnNames) {

        private static final long serialVersionUID = 1L;

        @Override
        public Class<?> getColumnClass(int column) {
            return getValueAt(0, column).getClass();
        }
    };

    public JComponent makeUI() {
        JTable table = new JTable(model);
        table.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseReleased(MouseEvent e) {
                JTable t = (JTable) e.getComponent();
                Point pt = e.getPoint();
                int row = t.rowAtPoint(pt), col = t.columnAtPoint(pt);
                if (t.convertColumnIndexToModel(col) == 1) {
                    t.getCellEditor(row, col).stopCellEditing();
                }
            }
        });
        table.setRowHeight(20);
        CheckBoxEditorRenderer1 cer = new CheckBoxEditorRenderer1();
        table.getColumnModel().getColumn(1).setCellRenderer(cer);
        table.getColumnModel().getColumn(1).setCellEditor(cer);
        JPanel p = new JPanel(new BorderLayout());
        p.add(new JScrollPane(table));
        return p;
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                createAndShowGUI();
            }
        });
    }

    public static void createAndShowGUI() {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        f.getContentPane().add(new CheckboxInCellTest().makeUI());
        f.setSize(320, 240);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }
}

class CheckBoxPanel1 extends JPanel {

    private static final long serialVersionUID = 1L;
    public final JCheckBox button = new JCheckBox();

    CheckBoxPanel1() {
        super(new GridBagLayout());
        add(button);
        button.setOpaque(false);
        setOpaque(false);
    }
}

class CheckBoxEditorRenderer1 extends AbstractCellEditor implements TableCellRenderer, TableCellEditor {

    private static final long serialVersionUID = 1L;
    private final CheckBoxPanel1 editor = new CheckBoxPanel1();
    private final CheckBoxPanel1 renderer = new CheckBoxPanel1();

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, boolean is, boolean hf, int row, int column) {
        renderer.button.setSelected(Boolean.TRUE.equals(value));
        renderer.button.setOpaque(is);
        return renderer;
    }

    @Override
    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
        editor.button.setSelected(Boolean.TRUE.equals(value));
        renderer.button.setOpaque(isSelected);
        return editor;
    }

    @Override
    public Object getCellEditorValue() {
        return editor.button.isSelected();
    }
}

and with JButton in the TableCell ... with Nimbus L&F (required Java > 1.6.17 ??? now I have xxx.25)

import com.sun.java.swing.Painter;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.util.LinkedList;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import sun.swing.table.DefaultTableCellHeaderRenderer;

public class TableButtonTest extends JFrame {

    private static final long serialVersionUID = 1L;

    public TableButtonTest() {
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JTable table = new JTable(new TestModel());
        table.getTableHeader().setReorderingAllowed(false);
        table.setRowHeight(20);
        table.getColumnModel().getColumn(1).setPreferredWidth(3);
        table.getColumnModel().getColumn(2).setPreferredWidth(3);
        table.getTableHeader().setDefaultRenderer(new WrappingRenderer(table.getTableHeader()));
        this.add(new JScrollPane(table));
        Action increase = new AbstractAction("+") {

            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                JTable table = (JTable) e.getSource();
                int row = Integer.valueOf(e.getActionCommand());
                TestModel model = (TestModel) table.getModel();
                model.increment(row, 0);
                //model.fireTableCellUpdated(row, 0);
            }
        };
        ButtonColumn inc = new ButtonColumn(table, increase, 1);
        Action decrease = new AbstractAction("-") {

            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                JTable table = (JTable) e.getSource();
                int row = Integer.valueOf(e.getActionCommand());
                TestModel model = (TestModel) table.getModel();
                model.decrement(row, 0);
            }
        };
        ButtonColumn dec = new ButtonColumn(table, decrease, 2);
        JTableHeader tbh = table.getTableHeader();
        tbh.setOpaque(false);
        Painter painter = new com.sun.java.swing.Painter() {

            public void paint(Graphics2D g, Object o, int w, int h) {
            }
        };
        javax.swing.UIDefaults defaults = new javax.swing.UIDefaults();
        defaults.put("TableHeader:\"TableHeader.renderer\"[Pressed].backgroundPainter", painter);
        defaults.put("TableHeader:\"TableHeader.renderer\"[Pressed].backgroundPainter",
                defaults.get(("TableHeader:\"TableHeader.renderer\"[Pressed].backgroundPainter")));
        tbh.putClientProperty("Nimbus.Overrides", defaults);
        tbh.putClientProperty("Nimbus.State", "Pressed");
        tbh.putClientProperty("JTree.lineStyle", "Angled");
        setLocation(200, 200);
        pack();
    }

    private static class WrappingRenderer implements TableCellRenderer {

        private DefaultTableCellHeaderRenderer delegate;
        private JTableHeader header;

        public WrappingRenderer(JTableHeader header) {
            this.header = header;
            this.delegate = (DefaultTableCellHeaderRenderer) header.getDefaultRenderer();
            header.setDefaultRenderer(this);
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            Component comp = delegate.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
            TableColumn draggedColumn = table.getTableHeader().getDraggedColumn();
            if (isSelected) {
                comp.setBackground(table.getSelectionBackground());
            } else {
                comp.setBackground(table.getBackground());
            }

            if (draggedColumn != null) {
                if (table.convertColumnIndexToModel(column) == draggedColumn.getModelIndex()) {
                    setNimbusState("Pressed");
                } else {
                    setNimbusState(null);
                }
            } else {
                setNimbusState(null);
            }
            // do similar for resizing column
            return comp;
        }

        public void setNimbusState(String state) {
            delegate.putClientProperty("Nimbus.State", state);
        }
    }

    public static void main(String[] args) {
        try {
            // UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
                if (info.getName().equals("Nimbus")) {
                    UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (Exception e1) {
            e1.printStackTrace();
        }
        new TableButtonTest().setVisible(true);
    }
}

class TestModel extends AbstractTableModel {

    private static final long serialVersionUID = 1L;
    private List<TestRecord> records = new LinkedList<TestRecord>();

    private static class TestRecord {

        private int val = 0;
    }

    public void increment(int row, int col) {
        records.get(row).val++;
        fireTableCellUpdated(row, col);
    }

    public void decrement(int row, int col) {
        records.get(row).val--;
        fireTableCellUpdated(row, col);
    }

    public TestModel() {
        records.add(new TestRecord());
        records.add(new TestRecord());
    }

    @Override
    public Class<?> getColumnClass(int col) {
        switch (col) {
            case 0:
                return Integer.class;
            case 1:
                return ButtonColumn.class;
            case 2:
                return Boolean.class;
            default:
                return String.class;
        }
    }

    @Override
    public boolean isCellEditable(int row, int col) {
        return true;
    }

    @Override
    public int getColumnCount() {
        return 3;
    }

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

    @Override
    public Object getValueAt(int row, int col) {
        if (col == 0) {
            return records.get(row).val;
        } else if (col == 1) {
            return "+";
        } else {
            return "-";
        }
    }
}
mKorbel
  • 109,525
  • 20
  • 134
  • 319
  • In your first example, why is the `MouseListener` added to `JTable`?Why is it needed in this case?It seems to work without out it ok – Cratylus Jul 09 '11 at 08:26
  • @user384706 for force that `t.getCellEditor(row, col).stopCellEditing();` check that with `JComboBox` too – mKorbel Jul 09 '11 at 08:33
  • really don't remove CellEditor#stopCellEditing() move that outside MouseListener that maybe wrong, maybe not, that your decision check this forum with stopCellEditing() or camickr's blog http://tips4java.wordpress.com/2008/12/12/table-stop-editing/, best of sugestions as I know and in runnable form – mKorbel Jul 09 '11 at 08:43
  • -1 for just too many mistakes (f.i. your editor implementation is invalid in not firing cancel/stop events) and too much completely unrelated code (f.i. the header looking pressed) I know you can do better ;-) – kleopatra Jul 09 '11 at 10:18
  • @kleopatra agreed thanks, you are welcome in my posts, that really best way how to learning and avoiding for non-Swing practices :-), just question it would be better listening for cancel/stop with FocusListener or put that into XxxTableModel ? – mKorbel Jul 09 '11 at 10:46
  • neither ... table can react as appropriate if the editor is well-behaved ;-) – kleopatra Jul 09 '11 at 11:00
  • then is time to overode >99% on Net, thanks for kick between eyes, I have to/I'll look at (collaborative) implementations for CellRenderer and CellEditor in SwingX, then I'll amended my post – mKorbel Jul 09 '11 at 14:10