How to identify a direct click on a JCheckBox in a JTable?
Asked Answered
A

3

6

With a JCheckBox as an Editor in a JTable column, I would like to ignore mouseclicks in the space left and right of a CheckBox in a TableCell.

I have found a discussion from 2011 on the Oracle forum, but the problem was not solved there: https://community.oracle.com/thread/2183210

This is the hack I've realized so far, the interesting part begins atclass CheckBoxEditor:

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.util.EventObject;
import javax.swing.DefaultCellEditor;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumn;

/**
 * Trying to set the Checkbox only if clicked directly on the box of the CheckBox. And ignore clicks on the
 * remaining space of the TableCell.
 * 
 * @author bobndrew
 */
public class JustCheckOnCheckboxTable extends JPanel
{
  private static final int        CHECK_COL = 1;
  private static final Object[][] DATA      = { { "One", Boolean.TRUE }, { "Two", Boolean.FALSE },
      { "Three", Boolean.TRUE }, { "Four", Boolean.FALSE }, { "Five", Boolean.TRUE },
      { "Six", Boolean.FALSE }, { "Seven", Boolean.TRUE }, { "Eight", Boolean.FALSE },
      { "Nine", Boolean.TRUE }, { "Ten", Boolean.FALSE } };
  private static final String[]   COLUMNS   = { "Number", "CheckBox" };
  private final DataModel         dataModel = new DataModel( DATA, COLUMNS );
  private final JTable            table     = new JTable( dataModel );

  public JustCheckOnCheckboxTable()
  {
    super( new BorderLayout() );
    this.add( new JScrollPane( table ) );
    table.setRowHeight( table.getRowHeight() * 2 );
    table.setPreferredScrollableViewportSize( new Dimension( 250, 400 ) );
    TableColumn checkboxColumn = table.getColumnModel().getColumn( 1 );
    checkboxColumn.setCellEditor( new CheckBoxEditor() );
  }

  private class DataModel extends DefaultTableModel
  {
    public DataModel( Object[][] data, Object[] columnNames )
    {
      super( data, columnNames );
    }

    @Override
    public Class<?> getColumnClass( int columnIndex )
    {
      if ( columnIndex == 1 )
      {
        return getValueAt( 0, CHECK_COL ).getClass();
      }
      return super.getColumnClass( columnIndex );
    }
  }


  class CheckBoxEditor extends DefaultCellEditor
  {
    private final JCheckBox checkBox;

    public CheckBoxEditor()
    {
      super( new JCheckBox() );
      checkBox = (JCheckBox) getComponent();
      checkBox.setHorizontalAlignment( JCheckBox.CENTER );
      System.out.println( "the checkbox has no size:   " + checkBox.getSize() );
    }

    @Override
    public boolean shouldSelectCell( final EventObject anEvent )
    {
      System.out.println( "\nthe checkbox fills the TableCell:  " + checkBox.getSize() );
      //Throws NullPointerException:      System.out.println( checkBox.getIcon().getIconWidth() );
      System.out.println( "always JTable :-(   " + anEvent.getSource() );

      MouseEvent ev =
          SwingUtilities.convertMouseEvent( ((ComponentEvent) anEvent).getComponent(), (MouseEvent) anEvent,
          getComponent() );
      System.out.println( "Position clicked in TableCell:   " + ev.getPoint() );
      System.out.println( "always JCheckBox :-(   " + getComponent().getComponentAt( ev.getPoint() ) );

      Point middleOfTableCell = new Point( checkBox.getWidth() / 2, checkBox.getHeight() / 2 );
      System.out.println( "middleOfTableCell: " + middleOfTableCell );

      Dimension preferredSizeOfCheckBox = checkBox.getPreferredSize();

      int halfWidthOfClickArea = (int) (preferredSizeOfCheckBox.getWidth() / 2);
      int halfHeightOfClickArea = (int) (preferredSizeOfCheckBox.getHeight() / 2);

      if ( (middleOfTableCell.getX() - halfWidthOfClickArea > ev.getX() || middleOfTableCell.getX() + halfWidthOfClickArea < ev.getX()) 
        || (middleOfTableCell.getY() - halfHeightOfClickArea > ev.getY() || middleOfTableCell.getY() + halfHeightOfClickArea < ev.getY()) )
      {
        stopCellEditing();
      }

      return super.shouldSelectCell( anEvent );
    }
  }


  private static void createAndShowUI()
  {
    JFrame frame = new JFrame( "Direct click on CheckBox" );
    frame.add( new JustCheckOnCheckboxTable() );
    frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
    frame.pack();
    frame.setLocationRelativeTo( null );
    frame.setVisible( true );
  }

  public static void main( String[] args )
  {
    java.awt.EventQueue.invokeLater( new Runnable()
    {
      @Override
      public void run()
      {
        createAndShowUI();
      }
    } );
  }
}

