[Eclipse]GEF入门系列(九、增加易用性)
當一個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 位置對齊和尺寸對齊
總結
以上是生活随笔為你收集整理的[Eclipse]GEF入门系列(九、增加易用性)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 愚人节的欢乐
- 下一篇: .Net中删除数据前进行外键冲突检测