2

在我的主应用程序中,当从单元格编辑器组件显示对话框时,JTable 失去焦点。

下面是我为您制作的简单 SSCCE 以查看问题。

做这些简单的实验:

  • 在第一个表格列中按 F2 开始编辑。然后将列内容更改为数字 2 并按 ENTER 键。表格将失去焦点,表单中的第一个字段将失去焦点。
  • 在第一个表格列中按 F2 开始编辑。然后将列内容更改为数字 2 并按 TAB 键。表格将失去焦点,表单中的第一个字段将失去焦点。

表单中的第一个字段也是一个 SearchField 组件。因为它不在 JTable 中,所以当您将其内容更改为数字 2 并提交编辑(使用 ENTER 或 TAB)时,它会正常运行。

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Objects;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultCellEditor;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.table.AbstractTableModel;
import javax.swing.text.DefaultFormatterFactory;
import javax.swing.text.NumberFormatter;

public class SSCCE extends JPanel
{
    private SSCCE()
    {
        setLayout(new BorderLayout());
        setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));

        JPanel pnlFields = new JPanel();
        pnlFields.setLayout(new BoxLayout(pnlFields, BoxLayout.PAGE_AXIS));
        pnlFields.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));

        SearchField field1 = new SearchField();
        configureField(field1);
        pnlFields.add(field1);

        pnlFields.add(Box.createRigidArea(new Dimension(0, 3)));

        JTextField field2 = new JTextField();
        configureField(field2);
        pnlFields.add(field2);

        add(pnlFields, BorderLayout.PAGE_START);
        add(new JScrollPane(createTable()), BorderLayout.CENTER);
    }

    private void configureField(JTextField field)
    {
        field.setPreferredSize(new Dimension(150, field.getPreferredSize().height));
        field.setMaximumSize(field.getPreferredSize());
        field.setAlignmentX(LEFT_ALIGNMENT);
    }

    private JTable createTable()
    {
        JTable table = new JTable(new CustomTableModel());

        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        table.setCellSelectionEnabled(true);
        table.getTableHeader().setReorderingAllowed(false);
        table.setPreferredScrollableViewportSize(new Dimension(500, 170));

        table.setDefaultEditor(Integer.class, new SearchFieldCellEditor(new SearchField()));

        return table;
    }

    private static void createAndShowGUI()
    {
        JFrame frame = new JFrame("SSCCE (JTable Loses Focus)");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new SSCCE());
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(
            new Runnable()
            {
                @Override
                public void run()
                {
                    createAndShowGUI();
                }
            }
        );
    }
}

class CustomTableModel extends AbstractTableModel
{
    private String[] columnNames = {"Column1 (Search Field)", "Column 2"};
    private Class<?>[] columnTypes = {Integer.class, String.class};
    private Object[][] data = {{1, ""}, {3, ""}, {4, ""}, {5, ""}, {6, ""}};

    @Override
    public int getColumnCount()
    {
        return columnNames.length;
    }

    @Override
    public int getRowCount()
    {
        return data.length;
    }

    @Override
    public String getColumnName(int col)
    {
        return columnNames[col];
    }

    @Override
    public Object getValueAt(int row, int col)
    {
        return data[row][col];
    }

    @Override
    public Class<?> getColumnClass(int c)
    {
        return columnTypes[c];
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex)
    {
        return true;
    }

    @Override
    public void setValueAt(Object value, int row, int col)
    {
        data[row][col] = value;
        fireTableCellUpdated(row, col);
    }
}

class SearchFieldCellEditor extends DefaultCellEditor
{
    SearchFieldCellEditor(final SearchField searchField)
    {
        super(searchField);
        searchField.removeActionListener(delegate);
        delegate = new EditorDelegate()
        {
            @Override
            public void setValue(Object value)
            {
                searchField.setValue(value);
            }

            @Override
            public Object getCellEditorValue()
            {
                return searchField.getValue();
            }
        };
        searchField.addActionListener(delegate);
    }

