如何使Java jtables、单元格编辑器和undo在不创建外部undo事件的情况下协同工作



在下面的代码中,我为第一列创建了一个带有自定义单元格编辑器的jtable,然后向表中添加撤消功能。当您运行程序时,程序允许您更改第一列中的值(通过在已经存在的"abc"后面附加一个"d"和一个"e"进行测试)。现在输入control-z(撤消),然后再次输入control-z。它按预期工作。但现在再次输入control-z(撤消)。这一次"abc"被删除。看起来swing系统正在设置列的初始值,并为该操作创建一个撤消事件,然后用户可以撤消该事件。我的问题是,我如何编写代码,以便用户只能撤消用户所做的操作?

import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.DefaultCellEditor;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JRootPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
import javax.swing.undo.UndoableEdit;
public class UndoExample extends JFrame {
private static final long serialVersionUID = 1L;;   
static Boolean objEnableUndoRedoActions = true; 
UndoExample rootFrame;
public UndoExample() {
    // This procedure starts the whole thing off.
    //Create table
    final String[] tableColumns = {"Column 1", "Column 2"};
    JTable tabUndoExample = new JTable(
            new DefaultTableModel(null, tableColumns) {
                private static final long serialVersionUID = 1L;                
    });
    final DefaultTableModel tabUndoExampleModel = (DefaultTableModel) tabUndoExample
            .getModel();
    tabUndoExampleModel.addRow(new Object[]{"abc", true});
    tabUndoExampleModel.addRow(new Object[]{"zyw", false});
    // Create the undo/redo manager
    UndoManager objUndoManager = new UndoManager();
    // Create a cell editor
    JTextField tfTabField = new JTextField();
    TableCellEditor objEditor = new DefaultCellEditor(tfTabField);
    // Make the cell editor the default editor for this table's first column
    tabUndoExample.getColumnModel().getColumn(0)
        .setCellEditor(objEditor);
    // Create the undo action on the field's document for the column
    tfTabField.getDocument().addUndoableEditListener(
            new uelUndoRedoTableCellField(objUndoManager, tabUndoExample));
    // Allow undo and redo to be entered by the user
    UndoRedoSetKeys(this, "Example", objUndoManager);
    tabUndoExample.setInheritsPopupMenu(true);
     //Add the table to the frame and show the frame         
    this.add(tabUndoExample);
    this.pack();
    setLocationRelativeTo(null);
    }
public static void main(final String[] args) {
    // Launches the application. This is required syntax.
    SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
            try {       
                final UndoExample rootFrame = new UndoExample();
                rootFrame.setVisible(true);             
            } catch (final Exception e) {
            }
        }
    });
}
@SuppressWarnings("serial")
static class aueUndoRedoTableCellField extends AbstractUndoableEdit {
    // Wrap the text edit action item as we need to add the table
    // row and column information.  This code is invoked when the
    // code sees an undo event created and then later when the 
    // user requests an undo/redo.
    JTable objTable = null;
    UndoableEdit objUndoableEdit;
    int objCol = -1;
    int objRow = -1;
    public aueUndoRedoTableCellField(UndoableEdit undoableEdit,
            JTable table, int row, int col) {
        super();
        objUndoableEdit = undoableEdit;
        objTable = table;
        objCol = col;
        objRow = row;
    }
    public void redo() throws CannotRedoException {
        // When the user enters redo (or undo), this code sets
        // that we are doing an redo (or undo), sets the cursor
        // to the right location, and then does the undo (or redo)
        // to the table cell.  
        UndoRedoManagerSetEnabled(false);
        super.redo();
        @SuppressWarnings("unused")
        boolean success = objTable.editCellAt(objRow, objCol);
        objTable.changeSelection(objRow, objCol, false, false);
        objUndoableEdit.redo();
        UndoRedoManagerSetEnabled(true);
    }
    public void undo() throws CannotUndoException {
        super.undo();
        UndoRedoManagerSetEnabled(false);
        @SuppressWarnings("unused")
        boolean success = objTable.editCellAt(objRow, objCol);
        objTable.changeSelection(objRow, objCol, false, false);
        objUndoableEdit.undo();
        UndoRedoManagerSetEnabled(true);
    }
}
static class aUndoRedo extends AbstractAction {
    // This code is bound to the undo/redo keystrokes and tells
    // Java what commands to run when the keys are later entered
    // by the user.
    private static final long serialVersionUID = 1L;
    Boolean objUndo = true;
    UndoManager objUndoManager = null;
    final String objLocation;
    public aUndoRedo(Boolean Undo, UndoManager undoManager, String location) {
        super();
        objUndo = Undo;
        objUndoManager = undoManager;
        objLocation = location;
    }
    @Override
    public void actionPerformed(ActionEvent ae) {
        try {
            // See if operation allowed
            if (!objUndoManager.canUndo() && objUndo
                    || !objUndoManager.canRedo() && !objUndo)
                return;
            UndoRedoManagerSetEnabled(false);
            if (objUndo) {
                objUndoManager.undo();
            } else {
                objUndoManager.redo();
            }
            UndoRedoManagerSetEnabled(true);
            // Catch errors and let user know
        } catch (Exception e) {
            UndoRedoManagerSetEnabled(true);
        }
    }
}
static class uelUndoRedoTableCellField implements UndoableEditListener {
    // This action is called when the user changes the table's
    // text cell.  It saves the change for later undo/redo.
    private UndoManager objUndoManager = null;
    private JTable objTable = null;
    public uelUndoRedoTableCellField(UndoManager undoManager,
            JTable table) {
        objUndoManager = undoManager;
        objTable = table;
    }
    @Override
    public void undoableEditHappened(UndoableEditEvent e) {
        // Remember the edit but only if the code isn't doing
        // an undo or redo currently.
        if (UndoRedoManagerIsEnabled()) {
            objUndoManager.addEdit(new aueUndoRedoTableCellField(e
                    .getEdit(), objTable, objTable.getSelectedRow(),
                    objTable.getSelectedColumn()));
        }
    }
}
static public Boolean UndoRedoManagerIsEnabled() {
    // See if we are currently doing an undo/redo.
    // Return true if so.
    return objEnableUndoRedoActions;
}
static public void UndoRedoManagerSetEnabled(Boolean state) {
    // Set the state of whether we are in undo/redo code.
    objEnableUndoRedoActions = state;
}

