我發現一旦稍稍體會到GEF的妙處,就會很自然的被它吸引住。不僅是因為用它做出的圖形介面好看,更重要的是,UI中最複雜和細微的問題,在GEF的設計中無不被周到的考慮並以適當的模式解決,當你瞭解了這些,完全可以把這些解決方案加以轉換,用來解決其他領域的設計問題。去年黃老大在一個GEF項目結束後,仍然沒有放棄對它的繼續研究,現在甚至利用業餘時間開發了基於GEF的SWT/JFace增強軟體包,Eclipse和GEF的魅力可見一斑。我相信在未來的兩年裡,由於RCP/GEF等技術的成熟,Java Standalone應用程式必將有所發展,在B/S模式難以實現的那部分領域裡扮演重要的角色。
本篇的主題是實現菜單功能,由於Eclipse的可擴充設計,在GEF應用程式中添加菜單要多幾處考慮,所以我首先介紹Eclipse裡關於菜單的一些概念,然後再通過執行個體描述如何在GEF裡添加菜單、工具條和操作功能表。
我們知道,Eclipse本身只是一個平台(Platform),使用者並不能直接用它來工作,它的作用是為那些提供實際功能的組件提供一個基礎環境,所有組件都通過平台指定的方式構造介面和使用資源。在Eclipse裡,這些組件被稱為外掛程式(Plugins),例如Java開發環境(JDT)、Ant支援、CVS用戶端和協助系統等等都是外掛程式,由於我們從eclipse.org下載的Eclipse本身已經包含了這些常用外掛程式,所以不需要額外的安裝,就好象Windows本身已經包含了記事本、畫圖等等工具一樣。如果我們需要新功能,就要通過下載安裝或線上更新的方式把它們安裝到Eclipse平台上,常見的如XML編輯器、Properties檔案編輯器,J2EE開發支援等等,包括GEF開發包也是這類外掛程式。外掛程式一般都安裝在Eclipse安裝目錄的plugins子目錄下,也可以使用link方式安裝在其他位置。
Eclipse平台的一個優秀之處在於,如此眾多的外掛程式能夠完美的整合在同一個環境中,要知道,每個外掛程式都可能具有編輯器、視圖、菜單、工具條、檔案關聯等等複雜元素,要讓它們能夠和平共處可不是件容易事。為此,Eclipse提供了一系列機制來解決由此帶來的各種問題。由於篇幅限制,這裡只能簡單講一下菜單和工具條的部分,更多內容請參考Eclipse隨機提供的外掛程式開發協助文檔。
大多數情況下,我們說開發一個基於Eclipse的應用程式就是指開發一個Eclipse外掛程式(plugin),Eclipse裡的每個外掛程式都有一個名為plugin.xml的檔案用來定義外掛程式裡的各種元素,例如這個外掛程式都有哪些編輯器,哪些視圖等等。在視圖中使用菜單和工具條請參考以前的貼子,本篇只介紹編輯器的情況,因為GEF應用程式大多數是基於編輯器的。
圖1 Eclipse平台的幾個組成部分
首先要介紹Retarget Action的概念,這是一種具有一定語義但沒有實際功能的Action,它唯一的作用就是在主菜單條或主工具條上佔據一個項位置,編輯器可以將具有實際功能的Action映射到某個Retarget Action,當這個編輯器被啟用時,主菜單/工具條上的那個Retarget Action就會具有那個Action的功能。舉例來說,Eclipse提供了IWorkbenchActionConstants.COPY這個Retarget Action,它的文字和表徵圖都是預先定義好的,假設我們的編輯器需要一個"複製節點到剪貼簿"功能,因為"複製節點"和"複製"這兩個詞的語義十分相近,所以可以建立一個具有實際功能的CopyNodeAction(extends Action),然後在適當的位置調用下面代碼實現二者的映射:
IActionBars.setGlobalActionHandler(IWorkbenchActionConstants.COPY,copyNodeAction);
當這個編輯器被啟用時,Eclipse會檢查到這個映射,讓COPY項變為可用狀態,並且當使用者按下它時去執行CopyNodeAction裡定義的操作,即run()方法裡的代碼。Eclipse引入Retarget Action的目的是為了盡量減少主菜單/工具條的重建消耗,並且有利於使用者使用上的一致性。在GEF應用程式裡,因為很可能存在多個視圖(例如編輯檢視和大綱視圖,即使暫時只有一個視圖,也要考慮到以後擴充為多個的可能),而每個視圖都應該能夠完成相類似的操作,例如在樹結構的大綱視圖裡也應該像編輯檢視一樣可以刪除選中節點,所以一般的操作都應以映射到Retarget Action的方式建立。
主菜單/主工具條
與視圖視窗不同,編輯器沒有自己的功能表列和工具條,它的菜單只能加在主菜單裡。由於一個編輯器可以有多個執行個體,而它們應當具有相同的菜單和工具條,所以在plugin.xml裡定義一個編輯器的時候,元素有一個contributorClass屬性,它的值是一個實現IEditorActionBarContributor介面的類的全名,該類可以稱為"菜單工具條添加器"。在添加器裡可以向Eclipse的主菜單/主工具條裡添加自己需要的項。還是以我們這個項目為例,它要求對每個操作可以撤消/重做,對畫布上的每個元素可以刪除,對每個節點元素可以設定它的優先順序為高、中、低三個等級。所以我們要添加這六個Retarget Action,以下就是DiagramActionBarContributor類的部分代碼:
public class DiagramActionBarContributor extends ActionBarContributor { protected void buildActions() { addRetargetAction(new UndoRetargetAction()); addRetargetAction(new RedoRetargetAction()); addRetargetAction(new DeleteRetargetAction()); addRetargetAction(new PriorityRetargetAction(IConstants.PRIORITY_HIGH)); addRetargetAction(new PriorityRetargetAction(IConstants.PRIORITY_MEDIUM)); addRetargetAction(new PriorityRetargetAction(IConstants.PRIORITY_LOW)); } protected void declareGlobalActionKeys() { } public void contributeToToolBar(IToolBarManager toolBarManager) { …… } public void contributeToMenu(IMenuManager menuManager) { IMenuManager mgr=new MenuManager("&Node","Node"); menuManager.insertAfter(IWorkbenchActionConstants.M_EDIT,mgr); mgr.add(getAction(IConstants.ACTION_MARK_PRIORITY_HIGH)); mgr.add(getAction(IConstants.ACTION_MARK_PRIORITY_MEDIUM)); mgr.add(getAction(IConstants.ACTION_MARK_PRIORITY_LOW)); }}
可以看到,DiagramActionBarContributor類繼承自GEF提供的類ActionBarContributor,後者是實現了IEditorActionBarContributor介面的一個抽象類別。buildActions()方法用於建立那些要添加到主菜單/工具條的Retarget Actions,並把它們註冊到一個專門的註冊表裡;而contributeToMenu()方法裡的代碼把這些Retarget Actions實際添加到主功能表列,使用IMenuManager.insertAfter()是為了讓新加的菜單出現在指定的系統功能表後面,contributeToToolBar()裡則是添加到主工具條的代碼。
圖2 添加到主菜單條和主工具條上的Action
GEF 在ActionBarContributor裡維護了retargetActions和globalActionKeys兩個列表,其中後者是一個Retarget Actions的ID列表,addRetargetAction()方法會把一個Retarget Action同時加到二者中,對於已有的Retarget Actions,我們應該在declareGlobalActionKeys()方法裡調用addGlobalActionKey()方法來聲明,在一個編輯器被啟用的時候,與globalActionKeys裡的那些ID具有相同ID值的(具有實際功能的)Action將被聯絡到該ID對應的Retarget Action,因此就不需要顯式的去調用setGlobalActionHandler()方法了,只要保證二者的ID相同即可實現映射。
GEF已經內建了撤消/重做和刪除這三個操作的Retarget Action(因為太常用了),它們的ID分別是IWorkbenchActionConstants.UNDO、REDO和DELETE,所以沒有什麼問題。而設定優先權這個Action沒有語義相近的現成Retarget Action可用,所以我們自己要先定義一個PriorityRetargetAction,內容如下(沒有經過國際化處理):
public class PriorityRetargetAction extends LabelRetargetAction{ public PriorityRetargetAction(int priority) { super(null,null); switch (priority) { case IConstants.PRIORITY_HIGH: setId(IConstants.ACTION_MARK_PRIORITY_HIGH); setText("High Priority"); break; case IConstants.PRIORITY_MEDIUM: setId(IConstants.ACTION_MARK_PRIORITY_MEDIUM); setText("Medium Priority"); break; case IConstants.PRIORITY_LOW: setId(IConstants.ACTION_MARK_PRIORITY_LOW); setText("Low Priority"); break; default: break; } }}
接下來要在編輯器(CbmEditor)的createActions()裡建立具有實際功能的Actions,它們應該是SelectionAction(GEF提供)的子類,因為我們需要得到當前選中的節點。稍後將給出PriorityAction的代碼,編輯器的createActions()方法的代碼如下所示:
protected void createActions() {super.createActions(); //高優先順序 IAction action=new PriorityAction(this, IConstants.PRIORITY_HIGH); action.setId(IConstants.ACTION_MARK_PRIORITY_HIGH); getActionRegistry().registerAction(action); getSelectionActions().add(action.getId()); //中等優先順序 action=new PriorityAction(this, IConstants.PRIORITY_MEDIUM); action.setId(IConstants.ACTION_MARK_PRIORITY_MEDIUM); getActionRegistry().registerAction(action); getSelectionActions().add(action.getId()); //低優先順序 action=new PriorityAction(this, IConstants.PRIORITY_LOW); action.setId(IConstants.ACTION_MARK_PRIORITY_LOW); getActionRegistry().registerAction(action); getSelectionActions().add(action.getId());}
請再次注意在這個方法裡每個Action的id都與前面建立的Retarget Action的ID對應,否則將無法對應到主菜單條和主工具條中的Retarget Actions。你可能已經發現了,這裡我們只建立了設定優先權的三個Action,而沒有建立負責撤消/重做和刪除的Action。其實GEF在這個類的父類(GraphicalEditor)裡已經建立了這些常用Action,包括撤消/重做、全選、儲存、列印等,所以只要別忘記調用super.createActions()就可以了。
GEF提供的UNDO/REDO/DELETE等Action會根據當前選擇的editpart(s)自動判斷自己是否可用,我們定義的Action則要自己在Action的calculateEnabled()方法裡計算。另外,為了實現撤消/重做的功能,一般Action執行的時候要建立一個Command,將後者加入CommandStack裡,然後執行這個Command對象,而不是直接把執行代碼寫在Action的run()方法裡。下面是我們的設定優先權PriorityAction的部分代碼,該類繼承自SelectionAction:
public void run() { execute(createCommand());}private Command createCommand() { List objects = getSelectedObjects(); if (objects.isEmpty()) return null; for (Iterator iter = objects.iterator(); iter.hasNext();) { Object obj = iter.next(); if ((!(obj instanceof NodePart)) && (!(obj instanceof NodeTreeEditPart))) return null; } CompoundCommand compoundCmd = new CompoundCommand(GEFMessages.DeleteAction_ActionDeleteCommandName); for (int i = 0; i < objects.size(); i++) { EditPart object = (EditPart) objects.get(i); ChangePriorityCommand cmd = new ChangePriorityCommand(); cmd.setNode((Node) object.getModel()); cmd.setNewPriority(priority); compoundCmd.add(cmd); } return compoundCmd;}protected boolean calculateEnabled() { Command cmd = createCommand(); if (cmd == null) return false; return cmd.canExecute();}
因為允許使用者一次對多個選中的節點設定優先權,所以在這個Action裡我們建立了多個Command對象,並把它們加到一個CompoundCommand對象裡,好處是在撤消/重做的時候也可以一次性完成,而不是一個節點一個節點的來。
操作功能表
在GEF裡實現右鍵彈出的操作功能表是很方便的,只要寫一個繼承org.eclipse.gef. ContextMenuProvider的自訂類,在它的buildContextMenu()方法裡編寫添加功能表項目的代碼,然後在編輯器裡調用GraphicalViewer. SetContextMenu()即可。GEF為我們預先定義了一些菜單組(Group)用來區分不同用途的功能表項目,每個組在外觀上表現為一條分隔線,例如有UNDO組、COPY組和PRINT組等等。如果你的功能表項目不適合放在任何一個組中,可以放在OTHERS組裡,當然如果你的功能表項目很多,也可以定義新的組用來分類。
圖3 操作功能表
假設我們要實現如所示的操作功能表,並且已經建立並在ActionRegistry裡了這些Action(在Editor的createActions()方法裡完成),ContextMenuProvider應該像下面這樣寫:
public class CbmEditorContextMenuProvider extends ContextMenuProvider { private ActionRegistry actionRegistry; public CbmEditorContextMenuProvider(EditPartViewer viewer, ActionRegistry registry) { super(viewer); actionRegistry = registry; } public void buildContextMenu(IMenuManager menu) { // Add standard action groups to the menu GEFActionConstants.addStandardActionGroups(menu); // Add actions to the menu menu.appendToGroup(GEFActionConstants.GROUP_UNDO,getAction(ActionFactory.UNDO.getId())); menu.appendToGroup(GEFActionConstants.GROUP_UNDO, getAction(ActionFactory.REDO.getId())); menu.appendToGroup(GEFActionConstants.GROUP_EDIT, getAction(ActionFactory.DELETE.getId())); menu.appendToGroup(GEFActionConstants.GROUP_REST,getAction(IConstants.ACTION_MARK_PRIORITY_HIGH)); menu.appendToGroup(GEFActionConstants.GROUP_REST,getAction(IConstants.ACTION_MARK_PRIORITY_MEDIUM)); menu.appendToGroup(GEFActionConstants.GROUP_REST,getAction(IConstants.ACTION_MARK_PRIORITY_LOW)); } private IAction getAction(String actionId) { return actionRegistry.getAction(actionId); }}
注意buildContextMenu()方法裡的第一句是建立預設的那些組,如果沒有忽略了這一步後面的語句會提示組不存在的錯誤,你也可以通過這個方法看到GEF是怎樣建組的以及都有哪些組。讓編輯器使用這個類的代碼一般寫在configureGraphicalViewer()方法裡。
因為順便介紹了Eclipse的一些基本概念,加上代碼比較多,所以這篇貼子看起來比較長,其實通過查看GEF對內建的UNDO/REDO等的實現很容易就會明白菜單的使用方法。