我發現一旦稍稍體會到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等的實現很容易就會明白菜單的使用方法。