What I like about this solution:

  • all TableCell behaviour is correct: selecting, MouseOver, EditModes, ...

What I don't like about it:

  • the hardcoded size of the JCheckBox (int halfWidthOfClickArea)
    • where can I get the dimensions of an unpainted component?

Or are there better ways to achieve this Table and CheckBox-behaviour?

EDIT:

I changed the sourcecode following the advice of camickr and added a vertical hitzone for tables with higher RowHeights.
But so far I forgot to mention the main reason for my question... ;-)
I'm calling stopCellEditing() in the method shouldSelectCell(..).

Is it ok to decide there about more than the Cell-Selection?

Atmolysis answered 16/12, 2014 at 17:0 Comment(4)
whats happens if you use getComponent().getC... from XxxTabeleCellEditor and to change setClickCountToStart for Editor tooMissymist
@Missymist If you mean getComponent().getComponentAt(ev.getPoint()), it returns the JCheckBox because it's filling the whole TableCell. Regarding your advice to change "setClickCountToStart for Editor", I don't know how it could help me!?Atmolysis
I don't want to be rude or anything, but did my answer work for you? If it didn't, could you leave a comment why? (If you just want to wait and see if there are any other answers, that's fine, just say so :) ) Just wondering if I can do anything to make my response better.Sideline
no problem for asking me to hurry up. please see my comment under your answer.Atmolysis
O
4

where can I get the dimensions of an unpainted component?

  System.out.println(checkBox.getPreferredSize() );
Oscaroscillate answered 16/12, 2014 at 17:59 Comment(1)
Thank you for your advice; it works! Please see my updated question for a more precise question.Atmolysis
S
3

I think that stopping the selection of the actual in the shouldSelectCell() method is kind of a roundabout method of doing this, and converting mouse events seems weird.

Instead, a much cleaner approach would be to make the checkbox not fill the entire cell, so that it only gets selected if you press directly on the "checkbox" part of it.

This behavior can be accomplished by putting your JCheckbox inside a JPanel and center it without stretching it. To do this, we can make the JPanel's layout manager a GridBagLayout. See how when using GridBagLayout, the inner content doesn't stretch:

Layout managers (from this StackOverflow answer)

So now, if you click in the empty space surrounding it, you will be clicking on a JPanel, so you won't be changing the JCheckBox's value.

The code for your CheckBoxEditor turns out like this in the end:

class CheckBoxEditor extends AbstractCellEditor implements TableCellEditor {
    private static final long serialVersionUID = 1L;
    private final JPanel componentPanel;
    private final JCheckBox checkBox;

    public CheckBoxEditor() {
        componentPanel = new JPanel(new GridBagLayout()); // Use GridBagLayout to center the checkbox
        componentPanel.setOpaque(false);
        checkBox = new JCheckBox();
        checkBox.setOpaque(false);
        componentPanel.add(checkBox);
    }

    @Override
    public Object getCellEditorValue() {
        return Boolean.valueOf(checkBox.isSelected());
    }

    @Override
    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
        if (value instanceof Boolean) {
            checkBox.setSelected(((Boolean) value).booleanValue());
        } else if (value instanceof String) {
            checkBox.setSelected(value.equals("true"));
        }
        if (isSelected) {
            setForeground(table.getSelectionForeground());
            setBackground(table.getSelectionBackground());
        } else {
            setForeground(table.getForeground());
            setBackground(table.getBackground());
        }
        return componentPanel;
    }
}

(Note that you can't be extending from a DefaultCellEditor anymore - in the code above, you're now having to extend from an AbstractCellEditor and implement a TableCellEditor).

I think that this version of your CheckBoxEditor does what you want - if you click in the empty space around the check box, nothing happens. The check box only becomes check if you click directly on it.

Also, by using a JPanel, you don't have to do any MouseEvent calculations and (to me, at least), the code looks much cleaner and it's easier to see what's going on.


EDIT1:
I read your comment and I found a solution: Why not leave the editor as it is, but then just make a cell renderer that derives from the DefaultTableCellRenderer? Then, in your CheckBoxEditor use the same borders and backgrounds as the renderer.

This should achieve the effect you want (I've moved common code into outer class methods so I don't have to repeat them):

private static void setCheckboxValue(JCheckBox checkBox, Object value) {
    if (value instanceof Boolean) {
        checkBox.setSelected(((Boolean) value).booleanValue());
    } else if (value instanceof String) {
        checkBox.setSelected(value.equals("true"));
    }
}

private static void copyAppearanceFrom(JPanel to, Component from) {
    if (from != null) {
        to.setOpaque(true);
        to.setBackground(from.getBackground());
        if (from instanceof JComponent) {
            to.setBorder(((JComponent) from).getBorder());
        }
    } else {
        to.setOpaque(false);
    }
}

