[Eclipse]GEF入门系列(九、增加易用性)
当一个GEF应用程序实现了大部分必需的业务功能后,为了能让用户使用得更方便,我们应该在易用性方面做些考虑。从3.0版本开始, GEF增加了更多这方面的新特性,开发人员很容易利用它们来改善自己的应用程序界面。这篇帖子将介绍主要的几个功能,它们有些在GEF 2.1中就出现了,但因为都是关于易用性的而且以前没有提到,所以放在这里一起来说。( 下载示例代码)
可折叠调色板
在以前的例子里,我们的编辑器都继承自GraphicalEditorWithPalette。GEF 3.0提供了一个功能更加丰富的编辑器父类:GraphicalEditorWithFlyoutPalette,继承它的编辑器具有一个可以折叠的工具条,并且能够利用Eclipse自带的调色板视图,当调色板视图显示时,工具条会自动转移到这个视图中。
图1 可折叠和配置的调色板
与以前的GraphicalEditorWithPalette相比,继承 GraphicalEditorWithFlyoutPalette的编辑器要多做一些工作。首先要实现getPalettePreferences() 方法,它返回一个FlyoutPreferences实例,作用是把调色板的几个状态信息(位置、大小和是否展开)保存起来,这样下次打开编辑器的时候就可以自动套用这些设置。下面使用偏好设置的方式保存和载入这些状态,你也可以使用其他方法,比如保存为.properties文件:
return new FlyoutPreferences() {
public int getDockLocation() {
return SubjectEditorPlugin.getDefault().getPreferenceStore().getInt(IConstants.PREF_PALETTE_DOCK_LOCATION);
}
public void setDockLocation(int location) {
SubjectEditorPlugin.getDefault().getPreferenceStore().setValue(IConstants.PREF_PALETTE_DOCK_LOCATION,location);
}
…
};
}
然后要覆盖缺省的createPaletteViewerProvider()实现,在这里为调色板增加拖放支持,即指定调色板为拖放源(之所以用这样的方式,原因是在编辑器里没有办法得到它对应的调色板实例),在以前这个工作通常是在initializePaletteViewer ()方法里完成的,而现在这个方法已经不需要了:
return new PaletteViewerProvider(getEditDomain()) {
protected void configurePaletteViewer(PaletteViewer viewer) {
super.configurePaletteViewer(viewer);
viewer.addDragSourceListener(new TemplateTransferDragSourceListener(viewer));
}
};
}
GEF 3.0还允许用户对调色板里的各种工具进行定制,例如隐藏某个工具,或是修改工具的描述等等,这是通过给PaletteViewer定义一个 PaletteCustomizer实例实现的,但由于时间关系,这里暂时不详细介绍了,如果需要这项功能你可以参考Logic例子中的实现方法。
缩放
由于Draw2D中的图形都具有天然的缩放功能,因此在GEF里实现缩放功能是很容易的,而且缩放的效果不错。GEF为我们提供了 ZoomInAction和ZoomOutAction以及对应的RetargetAction(ZoomInRetargetAction和 ZoomOutRetargetAction),只要在编辑器里构造它们的实例,然后在编辑器的ActionBarContributer类里将它们添加到想要的菜单或工具条位置即可。因为ZoomInAction和ZoomOutAction的构造方法要求一个ZoomManager类型的参数,而后者需要从GEF的RootEditPart中获得(ScalableRootEditPart或 ScalableFreeformRootEditPart),所以最好在编辑器的 configureGraphicalViewer()里构造这两个Action比较方便,请看下面的代码:
super.configureGraphicalViewer();
ScalableFreeformRootEditPart root = new ScalableFreeformRootEditPart();
getGraphicalViewer().setRootEditPart(root);
getGraphicalViewer().setEditPartFactory(new PartFactory());
action = new ZoomInAction(root.getZoomManager());
getActionRegistry().registerAction(action);
getSite().getKeyBindingService().registerAction(action);
action = new ZoomOutAction(root.getZoomManager());
getActionRegistry().registerAction(action);
getSite().getKeyBindingService().registerAction(action);
}
假设我们想把这两个命令添加到主工具条上,在DiagramActionBarContributor里应该做两件事:在 buildActions()里构造对应的RetargetAction,然后在contributeToToolBar()里添加它们到工具条(原理请参考前面关于菜单和工具条的 帖子):
//其他命令
…
//缩放命令
addRetargetAction(new ZoomInRetargetAction());
addRetargetAction(new ZoomOutRetargetAction());
}
public void contributeToToolBar(IToolBarManager toolBarManager) {
//工具条中的其他按钮
…
//缩放按钮
toolBarManager.add(getAction(GEFActionConstants.ZOOM_IN));
toolBarManager.add(getAction(GEFActionConstants.ZOOM_OUT));
toolBarManager.add(new ZoomComboContributionItem(getPage()));
}
请注意,在contributeToToolBar()方法里我们额外添加了一个ZoomComboContributionItem 的实例,这个类也是GEF提供的,它的作用是显示一个缩放百分比的下拉框,用户可以选择或输入想要的数值。为了让这个下拉框能与编辑器联系在一起,我们要修改一下编辑器的getAdapter()方法,增加对它的支持:
…
if (type == ZoomManager.class)
return getGraphicalViewer().getProperty(ZoomManager.class.toString());
return super.getAdapter(type);
}
现在,打开编辑器后主工具条中将出现下图所示的两个按钮和一个下拉框:
图2 缩放工具条
有时候我们想让程序把用户当前的缩放值记录下来,以便下次打开时显示同样的比例。这就须要在画布模型里增加一个zoom变量,在编辑器的初始化过程中增加下面的语句,其中diagram是我们的画布实例:
if (manager != null)
manager.setZoom(diagram.getZoom());
在保存模型前得到当前的缩放比例放在画布模型里一起保存:
if (manager != null)
diagram.setZoom(manager.getZoom());
辅助网格
你可能用过一些这样的应用程序,画布里可以显示一个灰色的网格帮助定位你的图形元素,当被拖动的节点接近网格线条时会被"吸附"到网格上,这样可以很容易的把画布上的图形元素排列整齐,GEF 3.0里就提供了显示这种辅助网格的功能。
图3 辅助编辑网格
是否显示网格以及是否打开吸附功能是由GraphicalViewer的两个布尔类型的属性(property)值决定的,它们分别是 SnapToGrid.PROPERTY_GRID_VISIBLE和SnapToGrid.PROPERTY_GRID_ENABLED,这些属性是通过GriaphicalViewer.getProperty()和setProperty()方法来操作的。GEF为我们提供了一个 ToggleGridAction用来同时切换它们的值(保持这两个值同步确实符合一般使用习惯),但没有像缩放功能那样提供对应的 RetargetAction,不知道GEF是出于什么考虑。另外因为这个Action没有预先设置的图标,所以把它直接添加到工具条上会很不好看,所以要么把它只放在菜单中,要么为它设置一个图标,至于添加到菜单的方法这里不赘述了。
要想在保存模型时同时记录当前网格线是否显示,必须在画布模型里增加一个布尔类型变量,并在打开模型和保存模型的方法中增加处理它的代码。
几何对齐
这个功能也是为了方便用户排列图形元素的,如果打开了此功能,当用户拖动的图形有某个边靠近另一图形的某个平行边延长线时,会自动吸附到这条延长线上;若两个图形的中心线(通过图形中心点的水平或垂直线)平行靠近时也会产生吸附效果。例如下图中,Subject1的左边与 Subject2的右边是吸附在一起的,Subject3原本是与Subject2水平中心线吸附的,而用户在拖动的过程中它的上边吸附到 Subject1的底边。
图4 几何对齐
几何对齐也是通过GraphicalViewer的属性来控制是否打开的,属性的名称是 SnapToGeometry.PROPERTY_SNAP_ENABLED,值为布尔类型。在程序里增加吸附对齐切换的功能和前面说的增加网格切换功能基本是一样的,记住GEF为它提供的Action是ToggleSnapToGeometryAction。
要实现对齐功能,还有一个重要的步骤,那就是在画布所对应的EditPart的getAdapter()方法里增加对 SnapToHelper类的回应,像下面这样:
if (adapter == SnapToHelper.class) {
List snapStrategies = new ArrayList();
Boolean val = (Boolean)getViewer().getProperty(RulerProvider.PROPERTY_RULER_VISIBILITY);
if (val != null && val.booleanValue())
snapStrategies.add(new SnapToGuides(this));
val = (Boolean)getViewer().getProperty(SnapToGeometry.PROPERTY_SNAP_ENABLED);
if (val != null && val.booleanValue())
snapStrategies.add(new SnapToGeometry(this));
val = (Boolean)getViewer().getProperty(SnapToGrid.PROPERTY_GRID_ENABLED);
if (val != null && val.booleanValue())
snapStrategies.add(new SnapToGrid(this));
if (snapStrategies.size() == 0)
return null;
if (snapStrategies.size() == 1)
return (SnapToHelper)snapStrategies.get(0);
SnapToHelper ss[] = new SnapToHelper[snapStrategies.size()];
for (int i = 0; i < snapStrategies.size(); i++)
ss[i] = (SnapToHelper)snapStrategies.get(i);
return new CompoundSnapToHelper(ss);
}
return super.getAdapter(adapter);
}
标尺和辅助线
标尺位于画布的上部和左侧,在每个标尺上可以建立很多与标尺垂直的辅助线,这些显示在画布上的虚线具有吸附功能。
图5 标尺和辅助线
标尺和辅助线的实现要稍微复杂一些。首先要修改原有的模型,新增加标尺和辅助线这两个类,它们之间的关系请看下图:< /p>
图6 增加标尺和辅助线后的模型
与上篇帖子里的 模型图比较后可以发现,在Diagram类里增加了四个变量,其中除rulerVisibility以外三个的作用都在前面部分做过介绍,而rulerVisibility和它们类似,作用记录标尺的可见性,当然只有在标尺可见的时候辅助线才是可见的。我们新增了Ruler和 Guide两个类,前者表示标尺,后者表示辅助线。因为辅助线是建立在标尺上的,所以Ruler到Guide有一个包含关系(黑色菱形);画布上有两个标尺,分别用topRuler和leftRuler这两个变量引用,也是包含关系,也就是说,画布上只能同时具有这两个标尺;Node到Guide有两个引用,表示Node吸附到的两条辅助线(为了简单起见,在本文附的例子中并没有实际使用到它们,Guide类中定义的几个方法也没有用到)。Guide类里的map变量用来记录吸附在自己上的节点和对应的吸附边。要让画布上能够显示标尺,首先要将原先的GraphicalViewer改放在一个 RulerComposite实例上(而不是直接放在编辑器上),后者是GEF提供的专门用于显示标尺的组件,具体的改变方法如下:
private RulerComposite rulerComp;
//创建RulerComposite,并把GraphicalViewer创建在其上< span style="color: #008000;">
protected void createGraphicalViewer(Composite parent) {
rulerComp = new RulerComposite(parent, SWT.NONE);
super.createGraphicalViewer(rulerComp);
rulerComp.setGraphicalViewer((ScrollingGraphicalViewer) getGraphicalViewer());
}
//覆盖getGraphicalControl返回RulerComposite实例< span style="color: #008000;">
protected Control getGraphicalControl() {
return rulerComp;
}
然后,要设置GraphicalViewer的几个有关属性,如下所示,其中前两个分别表示左侧和上方的标尺,而最后一个表示标尺的可见性:
getGraphicalViewer().setProperty(RulerProvider.PROPERTY_HORIZONTAL_RULER,new SubjectRulerProvider(diagram.getTopRuler()));
getGraphicalViewer().setProperty(RulerProvider.PROPERTY_RULER_VISIBILITY,new Boolean(diagram.isRulerVisibility()));
在前两个方法里用到了SubjectRulerProvider这个类,它是我们从RulerProvider类继承过来的, RulerProvider是一个比较特殊的类,其作用有点像EditPolicy,不过除了一些getXXXCommand()方法以外,还有其他几个方法要实现。需要返回Command的方法包括:getCreateGuideCommand()、getDeleteGuideCommand()和 getMoveGuideCommand(),分别返回创建辅助线、删除辅助线和移动辅助线的命令,下面列出创建辅助线的命令,其他两个的实现方式是类似的,你可以在本文所附例子中找到它们的代码:
private Guide guide;
private Ruler ruler;
private int position;
public CreateGuideCommand(Ruler parent, int position) {
setLabel("Create Guide");
this.ruler = parent;
this.position = position;
}
public void execute() {
guide = ModelFactory.eINSTANCE.createGuide();//创建一条新的辅助线
guide.setHorizontal(!ruler.isHorizontal());
guide.setPosition(position);
ruler.getGuides().add(guide);
}
public void undo() {
ruler.getGuides().remove(guide);
}
}
接下来再看看RulerProvider的其他方法,SubjectRulerProvider维护一个Ruler对象,在构造方法里要把它的值传入。此外,在构造方法里还应该给Ruler和Guide模型对象增加监听器用来响应标尺和辅助线的变化,下面是Ruler监听器的主要代码(因为使用了EMF作为模型,所以监听器实现为Adapter。如果你不用EMF,可以使用PropertyChangeListener实现):
switch (notification.getFeatureID(ModelPackage.class)) {
case ModelPackage.RULER__UNIT:
for (int i = 0; i < listeners.size(); i++)
((RulerChangeListener) listeners.get(i)).notifyUnitsChanged(ruler.getUnit());
break;
case ModelPackage.RULER__GUIDES:
Guide guide = (Guide) notification.getNewValue();
if (getGuides().contains(guide))
guide.eAdapters().add(guideAdapter);
else
guide.eAdapters().remove(guideAdapter);
for (int i = 0; i < listeners.size(); i++)
((RulerChangeListener) listeners.get(i)).notifyGuideReparented(guide);
break;
}
}
可以看到监听器在被触发时所做的工作实际上是触发这个RulerProvider的监听器列表(listeners)里的所有监听器,而这些监听器就是RulerEditPart或GuideEditPart,而我们不需要去关心这两个类。Ruler的事件有两种,一是单位(象素、厘米、英寸)改变,二是创建辅助线,在创建辅助线的情况要给这个辅助线增加监听器。下面是Guide监听器的主要代码:
Guide guide = (Guide) notification.getNotifier();
switch (notification.getFeatureID(ModelPackage.class)) {
case ModelPackage.GUIDE__POSITION:
for (int i = 0; i < listeners.size(); i++)
((RulerChangeListener) listeners.get(i)).notifyGuideMoved(guide);
break;
case ModelPackage.GUIDE__MAP:
for (int i = 0; i < listeners.size(); i++)
((RulerChangeListener) listeners.get(i)).notifyPartAttachmentChanged(notification.getNewValue(),guide);
break;
}
}
Guide监听器也有两种事件,一是辅助线位置改变,二是辅助线上吸附的图形的增减变化。请注意,这里的循环一定不要用 iterator的方式,而应该用上面列出的下标方式,否则会出现ConcurrentModificationException异常,原因和 RulerProvider的notifyXXX()实现有关。我们的SubjectRulerProvider构造方法如下所示,它的主要工作就是增加监听器:
this.ruler = ruler;
ruler.eAdapters().add(rulerAdapter);
//载入模型的情况下,ruler可能已经包含一些guides,所以要给它们增加监听器< span style="color: #008000;">
for (Iterator iter = ruler.getGuides().iterator(); iter.hasNext();) {
Guide guide = (Guide) iter.next();
guide.eAdapters().add(guideAdapter);
}
}
在RulerProvider里还有几个方法要实现才能正确使用标尺:getRuler()返回RulerProvider维护的 Ruler实例,getGuides()返回辅助线列表,getGuidePosition(Object)返回某条辅助线在标尺上的位置(以pixel 为单位),getPositions()返回标尺上所有辅助线位置构成的整数数组。以下是本例中的实现方式:
return ruler;
}
public List getGuides() {
return ruler.getGuides();
}
public int[] getGuidePositions() {
List guides = getGuides();
int[] result = new int[guides.size()];
for (int i = 0; i < guides.size(); i++) {
result[i] = ((Guide) guides.get(i)).getPosition();
}
return result;
}
public int getGuidePosition(Object arg0) {
return ((Guide) arg0).getPosition();
}
有了这个自定义的RulerProvider类,再通过把该类的两个实例被放在GraphicalViewer的两个属性(PROPERTY_VERTICAL_RULER和PROPERTY_HORIZONTAL_RULER)中,画布就具有标尺的功能了。GEF提供了用于切换标尺可见性的命令:ToggleRulerVisibilityAction,我们使用和前面同样的方法把它加到主菜单即可控制显示或隐藏标尺和辅助线。
位置和尺寸对齐
图形编辑工具大多具有这样的功能:选中两个以上图形,再按一下按钮就可以让它们以某一个边或中心线对齐,或是调整它们为同样的宽度高度。GEF提供AlignmentAction和MatchSizeAction分别用来实现位置对齐和尺寸对齐,使用方法很简单,在编辑器的 createActions()方法里构造需要的对齐方式Action(例如对齐到上边、下边等等),然后在编辑器的 ActionBarContributor里通过这些Action对应的RetargetAction将它们添加到菜单或工具条即可。编辑器里的代码如下,注意最后一句的作用是把它们加到selectionAction列表里以响应选择事件:
getActionRegistry().registerAction(action);
getSelectionActions().add(action.getId());
…
AlignmentAction的构造方法的参数是编辑器本身和一个代表对齐方式的整数,后者可以是 PositionConstants.LEFT、CENTER、RIGHT、TOP、MIDDLE、BOTTOM中的一个; MatchSizeAction有两个子类,MatchWidthAction和MatchHeightAction,你可以使用它们达到只调整宽度或高度的目的。下图是添加在工具条中的按钮,左边六个为位置对齐,最后两个为尺寸对齐,请注意,当选择多个图形时,被六个黑点包围的那个称为"主选择",对齐时以该图形所在位置和大小为准做调整。
图7 位置对齐和尺寸对齐