在第20章中,我們通過探討可插撥的觀感體繫結構支援瞭解了如何自訂基於Swing的程式。在本章中,我們將會探討由作為Swing包一部分的JFC所提供的Undo架構。
Sun的Swing包包含一個支援我們程式中撤銷操作支援的公用程式。他允許我們支援修改我們資料狀態的撤銷與重做操作。儘管這個架構是Swing包階層的一部分,他卻可以用在任意的程式中,而不僅是基於組件的程式。
21.1 使用Undo架構
Undo架構位於javax.swing.undo包中,包含五個類,兩個介面以及兩個異常。要支援Undo架構,在javax.swing.event包中還有一個相關的癌與事件。在其頂部是UndoableEdit介面。這個介面構成了用於封裝使用Command設計模式可以撤銷或是重做的操作。
可撤銷命令的根實作類別是AbstractUndoableEdit類。不要叫這個類名迷惑我們,他並不是抽象的。根命令的子命令是CompundEdit與StateEdit命令類。
CompoundEdit類可以使得我們組合多個可撤銷的操作,其中一些可撤銷的操作則是儲存狀態變化的StateEdit對象。Swing文本組件會在他們的內容發生變化時建立DefaultDocumentEvent命令。這個命令是CompundEdit的子類,同時也是AbstractDocument的內聯類。另一個封裝的命令是UndoManager,他是CompundEdit的子類。
UndoManager通過承擔UndoableEditListener的角色並且響應UndoableEditEvent的建立來管理可編輯對象上的編輯操作。當UndoableEdit不可撤銷時,則會拋出CannotUndoException。另外,當一個UndoableEdit不可以重做時,則會拋出CannotRedoException。
如查我們希望建立支援撤銷與重做操作的對象,對象需要實現StateEditable介面,並且他們可以使用UndoableEditSupport類來協助管理UndoableEdit對象的列表。
在深入單個Undo架構的片段細節之前,我們先來瞭解一下如何在Swing文本組件中使用Undo架構。如果這些就是我們所需要的,我們就不需要理解其他的工作了。
21.2 配合Swing文本組件使用Undo架構
Swing文本組件已經支援了必須的撤銷與重做功能。我們並不需要使用UndoManager來進行管理並且通知管理器何時撤銷/重做。
作為一個樣本,考慮一個包含一個JTextArea與兩個用於撤銷與重做的工具列按鈕,21-1所示。
要使得圖21-1中的JTextArea支援撤銷操作,我們必須將一個UndoableEditListener關聯到組件的Document。我們需要做的就是將UndoManager作為我們的監聽器。首先,我們建立管理器,然後關聯。
UndoManager manager = new UndoManager();textArea.getDocument().addUndoableEditListener(manager);
一旦管理器被關聯到JTextArea的文檔,他就會監聽文本域內容的所有變化。因為每一個Swing文本組件都有一個Document資料模型,我們可以直接將UndoManager與這些組件的文檔相關聯。
在將管理器關聯到文本組件之後,我們必須提供一些方法來通知管理器撤銷或是重做某一操作。通常這是通過菜單選擇或是工具列按鈕選擇來實現的。在圖21-1中,這是藉助於JToolBar上的按鈕來實現的,而另一個按鈕用於實現重做。對於Undo按鈕,我們希望管理撤銷一個操作。所以,按鈕的ActionListener應該調用UndoManager的public void undo()方法。Redo按鈕的ActionListener應該調用管理器的public void redo()方法。undo()方法與redo()方法都會拋出必須處理的異常。
因為Undo與Redo按鈕所必須的功能對於所用的管理器都是相同的,我們將會在列表21-1中包含一個助手類UndoManagerHelper來為我們建立Action對象。這些對象可以為JMenuBar,JToolBar,或是其他可以使用ActionListener響應用於處理撤銷與重做操作的組件所用。我們必須向助手類請求Action,然後將Action關聯到相應的組件。例如,下面的五行代碼將會擷取前面所建立的UndoManager,並且為JToolBar添加必要的按鈕:
JToolBar toolbar = new JToolBar();JButton undoButton = new JButton(UndoManagerHelper.getUndoAction(manager));toolbar.add(undoButton);JButton redoButton = new JButton(UndoManagerHelper.getRedoAction(manager));toolbar.add(redoButton);
使用Swing文本組件的撤銷功能就是如此簡單。UndoManagerHelper類的定義顯示在列表21-1中。如果我們不喜歡預設的按鈕標籤(顯示在圖21-1中),還有一些支援自訂的其他方法。另外,如果在撤銷或是重做過程中拋出異常,則會彈出一個警告資訊。警告資訊與彈出容器標題也是可以自訂的。
package swingstudy.ch21; import java.awt.Component;import java.awt.event.ActionEvent; import javax.swing.AbstractAction;import javax.swing.Action;import javax.swing.JOptionPane;import javax.swing.UIManager;import javax.swing.undo.CannotRedoException;import javax.swing.undo.CannotUndoException;import javax.swing.undo.UndoManager; public class UndoManagerHelper { public static Action getUndoAction(UndoManager manager, String label) {return new UndoAction(manager, label);} public static Action getUndoAction(UndoManager manager) {return new UndoAction(manager, (String)UIManager.get("AbstractUndoableEdit.undoText"));} public static Action getRedoAction(UndoManager manager, String label) {return new RedoAction(manager, label);} public static Action getRedoAction(UndoManager manager) {return new RedoAction(manager, (String)UIManager.get("AbstractUndoableEdit.redoText"));} private abstract static class UndoRedoAction extends AbstractAction {UndoManager undoManager = new UndoManager();String errorMessage = "Cannot undo";String errorTitle = "Undo Problem"; protected UndoRedoAction(UndoManager manager, String name) {super(name);undoManager = manager;} public void setErrorMessage(String newValue) {errorMessage = newValue;} public void setErrorTitle(String newValue) {errorTitle = newValue;} protected void showMessage(Object source) {if (source instanceof Component) {JOptionPane.showMessageDialog((Component)source, errorMessage, errorTitle, JOptionPane.WARNING_MESSAGE);}else {System.err.println(errorMessage);}}} public static class UndoAction extends UndoRedoAction {public UndoAction(UndoManager manager, String name) {super(manager, name);setErrorMessage("Cannot undo");setErrorTitle("Undo Problem");} public void actionPerformed(ActionEvent event) {try {undoManager.undo();}catch(CannotUndoException cannotUndoException) {showMessage(event.getSource());}}} public static class RedoAction extends UndoRedoAction {public RedoAction(UndoManager manager, String name) {super(manager, name);setErrorMessage("Cannot redo");setErrorTitle("Redo Problem");} public void actionPerformed(ActionEvent event) {try {undoManager.redo();}catch(CannotRedoException cannotRedoException) {showMessage(event.getSource());}}}}圖21-1中所示樣本的其餘原始碼顯示在列表21-2中。藉助於新建立的UndoManagerHelper類,在Swing文本組件中使用Undo架構的最大困難已經得到了極大的簡化。
package swingstudy.ch21; import java.awt.BorderLayout;import java.awt.EventQueue; import javax.swing.JButton;import javax.swing.JFrame;import javax.swing.JScrollPane;import javax.swing.JTextArea;import javax.swing.JToolBar;import javax.swing.undo.UndoManager; public class UndoSample { /** * @param args */public static void main(String[] args) {// TODO Auto-generated method stub Runnable runner = new Runnable() {public void run() {JFrame frame = new JFrame("Undo Sample");frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);JTextArea textArea = new JTextArea();JScrollPane scrollPane = new JScrollPane(textArea); UndoManager manager = new UndoManager();textArea.getDocument().addUndoableEditListener(manager); JToolBar toolbar = new JToolBar();JButton undoButton = new JButton(UndoManagerHelper.getUndoAction(manager));toolbar.add(undoButton);JButton redoButton = new JButton(UndoManagerHelper.getRedoAction(manager));toolbar.add(redoButton); frame.add(toolbar, BorderLayout.NORTH);frame.add(scrollPane, BorderLayout.CENTER);frame.setSize(300, 150);frame.setVisible(true);}};EventQueue.invokeLater(runner);} }如果我們只是希望在Swing文本組件中使用Undo架構,我們可以跳過本章的其他部分。相反,如果我們希望在其他的組件中使用該架構,或者甚至是在非組件設定中使用,我們需要閱讀其餘部分,我們將會在餘下的內容中深入探討架構的內部實現。
21.3 Command設計模式javax.swing.undo包的撤銷功能實現了Command設計模式,其包括如下組成部分:
- Command:UndoableEdit介面定義了用於執行撤銷與重做操作的介面。
- Concrete Command:AbstractUndoableEdit類的執行個體,或者是更為特定的子類,實現了必須的Command介面。他們將命令綁定到接收者(Document)來修改其內容。
- Client:在Swing文本組件的例子中,Document執行了實際的AbstractUndoableEdit子類的建立,預設為AbstractDocument.DefaultDocumentEvent。
- Invoker:UndoManager扮演UndoableEdit命令調用者的角色。通常情況下,其他的參與者會通知調用者何時執行調用。然而,調用者會通知特定的UndoableEdit執行個體何時撤銷或是重做命令。
- Receiver:Document是實際的AbstractUndoabelEdit子類中命令的接收者。他知道如何執行請求。
如果我們要Swing文本組件之外使用Undo架構,Document元素將會為特定於我們客戶程式的接收者所替代。我們需要建立我們自己的UndoableEdit介面實現來承擔模式中Concrete Command的任務。無需直接實現介面,我們只需要繼承AbstractUndoableEdit類來封裝關於我們命令的特定資訊。
Command設計模式十分強大。無論我們在模式中使用哪種命令,我們都可以設定功能,例如用於執行自動化的測試的宏,因為調用會順序執行這些命令。