當一個GEF應用程序實現了大部分必需的業務功能後,為了能讓用戶使用得更方便,我們 應該在易用性方面做些考慮。從3.0版本開始, GEF增加了更多這方面的新特性,開發人員很 容易利用它們來改善自己的應用程序界面。這篇帖子將介紹主要的幾個功能,它們有些在GEF 2.1中就出現了,但因為都是關於易用性的而且以前沒有提到,所以放在這裡一起來說。
可折疊調色板
在以前的例子裡,我們的編輯器都繼承自GraphicalEditorWithPalette。GEF 3.0提供了 一個功能更加豐富的編輯器父類:GraphicalEditorWithFlyoutPalette,繼承它的編輯器具 有一個可以折疊的工具條,並且能夠利用Eclipse自帶的調色板視圖,當調色板視圖顯示時, 工具條會自動轉移到這個視圖中。
圖1 可折疊和配置的調色板
與以前的GraphicalEditorWithPalette相比,繼承 GraphicalEditorWithFlyoutPalette 的編輯器要多做一些工作。首先要實現getPalettePreferences() 方法,它返回一個 FlyoutPreferences實例,作用是把調色板的幾個狀態信息(位置、大小和是否展開)保存起 來,這樣下次打開編輯器的時候就可以自動套用這些設置。下面使用偏好設置的方式保存和 載入這些狀態,你也可以使用其他方法,比如保存為.properties文件:
protected FlyoutPreferences getPalettePreferences() {
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 ()方法裡完成的,而現 在這個方法已經不需要了:
protected PaletteViewerProvider createPaletteViewerProvider() {
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比較方便,請看下面的代碼:
protected void configureGraphicalViewer() {
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() 裡添加它們到工具條(原理請參考前面關於菜單和工具條的 帖子):
protected void buildActions() {
//其他命令
…
//縮放命令
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()方法,增加對它的支持:
public Object getAdapter(Class type) {
…
if (type == ZoomManager.class)
return getGraphicalViewer().getProperty(ZoomManager.class.toString());
return super.getAdapter(type);
}
現在,打開編輯器後主工具條中將出現下圖所示的兩個按鈕和一個下拉框:
圖2 縮放工具條
有時候我們想讓程序把用戶當前的縮放值記錄下來,以便下次打開時顯示同樣的比例。這 就須要在畫布模型裡增加一個zoom變量,在編輯器的初始化過程中增加下面的語句,其中 diagram是我們的畫布實例:
ZoomManager manager = (ZoomManager) getGraphicalViewer().getProperty (ZoomManager.class.toString());
if (manager != null)
manager.setZoom(diagram.getZoom());
在保存模型前得到當前的縮放比例放在畫布模型裡一起保存:
ZoomManager manager = (ZoomManager) getGraphicalViewer().getProperty (ZoomManager.class.toString());
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類的回應,像下面這樣:
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,你可以使用它們達 到只調整寬度或高度的目的。下圖是添加在工具條中的按鈕,左邊六個為位置對齊,最後兩 個為尺寸對齊,請注意,當選擇多個圖形時,被六個黑點包圍的那個稱為"主選擇",對齊時 以該圖形所在位置和大小為准做調整。
圖7 位置對齊和尺寸對齊
本文配套源碼