【问题标题】:How to identify a direct click on a JCheckBox in a JTable?如何识别对 JTable 中 JCheckBox 的直接点击?
【发布时间】:2015-02-15 01:51:06
【问题描述】:

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

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

这是迄今为止我已经意识到的 hack,有趣的部分开始于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 行为都是正确的:选择、鼠标悬停、EditModes、...

我不喜欢它的地方:

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

或者有没有更好的方法来实现这个表和复选框行为?

编辑:

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

除了单元格选择之外可以决定吗?

【问题讨论】:

  • 如果您使用来自 XxxTabeleCellEditor 的 getComponent().getC... 并更改编辑器的 setClickCountToStart 会发生什么
  • @mKorbel 如果您的意思是getComponent().getComponentAt(ev.getPoint()),它会返回 JCheckBox,因为它填充了整个 TableCell。关于您更改“setClickCountToStart for Editor”的建议,我不知道它对我有什么帮助!?
  • 我不想变得粗鲁或其他任何事情,但是我的回答对您有用吗?如果没有,你能留下评论为什么吗? (如果你只是想等着看有没有其他答案,那没关系,直接说吧:))只是想知道我是否可以做些什么来让我的回复更好。
  • 让我快点没问题。请在您的回答下查看我的评论。

标签: java swing jtable jcheckbox tablecelleditor


【解决方案1】:

在哪里可以获得未绘制组件的尺寸?

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

【讨论】:

  • 感谢您的建议;有用!请查看我更新的问题以获得更准确的问题。
【解决方案2】:

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

相反,一种更简洁的方法是使复选框不填满整个单元格,这样只有在您直接按下它的“复选框”部分时才会选中它。

可以通过将JCheckbox 放入JPanel 并使其居中而不拉伸它来实现此行为。为此,我们可以将JPanel 的布局管理器设为GridBagLayout。看看使用GridBagLayout 时,内部内容不会拉伸:

(来自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:
我阅读了您的评论并找到了解决方案:为什么不保留 editor 原样,而只需制作一个源自DefaultTableCellRenderer 的单元格renderer?然后,在您的 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 方法:
   

我几乎看不出有什么不同:)

恕我直言,我仍然认为仅使用普通 Java 表 API 比根据鼠标指针位置计算检查更干净,但选择权在你。

我希望这会有所帮助!


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

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。

【讨论】:

  • 感谢您提供此工作代码 sn-p。在提出问题之前,我发现并思考了这种“我自己的复杂 CellEditor”方法。问题是您正在失去一些“正常”的 CellEditor 行为。与您的编辑器不同的是: 单击复选框会将其向左移动一些像素;因为 CellRenderer(现在)使用了不同的布局方法。 鼠标点击后,TableCell 没有得到选择和 FocusBorder。 复选框不能由空格键检查;仅当直接单击复选框时,空格键才有效 ...
  • ... 在使用键盘光标选择单元格(有效)后,按空格键会删除选择并删除焦点。 DragAndDrop 是什么?尚未测试。 由于这个问题,我尝试将原来的 CellEditor 留在原处,如果鼠标点击在错误的位置,则停止 celledit。我知道这是一个 hacky。
  • @bobndrew 感谢您的评论 :)。我会尝试找到解决这些问题的方法。它应该是可能的(我认为)而不必乱用鼠标坐标。
【解决方案3】:

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

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

【讨论】:

  • 不错的错误,您发现了 ;-) 我没有否决您的答案,但我认为禁用 CellEditor(并解决 JTable 机制)会导致新问题?!
  • 它肯定会导致其他错误并且它有它的缺点。但在我看来,shouldSelectCell 永远无法为您提供完美的解决方案,因为它与单元格选择流程相混淆,而不是与单元格编辑流程相混淆。我认为仔细注册的鼠标监听器可以为您的表提供更灵活和众所周知的行为。
猜你喜欢
  • 2012-08-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-21
  • 2017-02-01
  • 2013-08-10
  • 2021-08-08
  • 2014-04-18
相关资源
最近更新 更多