    @Override
    public boolean stopCellEditing()
    {
        try
        {
            ((SearchField) getComponent()).commitEdit();
        }
        catch (ParseException ex)
        {
            ex.printStackTrace();
        }
        return super.stopCellEditing();
    }
}

class SearchField extends JFormattedTextField implements PropertyChangeListener
{
    private Object _oldValue;

    SearchField()
    {
        setupFormatter();
        addPropertyChangeListener("value", this);
    }

    private void setupFormatter()
    {
        NumberFormat integerFormat = NumberFormat.getIntegerInstance();
        integerFormat.setGroupingUsed(false);

        NumberFormatter integerFormatter =
            new NumberFormatter(integerFormat)
            {
                @Override
                public Object stringToValue(String text) throws ParseException
                {
                    return text.isEmpty() ? null : super.stringToValue(text);
                }
            };
        integerFormatter.setValueClass(Integer.class);
        integerFormatter.setMinimum(Integer.MIN_VALUE);
        integerFormatter.setMaximum(Integer.MAX_VALUE);

        setFormatterFactory(new DefaultFormatterFactory(integerFormatter));
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt)
    {
        Object newValue = evt.getNewValue();
        if (!Objects.equals(newValue, _oldValue))
        {
            _oldValue = newValue;
            // Suppose that a value of 2 means that the data wasn't found.
            // So we display a message to the user.
            if (new Integer(2).equals(newValue))
            {
                JOptionPane.showMessageDialog(
                    null, "Not found: " + newValue + ".", "Warning",
                    JOptionPane.WARNING_MESSAGE);
            }
        }
    }
}

那么,有没有办法解决这个问题呢?这个问题的解决方案对我来说非常重要。

谢谢你。

马科斯

*更新*

我想我已经找到了一个解决方案,但如果它真的是一个值得信赖的解决方案,我想听听你的意见。

stopCellEditing方法更改为此并再次测试 SSCCE:

@Override
public boolean stopCellEditing()
{
    SearchField searchField = (SearchField) getComponent();

    try
    {
        searchField.commitEdit();
    }
    catch (ParseException ex)
    {
        ex.printStackTrace();
    }

    Component table = searchField.getParent();
    table.requestFocusInWindow();

    return super.stopCellEditing();
}

那么,您认为这真的解决了问题还是有任何缺陷?

马科斯

更新 2

我发现了一个小漏洞。通过以下更改进行了更正:

class SearchFieldCellEditor extends DefaultCellEditor
{
    SearchFieldCellEditor(final SearchField searchField)
    {
        super(searchField);
        searchField.removeActionListener(delegate);
        delegate = new EditorDelegate()
        {
            @Override
            public void setValue(Object value)
            {
                searchField.setValue(value);
            }

            @Override
            public Object getCellEditorValue()
            {
                return searchField.getValue();
            }
        };
        searchField.addActionListener(delegate);
    }

    @Override
    public Component getTableCellEditorComponent(
        JTable table, Object value, boolean isSelected, int row, int column)
    {
        SearchField searchField = (SearchField) getComponent();
        searchField.setPreparingForEdit(true);
        try
        {
            return super.getTableCellEditorComponent(
                table, value, isSelected, row, column);
        }
        finally
        {
            searchField.setPreparingForEdit(false);
        }
    }

    @Override
    public boolean stopCellEditing()
    {
        SearchField searchField = (SearchField) getComponent();

        try
        {
            searchField.commitEdit();
        }
        catch (ParseException ex)
        {
            ex.printStackTrace();
        }

        Component table = searchField.getParent();
        table.requestFocusInWindow();

        return super.stopCellEditing();
    }
}

class SearchField extends JFormattedTextField implements PropertyChangeListener
{
    private boolean _isPreparingForEdit;
    private Object _oldValue;

    SearchField()
    {
        setupFormatter();
        addPropertyChangeListener("value", this);
    }

