辅助网格
你可能用过一些这样的应用程序,画布里可以显示一个灰色的网格帮助定位你的图形元素,当被拖动的节点接近网格线条时会被"吸附"到网格上,这样可以很容易的把画布上的图形元素排列整齐,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类的回应,像下面这样:
public Object getAdapter(Class adapter) {
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提供的专门用于显示标尺的组件,具体的改变方法如下:
//定义一个RulerComposite类型的变量
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_VERTICAL_RULER,new SubjectRulerProvider(diagram.getLeftRuler()));
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(),分别返回创建辅助线、删除辅助线和移动辅助线的命令,下面列出创建辅助线的命令,其他两个的实现方式是类似的,你可以在本文所附例子中找到它们的代码:
public class CreateGuideCommand extends Command {
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实现):
public void notifyChanged(Notification notification) {
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监听器的主要代码:
public void notifyChanged(Notification notification) {
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构造方法如下所示,它的主要工作就是增加监听器:
public SubjectRulerProvider(Ruler ruler) {
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()返回标尺上所有辅助线位置构成的整数数组。以下是本例中的实现方式:
public Object getRuler() {
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列表里以响应选择事件:
IAction action=new AlignmentAction((IWorkbenchPart)this,PositionConstants.LEFT);
getActionRegistry().registerAction(action);
getSelectionActions().add(action.getId());
…
AlignmentAction的构造方法的参数是编辑器本身和一个代表对齐方式的整数,后者可以是 PositionConstants.LEFT、CENTER、RIGHT、TOP、MIDDLE、BOTTOM中的一个; MatchSizeAction有两个子类,MatchWidthAction和MatchHeightAction,你可以使用它们达到只调整宽度或高度的目的。下图是添加在工具条中的按钮,左边六个为位置对齐,最后两个为尺寸对齐,请注意,当选择多个图形时,被六个黑点包围的那个称为"主选择",对齐时以该图形所在位置和大小为准做调整。
代码:getGraphicalViewer().setProperty( SnapToGeometry.PROPERTY_SNAP_ENABLED, new Boolean(true));
Hi Cherie,
Thank you that you opened a bug for me.
I'don't have much time to look into this right now, but I'll submit a patch when I find the problem.
As for my first question (on how to integrate SnapToGeometry into a GMF generated diagram, here's the solution (in case anybody's interested):
1.Call the following line from your Editor
getGraphicalViewer().setProperty( SnapToGeometry.PROPERTY_SNAP_ENABLED, new Boolean(true));
2. Install a new EditPolicy in your root-EditPart
installEditPolicy(EditPolicyRoles.SNAP_FEEDBACK_ROLE, new SnapFeedbackPolicy());3. Return the correct Adapter in the root-EditPart
@Override
public Object getAdapter(Class key) {
if (key == SnapToHelper.class){
return new SnapToGeometry(this);
}
return super.getAdapter(key);
}
It's really simple to do and very nice to have.