static void UndoRedoSetKeys(JFrame frame, final String location, UndoManager undoManager) {
    // Allow undo and redo to be called via these keystrokes for this dialog
    final String cntl_y = "CNTL_Y";
    final KeyStroke ksCntlY = KeyStroke.getKeyStroke("control Y");
    final String cntl_z = "CNTL_Z";
    final KeyStroke ksCntlZ = KeyStroke.getKeyStroke("control Z");
    JRootPane root = frame.getRootPane();
    root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
            .put(ksCntlZ, cntl_z);
    root.getActionMap().put(
            cntl_z,
            new aUndoRedo(true, undoManager, location));
    root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
            .put(ksCntlY, cntl_y);
    root.getActionMap().put(
            cntl_y,
            new aUndoRedo(false, undoManager, location));
}
}

当你按下一个键时,会发生一系列的事情。JTable处理按键笔划,检查单元格是否可编辑(如TableModel),然后询问当前所选单元格的编辑器事件是否应编辑单元格(CellEditor#isCellEditable(EventObject))。

如果此方法返回true,则准备好编辑器,将TableModel的值应用于编辑器(即调用setText),并将编辑器添加到JTable,最后,触发编辑模式的事件将重新调度到编辑器,在您的情况下为Ctrl+Z,然后触发并撤消事件,返回编辑器的初始状态(在调用setText之前)。

你可以试着用。。。

TableCellEditor objEditor = new DefaultCellEditor(tfTabField) {
    @Override
    public boolean isCellEditable(EventObject anEvent) {
        boolean isEditable = super.isCellEditable(anEvent); //To change body of generated methods, choose Tools | Templates.
        if (isEditable && anEvent instanceof KeyEvent) {
            KeyEvent ke = (KeyEvent) anEvent;
            if (ke.isControlDown() && ke.getKeyCode() == KeyEvent.VK_Z) {
                isEditable = false;
            }
        }
        return isEditable;
    }
};

以防止JTable在特定的按键笔划发生时被置于编辑状态

更新

所以基于Andrew在JTextArea setText()&UndoManager,我设计了一个"可配置"的UndoableEditListener,它可以设置为忽略可撤消的操作,例如。。。

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.Document;
import javax.swing.text.PlainDocument;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
public class FixedField {
    public static void main(String[] args) {
        new FixedField();
    }
    public FixedField() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }
                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }
    public static class UndoableEditHandler implements UndoableEditListener {
        private static final int MASK
                = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
        private UndoManager undoManager = new UndoManager();
        private boolean canUndo = true;
        public UndoableEditHandler(JTextField field) {
            Document doc = field.getDocument();
            doc.addUndoableEditListener(this);
            field.getActionMap().put("Undo", new AbstractAction("Undo") {
                @Override
                public void actionPerformed(ActionEvent evt) {
                    try {
                        if (undoManager.canUndo()) {
                            undoManager.undo();
                        }
                    } catch (CannotUndoException e) {
                        System.out.println(e);
                    }
                }
            });
            field.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, MASK), "Undo");
            field.getActionMap().put("Redo", new AbstractAction("Redo") {
                @Override
                public void actionPerformed(ActionEvent evt) {
                    try {
                        if (undoManager.canRedo()) {
                            undoManager.redo();
                        }
                    } catch (CannotRedoException e) {
                        System.out.println(e);
                    }
                }
            });
            field.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, MASK), "Redo");
        }
        @Override
        public void undoableEditHappened(UndoableEditEvent e) {
            if (canUndo()) {
                undoManager.addEdit(e.getEdit());
            }
        }
        public void setCanUndo(boolean canUndo) {
            this.canUndo = canUndo;
        }
        public boolean canUndo() {
            return canUndo;
        }
    }
    public class TestPane extends JPanel {
        public TestPane() {
            JTextField field = new JTextField(10);
            UndoableEditHandler handler = new UndoableEditHandler(field);
            handler.setCanUndo(false);
            field.setText("Help");
            handler.setCanUndo(true);
            add(field);
        }
    }
}

现在,您将不得不使用自己的TableCellEditor设备来支持这一点,例如。。。

public static class MyCellEditor extends AbstractCellEditor implements TableCellEditor {
    private JTextField editor;
    private UndoableEditHandler undoableEditHandler;
    public MyCellEditor(JTextField editor) {
        this.editor = editor;
        undoableEditHandler = new UndoableEditHandler(editor);
        editor.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                fireEditingStopped();
            }
        });
    }
    @Override
    public Object getCellEditorValue() {
        return editor.getText();
    }
    @Override
    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
        undoableEditHandler.setCanUndo(false);
        editor.setText(value == null ? null : value.toString());
        undoableEditHandler.setCanUndo(true);
        return editor;
    }
}

最新更新