    void setPreparingForEdit(boolean isPreparingForEdit)
    {
        _isPreparingForEdit = isPreparingForEdit;
    }

    private void setupFormatter()
    {
        NumberFormat integerFormat = NumberFormat.getIntegerInstance();
        integerFormat.setGroupingUsed(false);

        NumberFormatter integerFormatter =
            new NumberFormatter(integerFormat)
            {
                @Override
                public Object stringToValue(String text) throws ParseException
                {
                    return text.isEmpty() ? null : super.stringToValue(text);
                }
            };
        integerFormatter.setValueClass(Integer.class);
        integerFormatter.setMinimum(Integer.MIN_VALUE);
        integerFormatter.setMaximum(Integer.MAX_VALUE);

        setFormatterFactory(new DefaultFormatterFactory(integerFormatter));
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt)
    {
        final Object newValue = evt.getNewValue();
        if (!Objects.equals(newValue, _oldValue))
        {
            _oldValue = newValue;
            // Suppose that a value of 2 means that the data wasn't found.
            // So we display a message to the user.
            if (new Integer(2).equals(newValue) && !_isPreparingForEdit)
            {
                JOptionPane.showMessageDialog(null, "Not found: " + newValue + ".", "Warning",
                    JOptionPane.WARNING_MESSAGE);
            }
        }
    }
}

你是否也发现了更多的漏洞?我想听听你的评论。

马科斯

更新 3

kleopatra建议后的另一个解决方案:

class SearchFieldCellEditor extends DefaultCellEditor
{
    SearchFieldCellEditor(final SearchField searchField)
    {
        super(searchField);
        searchField.setShowMessageAsynchronously(true);
        searchField.removeActionListener(delegate);
        delegate = new EditorDelegate()
        {
            @Override
            public void setValue(Object value)
            {
                searchField.setValue(value);
            }

            @Override
            public Object getCellEditorValue()
            {
                return searchField.getValue();
            }
        };
        searchField.addActionListener(delegate);
    }

    @Override
    public Component getTableCellEditorComponent(
        JTable table, Object value, boolean isSelected, int row, int column)
    {
        SearchField searchField = (SearchField) getComponent();
        searchField.setPreparingForEdit(true);
        try
        {
            return super.getTableCellEditorComponent(
                table, value, isSelected, row, column);
        }
        finally
        {
            searchField.setPreparingForEdit(false);
        }
    }

    @Override
    public boolean stopCellEditing()
    {
        SearchField searchField = (SearchField) getComponent();

        try
        {
            searchField.commitEdit();
        }
        catch (ParseException ex)
        {
            ex.printStackTrace();
        }

        return super.stopCellEditing();
    }
}

class SearchField extends JFormattedTextField implements PropertyChangeListener
{
    private boolean _showMessageAsynchronously;
    private boolean _isPreparingForEdit;
    private Object _oldValue;

    SearchField()
    {
        setupFormatter();
        addPropertyChangeListener("value", this);
    }

    public boolean isShowMessageAsynchronously()
    {
        return _showMessageAsynchronously;
    }

    public void setShowMessageAsynchronously(boolean showMessageAsynchronously)
    {
        _showMessageAsynchronously = showMessageAsynchronously;
    }

    void setPreparingForEdit(boolean isPreparingForEdit)
    {
        _isPreparingForEdit = isPreparingForEdit;
    }

