我发现一旦稍稍体会到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等的实现很容易就会明白菜单的使用方法。