如何识别JTable中JCheckBox的直接点击?

时间:2014-12-16 17:00:21

标签: java swing jtable jcheckbox tablecelleditor

使用JCheckBox作为JTable列中的编辑器,我想忽略TableCell中CheckBox左右两侧的鼠标点击。

我在2011年的Oracle论坛上找到了一个讨论,但问题没有在那里得到解决:https://community.oracle.com/thread/2183210

这是我到目前为止所实现的黑客攻击,有趣的部分始于class 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();
      }
    } );
  }
}

我喜欢这个解决方案:

  • 所有TableCell行为都是正确的:选择,MouseOver,EditModes,...

我不喜欢它:

  • JCheckBox的硬编码大小(int halfWidthOfClickArea
    • 我在哪里可以获得未上漆组件的尺寸?

还是有更好的方法来实现这个Table和CheckBox行为吗?

编辑:

我按照camickr的建议更改了源代码,并为具有更高RowHeights的表添加了垂直hitzone。
但到目前为止,我忘了提到我的问题的主要原因...... ;-)
我在方法stopCellEditing()中调用shouldSelectCell(..)

可以决定选择的不仅仅是细胞选择吗?

3 个答案:

答案 0 :(得分:4)

  

我在哪里可以获得未上漆组件的尺寸?

  System.out.println(checkBox.getPreferredSize() );

答案 1 :(得分:2)

我认为停止选择shouldSelectCell()方法中的实际值是一种迂回的方法,转换鼠标事件似乎很奇怪。

相反,一个更清洁的方法是使复选框不填充整个单元格,这样只有直接按下它的“复选框”部分才能选中它。

这种行为可以通过将JCheckbox置于JPanel内并将其居中而不拉伸来实现。为此,我们可以将JPanel的布局管理器设为GridBagLayout。了解如何使用GridBagLayout,内部内容不会延伸:

Layout managers (来自this StackOverflow answer

现在,如果您点击它周围的空白区域,您将点击JPanel,这样您就不会更改JCheckBox的值。

CheckBoxEditor的代码最终结果如下:

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;
    }
}

(请注意,您不能再从DefaultCellEditor延伸 - 在上面的代码中,您现在必须从AbstractCellEditor扩展并实施TableCellEditor)。

我认为这个版本的CheckBoxEditor可以满足您的需求 - 如果您点击复选框周围的空白区域,则不会发生任何事情。如果您直接单击该复选框,则仅会选中该复选框。

此外,通过使用JPanel,您不必进行任何MouseEvent计算和(至少对我而言),代码看起来更清晰,更容易看到发生了什么


<强> EDIT1:
我读了你的评论,我找到了一个解决方案:为什么不按原样保留编辑器,然后只需创建一个源自DefaultTableCellRenderer渲染器?然后,在CheckBoxEditor中使用与渲染器相同的边框和背景。

这应该达到你想要的效果(我将公共代码移动到外部类方法中,所以我不必重复它们):

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;
    }
}

然后,您必须在构造函数中将渲染器与编辑器一起设置:

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

以下是两个比较两种方法的截图:

您的原始方法: JPanel JCheckBox方法:
Old New

我几乎看不出差异:)

恕我直言,我仍然认为只使用普通的Java表格API比根据鼠标指针位置计算检查更清晰,但选择取决于你。

我希望这有帮助!


<强> EDIT2:
此外,如果您希望能够使用空格键进行切换,我认为您只需在componentPanel构造函数中添加一个键绑定到CheckBoxEditor

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());
            }
        });
        // ...
    }

    // ...
}

我不确定你是否可以使用布尔值拖放。我尝试将“true”拖到原始版本的复选框中,但没有任何反应,所以我认为你不必担心DnD。

答案 2 :(得分:0)

您的代码有错误。尝试按下单元格编辑器的复选框,拖动单元格并释放鼠标按钮。然后单击复选框外的某处。正在编辑单元格。我想这意味着shouldSelectCell不适合你。

我认为您应该考虑停用整个列的单元格编辑器,并在MouseAdapter上实现自定义JTable来计算复选框的位置并更改模型本身。