class CheckBoxEditor extends AbstractCellEditor implements TableCellEditor {
    private static final long serialVersionUID = 1L;
    private final JPanel componentPanel;
    private final JCheckBox checkBox;

    public CheckBoxEditor() {
        componentPanel = new JPanel(new GridBagLayout());  // Use GridBagLayout to center the checkbox
        checkBox = new JCheckBox();
        checkBox.setOpaque(false);
        componentPanel.add(checkBox);
    }

    @Override
    public Object getCellEditorValue() {
        return Boolean.valueOf(checkBox.isSelected());
    }

    @Override
    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
        setCheckboxValue(checkBox, value);
        TableCellRenderer renderer = table.getCellRenderer(row, column);
        Component c = renderer.getTableCellRendererComponent(table, value, true, true, row, column);
        copyAppearanceFrom(componentPanel, c);
        return componentPanel;
    }
}

class CheckBoxRenderer extends DefaultTableCellRenderer {
    private static final long serialVersionUID = 1L;
    private final JPanel componentPanel;
    private final JCheckBox checkBox;

    public CheckBoxRenderer() {
        componentPanel = new JPanel(new GridBagLayout());  // Use GridBagLayout to center the checkbox
        checkBox = new JCheckBox();
        checkBox.setOpaque(false);
        componentPanel.add(checkBox);
    }

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
        super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
        setCheckboxValue(checkBox, value);
        copyAppearanceFrom(componentPanel, this);
        return componentPanel;
    }
}

Then, you have to set the renderer along with the editor in your constructor:

checkboxColumn.setCellEditor(new CheckBoxEditor());
checkboxColumn.setCellRenderer(new CheckBoxRenderer());

Here's a couple of screenshots comparing the two methods:

Your original method:                                 JPanel and JCheckBox method:
Old   New

I can barely see a difference :)

IMHO, I still think that just using the plain Java table API's is cleaner than calculating checks based on mouse pointer positions, but the choice is up to you.

I hope this helped!


EDIT2:
Also, if you want to be able to toggle using the spacebar I think that you can just add a key binding to the componentPanel in the CheckBoxEditor constructor:

class CheckBoxEditor extends AbstractCellEditor implements TableCellEditor {
    // ...

    public CheckBoxEditor() {
        // ...
        componentPanel.getInputMap().put(KeyStroke.getKeyStroke("SPACE"), "spacePressed");
        componentPanel.getActionMap().put("spacePressed", new AbstractAction() {
            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                checkBox.setSelected(!checkBox.isSelected());
            }
        });
        // ...
    }

    // ...
}

I'm not sure if you can use Drag-and-Drop with boolean values. I tried dragging "true" onto the checkboxes in the original version, but nothing happened, so I don't think you have to worry about DnD.

Sideline answered 5/1, 2015 at 19:37 Comment(3)
Thank you for this working code snippet. I found and thought about this "my own complex CellEditor"-approach before asking the question. The problem is you are losing some "normal" CellEditor behaviour. With your editor the differences are: A click on the Checkbox moves it some pixel to the left; because of the CellRenderer that (now) uses a different layout-method. The TableCell doesn't get a Selection and FocusBorder after a MouseClick on it. The Checkbox can't be checked by the spaceBar; only if the checkbox was directly clicked the spacebar works ...Atmolysis
... After a keyboard-cursor selection of a cell (which works), a spacebar-press removes the Selection and Focus is removed. What's with DragAndDrop? Not tested yet. Because of this problems I tried to leave the original CellEditor in place and stop the celledit if the mouseclick is in the wrong place. I know it's a little hacky.Atmolysis
@Atmolysis Thanks for your comment :). I'll try to find a way to fix these problems. It should be possible (I think) without having to mess around with mouse coordinates.Sideline
D
0

Your code has a bug. Try pressing a cell editor's checkbox, drag outside of the cell and release the mouse button. Then click somewhere OUTSIDE the checkbox. The cell is being edited. I guess it means shouldSelectCell is not the right solution for you.

I think you should consider disabling the cell editors for the entire column, and implementing a custom MouseAdapter on th JTable to calculate the checkbox's location and change the model itself.

Denial answered 5/1, 2015 at 12:11 Comment(2)
nice bug, you found ;-) I didn't downvote your answer, but I think disabling the CellEditors (and working around the JTable-mechanics) will lead to new problems?!Atmolysis
It sure can lead to other bugs and it has it's drawbacks. But the way I see it, shouldSelectCell can NEVER give you a perfect solution, since it messes with cell-selection flows, rather then with cell editing flows. I think carefully registered mouse listener can give you more flexible and well known behaivour of your table.Denial

© 2022 - 2024 — McMap. All rights reserved.