java - reales - ¿Cómo identificar un clic directo en un JCheckBox en una JTable?
libro de android studio en español pdf (3)
Con un JCheckBox como Editor en una columna JTable, me gustaría ignorar los clics del mouse en el espacio a la izquierda y derecha de un CheckBox en un TableCell.
He encontrado una discusión desde 2011 en el foro de Oracle, pero el problema no se resolvió allí: https://community.oracle.com/thread/2183210
Este es el truco que me di cuenta hasta ahora, la parte interesante comienza en la 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();
}
} );
}
}
Lo que me gusta de esta solución:
- todo el comportamiento de TableCell es correcto: seleccionando, MouseOver, EditModes, ...
Lo que no me gusta de eso:
- el tamaño codificado de JCheckBox (
int halfWidthOfClickArea
)- ¿Dónde puedo obtener las dimensiones de un componente sin pintar?
¿O hay mejores formas de lograr esta tabla y el comportamiento CheckBox?
EDITAR:
Cambié el código fuente siguiendo el consejo de camickr y agregué un hitzone vertical para tablas con RowHeights más alto.
Pero hasta ahora olvidé mencionar el motivo principal de mi pregunta ... ;-)
Estoy llamando a stopCellEditing()
en el método shouldSelectCell(..)
.
¿Está bien decidir sobre eso más que la Selección de celda?
¿Dónde puedo obtener las dimensiones de un componente sin pintar?
System.out.println(checkBox.getPreferredSize() );
Tu código tiene un error. Intente presionar la casilla de verificación de un editor de celda, arrastre fuera de la celda y suelte el botón del mouse. Luego haga clic en algún lugar FUERA de la casilla de verificación. La celda está siendo editada. Supongo que significa que shouldSelectCell
no es la solución adecuada para ti.
Creo que debería considerar deshabilitar los editores de la columna completa e implementar un MouseAdapter
personalizado en la JTable
para calcular la ubicación de la casilla de verificación y cambiar el modelo en sí.
Creo que detener la selección del real en el método shouldSelectCell()
es una especie de método indirecto para hacer esto, y la conversión de eventos del mouse parece extraña.
En cambio, un enfoque mucho más limpio sería hacer que la casilla de verificación no llene toda la celda, por lo que solo se selecciona si presiona directamente en la parte de "casilla de verificación".
Este comportamiento se puede lograr colocando su JCheckbox
dentro de un JPanel
y centrarlo sin estirarlo. Para hacer esto, podemos hacer que el JPanel
de diseño de GridBagLayout
un GridBagLayout
. Vea cómo al usar GridBagLayout
, el contenido interno no se estira:
(de esta respuesta de )
Entonces, si hace clic en el espacio vacío que lo rodea, estará haciendo clic en un JPanel
, por lo que no cambiará el valor de JCheckBox
.
El código para su CheckBoxEditor
resulta así al final:
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;
}
}
(Tenga en cuenta que ya no puede extenderse desde DefaultCellEditor
- en el código anterior, ahora tiene que extender desde AbstractCellEditor
e implementar TableCellEditor
).
Creo que esta versión de CheckBoxEditor
hace lo que quiere: si hace clic en el espacio vacío alrededor de la casilla de verificación, no pasa nada. La casilla de verificación solo se vuelve activa si hace clic directamente en ella.
Además, al usar un JPanel
, no tiene que hacer ningún cálculo de MouseEvent
y (para mí, al menos), el código se ve mucho más limpio y es más fácil ver lo que está sucediendo.
EDIT1:
Leí tu comentario y encontré una solución: ¿por qué no dejaste el editor tal como está, pero luego solo creas un procesador de células que deriva del DefaultTableCellRenderer
? Luego, en su CheckBoxEditor
use los mismos bordes y fondos que el renderizador.
Esto debería lograr el efecto que desee (he movido el código común a los métodos de la clase externa para que no tenga que repetirlos):
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;
}
}
Luego, debe configurar el renderizador junto con el editor en su constructor:
checkboxColumn.setCellEditor(new CheckBoxEditor());
checkboxColumn.setCellRenderer(new CheckBoxRenderer());
Aquí hay un par de capturas de pantalla que comparan los dos métodos:
Su método original: método JPanel
y JCheckBox
:
Apenas puedo ver la diferencia :)
En mi humilde opinión, sigo creyendo que solo usar las API de la tabla Java simple es más limpio que calcular las comprobaciones basadas en las posiciones del puntero del mouse, pero la elección depende de usted.
Espero que esto haya ayudado!
EDIT2:
Además, si deseas poder alternar usando la barra espaciadora, creo que puedes simplemente agregar un enlace de clave al panel de componentPanel
en el constructor 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());
}
});
// ...
}
// ...
}
No estoy seguro de si puede usar arrastrar y soltar con valores booleanos. Traté de arrastrar "verdadero" a las casillas de verificación en la versión original, pero no pasó nada, así que no creo que tengas que preocuparte por DnD.