    private void setupFormatter()
    {
        NumberFormat integerFormat = NumberFormat.getIntegerInstance();
        integerFormat.setGroupingUsed(false);

        NumberFormatter integerFormatter =
            new NumberFormatter(integerFormat)
            {
                @Override
                public Object stringToValue(String text) throws ParseException
                {
                    return text.isEmpty() ? null : super.stringToValue(text);
                }
            };
        integerFormatter.setValueClass(Integer.class);
        integerFormatter.setMinimum(Integer.MIN_VALUE);
        integerFormatter.setMaximum(Integer.MAX_VALUE);

        setFormatterFactory(new DefaultFormatterFactory(integerFormatter));
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt)
    {
        final Object newValue = evt.getNewValue();
        if (!Objects.equals(newValue, _oldValue))
        {
            _oldValue = newValue;
            // Suppose that a value of 2 means that the data wasn't found.
            // So we display a message to the user.
            if (new Integer(2).equals(newValue) && !_isPreparingForEdit)
            {
                if (_showMessageAsynchronously)
                {
                    SwingUtilities.invokeLater(
                        new Runnable()
                        {
                            @Override
                            public void run()
                            {
                                showMessage(newValue);
                            }
                        }
                    );
                }
                else
                {
                    showMessage(newValue);
                }
            }
        }
    }

    private void showMessage(Object value)
    {
        JOptionPane.showMessageDialog(null, "Not found: " + value + ".",
            "Warning", JOptionPane.WARNING_MESSAGE);
    }
}

关于最后一个解决方案的评论和建议仍然受到赞赏。这是最终和最优的解决方案吗?

马科斯

4

2 回答 2

1

在 stopCellEditing() 方法中进行编辑。

在此示例中,您必须输入 5 个字符的字符串:

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

public class TableEdit extends JFrame
{
    TableEdit()
    {
        JTable table = new JTable(5,5);
        table.setPreferredScrollableViewportSize(table.getPreferredSize());

        JScrollPane scrollpane = new JScrollPane(table);
        add(scrollpane);

        //  Use a custom editor

        TableCellEditor fce = new FiveCharacterEditor();
        table.setDefaultEditor(Object.class, fce);

        add(new JTextField(), BorderLayout.NORTH);
    }

    class FiveCharacterEditor extends DefaultCellEditor
    {
        FiveCharacterEditor()
        {
            super( new JTextField() );
        }

        public boolean stopCellEditing()
        {
            JTable table = (JTable)getComponent().getParent();

            try
            {
                System.out.println(getCellEditorValue().getClass());
                String editingValue = (String)getCellEditorValue();

                if(editingValue.length() != 5)
                {
                    JTextField textField = (JTextField)getComponent();
                    textField.setBorder(new LineBorder(Color.red));
                    textField.selectAll();
                    textField.requestFocusInWindow();

                    JOptionPane.showMessageDialog(
                        null,
                        "Please enter string with 5 letters.",
                        "Alert!",JOptionPane.ERROR_MESSAGE);
                    return false;
                }
            }
            catch(ClassCastException exception)
            {
                return false;
            }

            return super.stopCellEditing();
        }

        public Component getTableCellEditorComponent(
            JTable table, Object value, boolean isSelected, int row, int column)
        {
            Component c = super.getTableCellEditorComponent(
                table, value, isSelected, row, column);
            ((JComponent)c).setBorder(new LineBorder(Color.black));

            return c;
        }

    }

    public static void main(String [] args)
    {
        JFrame frame = new TableEdit();
        frame.setDefaultCloseOperation(EXIT_ON_CLOSE);
        frame.pack();
        frame.setLocationRelativeTo( null );
        frame.setVisible(true);
    }
}
于 2013-04-05T16:45:48.430 回答
1

正如我已经评论过的:在编辑器中更改表格的状态有点可疑,特别是如果它与即使在最好的情况下也很脆弱的焦点有关。所以我会竭尽全力避免它。

错误行为感觉类似于错误实现的 InputVerifier,它在其验证与在其 shouldYieldFocus 中具有副作用(如抓住焦点),这是正确的:在这种情况下,focusManager 会感到困惑,它“忘记”了自然last-focusOwner-before。

补救措施可能是让经理先完成其工作,并仅在完成后才显示信息。在您的示例代码中,可以通过包装到 invokeLater 中来实现:

if (needsMessage()) {
    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            JOptionPane.showMessageDialog(null, "Not found: " +
                    newValue + ".", "Warning",
                    JOptionPane.WARNING_MESSAGE);

        }
    });
}
于 2013-04-06T09:43:36.350 回答