繪圖編輯框架(GEF)被設計用來(lái)以圖形而不是文本的方式來(lái)編輯用戶(hù)數據,一般被稱(chēng)為模型(model)。當處理包含多對多,一對多以及其他復雜關(guān)系的實(shí)體時(shí),GEF是一種很有價(jià)值的工具。隨著(zhù)Eclipse Rich Client Platform 的流行,使得編輯器的開(kāi)發(fā)不僅僅局限于編程,GEF的重要性也與日俱增。比如說(shuō),數據庫schema編輯器 [7],邏輯電路編輯器和任務(wù)流管理器,這些例子都很好地展示了GEF是一種可以用于各個(gè)不同領(lǐng)域的,具有強大功能和靈活性的框架。
然而,任何通用框架都設計復雜,難于學(xué)習,GEF也不例外。到現在為止,最小的例子也將涉及75個(gè)類(lèi)。即使對于最勤勉的開(kāi)發(fā)者來(lái)說(shuō),要從GEF用戶(hù)定義類(lèi)型和GEF提供的上百種類(lèi)型之間相互作用來(lái)理解GEF的獨特之處,對耐心和智力的都是一種考驗。為了改變這種狀況,一個(gè)全新的,規模更小的編輯器例子被添加進(jìn)即將到來(lái)的Eclipse 3.1(譯:翻譯此文時(shí),Eclipse 3.1已經(jīng)發(fā)布)。這個(gè)幾何圖形編輯器(看圖1)允許你創(chuàng )建,編輯簡(jiǎn)單的圖。它處理兩種對象,矩形和橢圓。你可以在實(shí)線(xiàn)和虛線(xiàn)這兩種連接類(lèi)型中選擇一種來(lái)連接兩個(gè)對象。每一個(gè)連接都是有方向的,也就是說(shuō)從一個(gè)源對象開(kāi)始,在目標對象處終止。箭頭用來(lái)表示連接方向。連接可以轉移,也就是通過(guò)拖動(dòng)它的源點(diǎn)或目標點(diǎn)到一個(gè)新的對象上。編輯器中的對象可以點(diǎn)擊選中,也可以通過(guò)拖拉一個(gè)區域來(lái)選擇。選中的對象可以被刪除。所有的模型操作,比如添加,刪除對象,移動(dòng)對象,改變大小等等,都可以undo或redo。最后,編輯器集成了兩個(gè)Eclipse標準視圖Properties和Outline。這個(gè)編輯器的價(jià)值不是在于它的可用性,而是作為例子,通過(guò)有限的兩種用戶(hù)定義類(lèi)型來(lái)演示在一個(gè)成熟GEF編輯器中會(huì )碰到的大多數概念和技術(shù)。


GEF幫助你為數據構造一個(gè)可視化的編輯器。數據可以是帶有簡(jiǎn)單溫度旋鈕的溫度調節器,也可以是一個(gè)包含幾百個(gè)路由器,連接和服務(wù)質(zhì)量策略的虛擬局域網(wǎng)。幸虧GEF設計者,他們設法建立一種框架,使得它能夠和任何數據一起工作,用GEF的術(shù)語(yǔ)來(lái)說(shuō),就是任何模型(model)。這是通過(guò)嚴格遵循了模型-視圖-控制器模式(MVC)來(lái)做到的。模型就是你的數據。對于GEF,模式可以是任何普通的Java對象(POJO)。模型不應該知道任何有關(guān)于控制器或視圖的信息。視圖(view)是模型或其某一部分在屏幕上的可視化表示。它可以是矩形,線(xiàn)或橢圓這樣的簡(jiǎn)單圖形,也可以是彼此嵌套的邏輯電路。同時(shí),視圖也應該對模型和控制器一無(wú)所知。雖然任何實(shí)現IFigure接口的類(lèi)都可以作為視圖,但是GEF使用Draw2D可視圖形(figure)。控制器,可稱(chēng)為編輯部件(edit part),是模型和視圖之間的橋梁。當你開(kāi)始編輯你的模型時(shí),一個(gè)頂層的控制器被創(chuàng )建出來(lái)。如果模型由若干個(gè)片段組成,頂層控制器就會(huì )將這個(gè)信息通知GEF。接下來(lái),每個(gè)片段的子控制器被創(chuàng )建出來(lái)。如果它們又包含子片段,這個(gè)過(guò)程就會(huì )一直繼續下去,直到所有組成模型的對象都有它們的控制器??刂破鞯牧硪粋€(gè)任務(wù)是創(chuàng )建可視圖形來(lái)表示模型。一旦模型被設置到某個(gè)控制器,GEF就向控制器要合適的IFigure對象。既然模型和視圖彼此都不知道對方,控制器負責監聽(tīng)模型的修改,并更新模型的可視化表示。結果,在許多GEF編輯器中,一個(gè)常見(jiàn)的模式就是模型發(fā)PropertyChangeEvent通知。當一個(gè)編輯部件收到事件通知時(shí),它通過(guò)調整模型的外觀(guān)或結構上的表示來(lái)作相應的改變。
可視編輯的另一個(gè)方面就是對用戶(hù)動(dòng)作和鼠標,鍵盤(pán)事件作出響應。這里的挑戰在于提供一種機制,提供合理的缺省行為,并且允許重新定義行為來(lái)覆蓋缺省行為,以適應所編輯模型。比如鼠標拖動(dòng)事件,如果我們假設每次檢測到鼠標拖動(dòng)事件,所選中對象都被移動(dòng)的話(huà),我們就限制編輯器開(kāi)發(fā)者的自由。很有可能有人希望在鼠標拖動(dòng)的時(shí)候,提供放大,縮小的行為。GEF通過(guò)使用工具(tool),請求(request)和策略(policy)解決了這個(gè)問(wèn)題。
工具是一種有狀態(tài)的對象,它將象鼠標按鈕被按下,被拖動(dòng)等低層事件翻譯成高層的由Request對象表示的請求。發(fā)送哪個(gè)請求取決于所激活的工具。例如,連接工具在收到鼠標按鈕被按下這樣的事件時(shí),會(huì )發(fā)送一個(gè)連接開(kāi)始或結束的請求。如果是一個(gè)創(chuàng )建工具,我們就會(huì )收到一個(gè)創(chuàng )建請求。GEF包含了大量預定義的工具以及創(chuàng )建應用特定工具的方法。工具可以由程序控制激活,也可以在用戶(hù)實(shí)施一個(gè)動(dòng)作后激活。在大多數情況下,工具將請求發(fā)送給鼠標位置下面的圖形的EditPart。例如,如果你點(diǎn)擊一個(gè)代表widget的矩形,與此相關(guān)的編輯部件就會(huì )收到一個(gè)選中請求或者直接編輯的請求。有時(shí)候,請求會(huì )發(fā)送給區域中的所有可視圖形的編輯部件,比如MarqueeSelectionTool就是這樣。無(wú)論一個(gè)或多個(gè)編輯部件怎樣被選擇為請求目標,它們自己并不處理請求。而是將這個(gè)任務(wù)交給所注冊的編輯策略( edit policies)。每個(gè)編輯策略都會(huì )為該請求提供一個(gè)命令。不希望處理請求的策略將返回一個(gè)null。使用策略而不是編輯部件來(lái)響應請求的機制使得策略和編輯部件都盡可能短小,功能集中。同時(shí),也意味著(zhù)調試和維護代碼變得更容易。GEF的最后一個(gè)部分就是命令(command)。GEF并沒(méi)有直接修改模型,它要求你使用命令來(lái)做實(shí)際的修改。每個(gè)命令應該實(shí)現執行對模型或模型一部分的修改和撤銷(xiāo)修改。這樣,GEF編輯器自動(dòng)支持模型修改的undo/redo。
除了能夠提升你的技能以及設計模式方面的知識外,使用GEF的一個(gè)重要的優(yōu)點(diǎn)在于它能夠和Eclipse平臺完全集成在一起。在編輯器中選中的對象可以為標準Properties視圖提供屬性。Eclipse向導可以用來(lái)創(chuàng )建,初始化GEF編輯器編輯的模型。Edit菜單中的Undo和Redo可以觸發(fā)GEF編輯修改的撤銷(xiāo)和重做。簡(jiǎn)單地說(shuō),GEF編輯器實(shí)現IEditorPart接口,是Eclipse平臺中的一員,它和文本編輯器或其他workbench編輯器處于同樣的集成層次。
創(chuàng )建GEF編輯器的第一步是創(chuàng )建模型。在我們的例子里,模型由四類(lèi)對象組成:幾何圖(包含所有的圖形),兩種類(lèi)型的圖形,和圖形間的連接。在我們?yōu)檫@些類(lèi)編寫(xiě)代碼前,我們準備了一些基礎結構。
當你創(chuàng )建模型時(shí),你可以參考下面的內容:
java.beans包中的屬性修改事件通知。上面所列的規則對于所有模型都是相同的,為基本類(lèi)建立類(lèi)層次來(lái)強化這些規則是很有好處的。ModelElement類(lèi)繼承了Java的Object類(lèi),并提供了三個(gè)功能:持久化,屬性改變和屬性源支持。簡(jiǎn)單的模型持久化可以通過(guò)實(shí)現

java.io.Serializable接口以及
readObject方法來(lái)完成。這使得你可以將編輯器的模型以二進(jìn)制格式存儲。當需要和某種應用一起工作時(shí),這并不能提供的格式的可移植性。在復雜的情況下,你需要實(shí)現將模型以XML或類(lèi)似的格式存儲。模型的改變通過(guò)屬性改變事件來(lái)通知。這個(gè)基本類(lèi)允許編輯部件
注冊和
撤銷(xiāo)注冊為屬性改變通知的接受者。屬性改變通知是通過(guò)調用
firePropertyChange方法觸發(fā)的。最后,為了幫助和workbench的Properties視圖集成,需要實(shí)現IPropertySource接口(細節在圖2中忽略)。public abstract class ModelElement implementsIPropertySource,Serializable {
private transient PropertyChangeSupport pcsDelegate =
new PropertyChangeSupport(this);
public synchronized void addPropertyChangeListener(PropertyChangeListener l) {
if (l == null) {
throw new IllegalArgumentException();
}
pcsDelegate.addPropertyChangeListener(l);
}
protected void firePropertyChange(String property,
Object oldValue,
Object newValue) {
if (pcsDelegate.hasListeners(property)) {
pcsDelegate.firePropertyChange(property, oldValue, newValue);
}
}
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
in.defaultReadObject();
pcsDelegate = new PropertyChangeSupport(this);
}
public synchronized void removePropertyChangeListener(PropertyChangeListener l) {
if (l != null) {
pcsDelegate.removePropertyChangeListener(l);
}
}
...
}
橢圓和矩形這兩類(lèi)對象,在許多方面是相同的,它們的公共功能可以被提取出來(lái)放在公共類(lèi)中。尤其是兩者都代表著(zhù)占據某個(gè)位置,具有一定大小的對象。它們可以彼此連接。這些屬性的任何修改都需要通知監聽(tīng)者。更進(jìn)一步地說(shuō),它們的位置和大小屬性都可以通過(guò)IPropertySource接口暴露,這允許用戶(hù)通過(guò)Properties視圖來(lái)查看,和修改它們。
對象間連接的管理很值得仔細看一下。這里并沒(méi)有一個(gè)全局的用于存儲所有連接的地方。GEF要求模型部件報告它們之間的連接的情況,是源還是目標。這些信息都以List對象的形式提供。Shape類(lèi)維護了兩個(gè)數組列表,分別存儲






model包外面的類(lèi)知道圖形的連接情況。這些方法都會(huì )被相關(guān)的圖形(形狀)控制器所使用,具體內容將在接下來(lái)的部分中加以介紹。public abstract class Shape extends ModelElement {
private Point location = new Point(0, 0);
private Dimension size = new Dimension(50, 50);
private List sourceConnections = new ArrayList();
private List targetConnections = new ArrayList();
public Point getLocation() {
return location.getCopy();
}
public void setLocation(Point newLocation) {
if (newLocation == null) {
throw new IllegalArgumentException();
}
location.setLocation(newLocation);
firePropertyChange(LOCATION_PROP, null, location);
}
void addConnection(Connection conn) {
if (conn == null || conn.getSource() == conn.getTarget()) {
throw new IllegalArgumentException();
}
if (conn.getSource() == this) {
sourceConnections.add(conn);
firePropertyChange(SOURCE_CONNECTIONS_PROP, null, conn);
} else if (conn.getTarget() == this) {
targetConnections.add(conn);
firePropertyChange(TARGET_CONNECTIONS_PROP, null, conn);
}
}
void removeConnection(Connection conn) {
if (conn == null) {
throw new IllegalArgumentException();
}
if (conn.getSource() == this) {
sourceConnections.remove(conn);
firePropertyChange(SOURCE_CONNECTIONS_PROP, null, conn);
} else if (conn.getTarget() == this) {
targetConnections.remove(conn);
firePropertyChange(TARGET_CONNECTIONS_PROP, null, conn);
}
}
public List getSourceConnections() {
return new ArrayList(sourceConnections);
}
public List getTargetConnections() {
return new ArrayList(targetConnections);
}
...
}通過(guò)上面的準備,我們可以開(kāi)始編寫(xiě)頂層模型類(lèi)。Connection類(lèi)表示兩個(gè)圖形間的連接。它存儲連接的源和目標。通過(guò)調用disconnect或reconnect方法可以修改連接。連接含有一個(gè)boolean值來(lái)表示連接是否存在。命令會(huì )使用這個(gè)值來(lái)驗證某種操作的合法性。源連接和目標連接都保持一個(gè)到源圖形的引用,這樣使得被斷開(kāi)的連接可以很容易地被重新連接。連接包含一個(gè)屬性,就是線(xiàn)的類(lèi)型。EllipticalShape和RectangularShape類(lèi)都擴展了Shape類(lèi),添加了很少的功能。
ShapeDiagram類(lèi)是ModelElement類(lèi)的子類(lèi),它可以作為一種容器。它維護一組圖形,并通知監聽(tīng)器這組圖形的變化。命令可以調用

addChild和
removeChild方法,并檢查返回的boolean值來(lái)驗證它們的操作。這個(gè)類(lèi)也提供了
public class ShapesDiagram extends ModelElement {
...
private Collection shapes = new Vector();
public boolean addChild(Shape s) {
if (s != null && shapes.add(s)) {
firePropertyChange(CHILD_ADDED_PROP, null, s);
return true;
}
return false;
}
public List getChildren() {
return new Vector(shapes);
}
public boolean removeChild(Shape s) {
if (s != null && shapes.remove(s)) {
firePropertyChange(CHILD_REMOVED_PROP, null, s);
return true;
}
return false;
}
}ShapeDiagram - 圖形的容器細心的讀者一定意識到這個(gè)模型創(chuàng )建了一個(gè)有向圖的實(shí)現,圖形作為頂點(diǎn),連接作為邊,所有圖形,連接構成的圖就是圖。這里所形成的表示方式稱(chēng)為鄰接點(diǎn)列表表示法,它很適合稀疏圖。只要略作修改,這個(gè)模型的代碼就可以轉變?yōu)橐话愕膱D表示。這里對算法書(shū)中的圖實(shí)現所需要做的就是添加代碼使得圖,節點(diǎn),和邊在發(fā)生改變的時(shí)候發(fā)送事件。不象數學(xué)上的圖,節點(diǎn)不是零維的點(diǎn),而是有矩形邊框。最后,圖存儲了所有的邊,而圖形并沒(méi)有存儲連接,因為GEF并沒(méi)有要求這么做。
值得注意的是,由上面的類(lèi)所提供的解決方案并不是唯一的方法。那些開(kāi)發(fā)計算機圖形的人更愿意用另一種方法來(lái)存儲連接,安排節點(diǎn)和邊之間的通信。然而,這些細節并不是那么重要。設計者可以自由地選擇他們認為更具普遍性,更快,或者功能更強的模型表示。關(guān)鍵的地方在模型改變的消息通知,模型修改的維護,包括對可視屬性和模型持久化的支持。其余的都取決于你的經(jīng)驗和需要,你可以自由地進(jìn)行選擇。
由于這個(gè)圖形編輯器非常的簡(jiǎn)單,我們不必創(chuàng )建可視圖形來(lái)表示我們的模型,而是使用預定義的可視圖形。Figure類(lèi)加上FreeformLayout布局管理器用來(lái)表示圖。這允許我們將對象拖放到任何位置。RectangleFigure和Ellipse都可以表示對象。使用預定義的可視圖形來(lái)表示部分模型并不是通常的做法。即使你的視圖沒(méi)有引用模型或控制器,它都必須為每個(gè)用戶(hù)可能需要查看或修改的模型重要方面都定義可視化屬性。因此常常會(huì )定義擁有大量可視化屬性,比如顏色,文本,嵌套可視圖形等,的復雜可視圖形,每個(gè)屬性都對應于它們所表示的模型屬性。有關(guān)創(chuàng )建復雜可視圖形的詳細處理,請參考 [4]。
對于模型的每個(gè)獨立部分,我們都必須定義控制器。所謂“獨立”,指的是這個(gè)實(shí)體都可以作為用戶(hù)操作的對象。一個(gè)比較好的原則就是任何可以被選擇,或刪除的對象都應該有它自己的編輯部件。
編輯部件知道模型,監聽(tīng)模型改變所產(chǎn)生的事件,然后更新視圖。由于在模型層所做的設計選擇,所有的編輯部件都必需遵循圖5所示的模式。每個(gè)部件

PropertyChangeListener接口。當它被激活時(shí)


public abstract class SpecificPart extends AbstractGraphicalEditPartimplements PropertyChangeListener {
public void activate() {
if (!isActive()) {
super.activate();
((PropertyAwareModel) this.getModel()).addPropertyChangeListener(this);
}
}public void deactivate() {
if (isActive()) {
((PropertyAwareModel) this.getModel()).removePropertyChangeListener(this);
super.deactivate();
}
}
public void propertyChage(PropertyChangeEvent evt) {
String prop = evt.getPropertyName();
...
}
}
當編輯器成功載入一個(gè)幾何圖,并將它設置在一個(gè)圖形viewer上,就要求ShapesEditPartFactory創(chuàng )建一個(gè)編輯部件來(lái)控制圖。它創(chuàng )建一個(gè)新的DiagramEditPart實(shí)例,并將圖設置為它的模型。當新創(chuàng )建的編輯部件被激活時(shí),它將自己注冊為模型的監聽(tīng)器,并創(chuàng )建一個(gè)使用free form布局管理器的可視圖形,這種布局管理器允許通過(guò)它們的邊界來(lái)定位圖的可視圖形。DiagramEditPart通過(guò) getModelChildren方法來(lái)獲取圖中包含的所有圖形。就象前面提到的,GEF為返回的所有子模型對象都會(huì )創(chuàng )建編輯部件和可視圖形。
DiagramEditPart類(lèi)安裝了三個(gè)策略。所有的策略都在AbstractEditPart類(lèi)的createEditPolicies方法中定義,同時(shí)所有繼承自AbstractGraphicalEditPart 的實(shí)類(lèi)都必需實(shí)現這個(gè)方法。編輯部件使用這些策略來(lái)處理工具發(fā)出的請求。在最簡(jiǎn)單的情況下,策略負責生成許多命令。策略使用String類(lèi)型的索引字注冊在編輯部件上,這個(gè)索引字被稱(chēng)為策略角色。這些索引字對編輯部件本身來(lái)說(shuō)沒(méi)有什么意義。然而,對軟件開(kāi)放人員,就有意義了,它使得其他人,尤其是擴展你的控制器的人,可以通過(guò)這些索引字來(lái)關(guān)閉或移除策略。就GEF而言,你的索引字可以是“foobar”。然而,你最好告訴你程序員同伴,當布局管理器改變的時(shí)候,為了設置新的布局策略,需要安裝新的“foobar”策略。由于這可能很有趣,且不是那么顯而易見(jiàn),所以推薦你使用EditPolicy接口定義索引字,這些名字需要很好的表達該策略在編輯部件中的角色。
安裝的第一個(gè)策略

EditPolicy.COMPONENT_ROLE,它負責阻止模型的根被刪除。它重寫(xiě)了createDeleteCommand方法,并返回一個(gè)不能被執行的命令。第二個(gè)策略
LAYOUT_ROLE,它處理創(chuàng )建請求和邊界修改請求。當新的圖形被放置到圖中,第一個(gè)請求被發(fā)送出來(lái)。布局策略返回一個(gè)命令,這個(gè)命令添加新的圖形到圖編輯器中,并把它放置在適當的位置。用戶(hù)修改圖中已存在的圖形大小或移動(dòng)它時(shí),都會(huì )發(fā)出邊界修改請求。第三個(gè)installEditPolicy調用
protected void createEditPolicies() {
installEditPolicy(EditPolicy.COMPONENT_ROLE, new RootComponentEditPolicy());
XYLayout layout = (XYLayout) getContentPane().getLayoutManager();
installEditPolicy(EditPolicy.LAYOUT_ROLE, new ShapesXYLayoutEditPolicy(layout));
installEditPolicy(EditPolicy.SELECTION_FEEDBACK_ROLE, null);
}圖編輯部件監視子編輯部件的添加,移除事件。當任何新的圖形添加或移除時(shí),ShapesDiagam類(lèi)將發(fā)送這些事件。當圖編輯部件檢測到這兩種屬性修改事件時(shí),圖編輯部件都會(huì )調用AbstractEditPart類(lèi)中定義的refreshChildren方法。這個(gè)方法會(huì )遍歷所有子模型對象,并相應地添加,移除,或重新排序子編輯部件。
ShapeEditPart類(lèi)管理所有的圖形。當DiagramEditPart會(huì )返回子模型列表時(shí),ShapeEditPart由ShapesEditPartFactory類(lèi)根據每個(gè)模型對象的類(lèi)型創(chuàng )建。工廠(chǎng)類(lèi)創(chuàng )建的每個(gè)部件都擁有一個(gè)它們所控制的子模型。一旦模型對象被設置,編輯部件被要求創(chuàng )建可視圖形來(lái)表示模型對象。根據模型對象的類(lèi)型,返回橢圓或矩形的編輯部件。
這個(gè)編輯部件關(guān)注四類(lèi)屬性修改事件:大小,位置,源連接,和目標連接。如果圖形改變了大小或位置,

refreshVisual方法會(huì )被調用。這個(gè)方法在可視圖形被創(chuàng )建的時(shí)候就會(huì )由GEF自動(dòng)調用。在這個(gè)方法中,可視圖形的可視屬性應該根據模型的狀態(tài)做相應調整。重用模型更新方法是 GEF編輯器中經(jīng)常碰到的又一種模式。在我們這個(gè)編輯部件類(lèi)中,新的位置和大小被獲取并儲存在表示圖形的可視圖形中。此外,新的邊界會(huì )傳給父控制器的布局管理器。當源連接或目標連接改變時(shí),源連接或目標連接改編輯部件會(huì )調用AbstractGraphicalEditPart類(lèi)中的方法刷新。和refreshChildren方法相似,這些方法會(huì )遍歷所有的連接,并相應添加,刪除,或重新定位它們的編輯部件。class ShapeEditPart extends AbstractGraphicalEditPartimplements PropertyChangeListener, NodeEditPart {
protected List getModelSourceConnections() {
return getCastedModel().getSourceConnections();
}
protected List getModelTargetConnections() {
return getCastedModel().getTargetConnections();
}
public ConnectionAnchor getSourceConnectionAnchor(ConnectionEditPart connection) {
return new ChopboxAnchor(getFigure());
}
public ConnectionAnchor getSourceConnectionAnchor(Request request) {
return new ChopboxAnchor(getFigure());
}
public void propertyChange(PropertyChangeEvent evt) {
String prop = evt.getPropertyName();
if (Shape.SIZE_PROP.equals(prop) || Shape.LOCATION_PROP.equals(prop)) {
refreshVisuals();
}
if (Shape.SOURCE_CONNECTIONS_PROP.equals(prop)) {
refreshSourceConnections();
}
if (Shape.TARGET_CONNECTIONS_PROP.equals(prop)) {
refreshTargetConnections();
}
}
protected void refreshVisuals() {
Rectangle bounds = new Rectangle(getCastedModel().getLocation(),
getCastedModel().getSize());
figure.setBounds(bounds);
((GraphicalEditPart) getParent()).setLayoutConstraint(this, figure, bounds);
}
}
由于圖形可以連接到其他圖形,圖形編輯部件重寫(xiě)了方法和
方法。這兩個(gè)方法的任務(wù)就是要通知GEF有關(guān)該圖形的源連接和目標連接。此外,
ShapeEditPart實(shí)現了


圖形編輯部件安裝了兩個(gè)策略。ShapeComponentEditPolicy提供命令將一個(gè)圖形從圖刪除。第二個(gè)策略處理圖形間連接的創(chuàng )建和轉移,它的索引字是GRAPHICAL_NODE_ROLE。連接創(chuàng )建工具創(chuàng )建新的連接需要兩個(gè)步驟。當用戶(hù)點(diǎn)擊模型元素的可視圖形時(shí),該策略被要求

null,表示這個(gè)連接不能從所給的模型元素開(kāi)始。如果允許連接的話(huà),將創(chuàng )建新的命令,并作為起始命令存儲在請求中。當用戶(hù)點(diǎn)擊另一個(gè)可視圖形時(shí),會(huì )要求策略提供一個(gè)
new GraphicalNodeEditPolicy() {
protected Command getConnectionCreateCommand(CreateConnectionRequest request) {
Shape source = (Shape) getHost().getModel();
int style = ((Integer) request.getNewObjectType()).intValue();
ConnectionCreateCommand cmd = new ConnectionCreateCommand(source, style);
request.setStartCommand(cmd);
return cmd;
}
protected Command getConnectionCompleteCommand(CreateConnectionRequest request) {
ConnectionCreateCommand cmd =
(ConnectionCreateCommand) request.getStartCommand();
cmd.setTarget((Shape) getHost().getModel());
return cmd;
}
...
}圖形節點(diǎn)編輯策略的另一個(gè)任務(wù)是提供連接的轉移命令。連接可以修改連接的源或目標實(shí)現轉移。連接轉移命令和連接創(chuàng )建命令有同樣的規則。尤其是當一個(gè)連接不能轉移時(shí),策略返回null。策略也可能通過(guò)canExecute方法返回false來(lái)得到一個(gè)拒絕執行的命令。由于篇幅限制,這些命令的細節就不多說(shuō)了,讀者可以參考代碼。
由于連接也是用戶(hù)可編輯的模型對象,它們必須有自己的控制器。連接的控制器是由ConnectionEditPart類(lèi)實(shí)現,它繼承自AbstractConnectionEditPart類(lèi)。和其他控制器類(lèi)似,它也實(shí)現了

PropertyChangeListener接口,并注冊自己為模型的監聽(tīng)器。連接部件

ConnectionComponentPolicy,它提供刪除命令給Delete菜單項所需要的action。第二個(gè)
比較有意思。它含有一個(gè)被選擇的連接,這個(gè)連接包括起始端和結束端的標識。沒(méi)有這個(gè)策略,就不可能轉移連接,因為當一個(gè)連接被拖動(dòng)時(shí),GEF沒(méi)有辦法獲取連接兩端的標識。GEF的設計者建議所有的ConnectionEditParts都應該有這個(gè)策略,即使連接的兩端都不能拖動(dòng)。至少這個(gè)策略提供了一種視覺(jué)上的選擇反饋。propertyChange方法可以收到
線(xiàn)風(fēng)格屬性的變化,并對線(xiàn)figure作相應的調整。class ConnectionEditPart extends AbstractConnectionEditPartimplements PropertyChangeListener {
protected IFigure createFigure() {PolylineConnection connection = (PolylineConnection) super.createFigure();
connection.setTargetDecoration(new PolygonDecoration());
connection.setLineStyle(getCastedModel().getLineStyle());
return connection;
}
protected void createEditPolicies() {installEditPolicy(EditPolicy.CONNECTION_ROLE, new ConnectionEditPolicy() {
protected Command getDeleteCommand(GroupRequest request) {
return new ConnectionDeleteCommand(getCastedModel());
}
});installEditPolicy(EditPolicy.CONNECTION_ENDPOINTS_ROLE,
new ConnectionEndpointEditPolicy());
}
public void propertyChange(PropertyChangeEvent event) {
String property = event.getPropertyName();if (Connection.LINESTYLE_PROP.equals(property)) {
((PolylineConnection) getFigure()).
setLineStyle(getCastedModel().getLineStyle());
}
}
...
}
幾何圖形編輯器繼承了GraphicalEditorWithFlyoutPalette類(lèi)。這個(gè)類(lèi)是圖形編輯器的一種特殊形式,它本身也是一種編輯部件,并可以擁有一個(gè)提供工具的面板。使用這個(gè)類(lèi)必須實(shí)現兩個(gè)方法,getPaletteRoot和getPalettePreferences。第一個(gè)方法必須返回包含所有工具選項的面板的根節點(diǎn)。工具選項是一種特殊的面板選項,它將工具安裝在編輯器的編輯域上。它們必須位于面板抽屜中,面板抽屜將工具選項很方便地組合起來(lái)。一般推薦有一個(gè)工具選項作為整個(gè)工具面板的缺省選項。一個(gè)典型的解決方法就是直接使用SelectionToolEntry類(lèi)的實(shí)例。第二個(gè)方法返回的面板首選項中包含的內容有,報告面板是可見(jiàn)還是被折疊起來(lái)了,面板??康奈恢?,以及面板的寬度。通常的解決方法是將它們存在plug-in的首選項存儲區中。
我們上面提到的編輯域起了一個(gè)中心控制器的作用。它負責保存工具,載入缺省工具,維護當前激活的工具,并將鼠標和鍵盤(pán)事件轉發(fā)給當前激活的工具,以及處理命令棧。GEF提供了缺省實(shí)現,DefaultEditDomain,你應該在編輯器的構造函數中設置它的實(shí)例。
圖形編輯器的另一部分工作是創(chuàng )建并初始化圖形viewer。圖形viewer是一種特殊的EditPartViewer,它能夠做點(diǎn)擊測試。我們可以使用GraphicalEditor類(lèi)提供的缺省viewer。然而,還是需要做一些事。在configureGraphicalViewer方法

EditPartFactory,這個(gè)接口只有一個(gè)方法,createEditPart(EditPart, Object)。它的第一個(gè)參數是編輯部件,它一般是所創(chuàng )建的編輯部件的父部件,第二個(gè)參數是新創(chuàng )建的編輯部件所對應的模型部件。其他要做的包括設置鍵處理器,上下文菜單等。protected void configureGraphicalViewer() {
super.configureGraphicalViewer();
GraphicalViewer viewer = getGraphicalViewer();
viewer.setRootEditPart(new ScalableRootEditPart());
viewer.setEditPartFactory(new ShapesEditPartFactory());
viewer.setKeyHandler(
new GraphicalViewerKeyHandler(viewer).setParent(getCommonKeyHandler()));
ContextMenuProvider cmProvider =
new ShapesEditorContextMenuProvider(viewer, getActionRegistry());
viewer.setContextMenu(cmProvider);
getSite().registerContextMenu(cmProvider, viewer);
}
protected void initializeGraphicalViewer() {
super.initializeGraphicalViewer();
GraphicalViewer graphicalViewer = getGraphicalViewer();
graphicalViewer.setContents(getModel());
graphicalViewer.addDropTargetListener(createTransferDropTargetListener());
}一旦工廠(chǎng)類(lèi)被設置,你應該在圖形viewer中

IEditorInput實(shí)例恢復得到的對象,IEditorInput實(shí)例通過(guò)setInput方法傳遞給編輯器。這個(gè)例子在圖形viewer上添加
TemplateTransferDropTargetListener的子類(lèi),它使用CreateRequest來(lái)獲得添加對象到模型的命令,這個(gè)模型當然就是拖放動(dòng)作結束時(shí)所在的編輯部件所表示的模型。除了上面談到的任務(wù),編輯器還負責監視命令棧來(lái)報告當前編輯的內容是否被修改。這是一個(gè)比較好的解決方法,因為它可以使這個(gè)標記和用戶(hù)所做的undo和redo同步起來(lái)。注意,命令棧含有上次存儲的位置信息,這個(gè)信息在doSave和doSaveAs這兩個(gè)方法中被標記。編輯器的其他細節,比如模型的實(shí)際存儲和恢復,這里就不討論了,因為它們和具體的應用相關(guān)。接下來(lái),我們討論編輯器的如何將編輯器內容暴露給其視圖,如何將菜單選項和編輯器的action聯(lián)系起來(lái),以及其他workbench協(xié)作的技術(shù)。
到目前為止,我們談的都是這個(gè)幾何圖形編輯器如何工作。然而,它沒(méi)有和workbench很好地集成。例如,Edit菜單動(dòng)作,比如Delete,Undo和Redo,就不能工作。其他視圖不能用其他方式顯示編輯器內容。換句話(huà)說(shuō),目前所完成的編輯器沒(méi)有很好地利用Eclipse workbench的優(yōu)勢。在下面的三小節,將解釋如何將這個(gè)孤立的編輯器變成workbench的一部分。
ShapesEditor類(lèi)創(chuàng )建了大量缺省動(dòng)作,它們在編輯器初始化過(guò)程中被createActions方法中創(chuàng )建。這些動(dòng)作是undo,redo,select all,delete,save和print。為了將標準菜單選項連接到這些動(dòng)作,你應該在plugin.xml文件中定義一個(gè)action bar contributor。在這個(gè)action bar contributor中,你需要實(shí)現兩個(gè)方法。第一個(gè)是

buildActions,它可以為undo,redo和delete創(chuàng )建可重定位的動(dòng)作。如果你需要使用鍵盤(pán)選擇所有的widget,你需要在第二個(gè)方法declareGlobalActionKeys中為所選擇的動(dòng)作
public class ShapesEditorActionBarContributor extends ActionBarContributor {
protected void buildActions() {
this.addRetargetAction(new UndoRetargetAction());
this.addRetargetAction(new RedoRetargetAction());
this.addRetargetAction(new DeleteRetargetAction());
}
public void contributeToToolBar(IToolBarManager toolBarManager) {
super.contributeToToolBar(toolBarManager);
toolBarManager.add(getAction(ActionFactory.UNDO.getId()));
toolBarManager.add(getAction(ActionFactory.REDO.getId()));
}
protected void declareGlobalActionKeys() {
this.addGlobalActionKey(ActionFactory.SELECT_ALL.getId());
}
}我們來(lái)仔細看一下當用戶(hù)在Edit菜單中選擇Delete時(shí)發(fā)生了些什么(看圖12)。ShapesEditor類(lèi)的父類(lèi)將刪除動(dòng)作添加到動(dòng)作注冊表中。當刪除動(dòng)作被執行時(shí),它檢查當前的所選擇的對象是否是EditPart類(lèi)的實(shí)例。對每個(gè)對象,它都從編輯部件中請求一個(gè)命令。接下來(lái),每個(gè)編輯部件檢查是否有編輯策略可以處理刪除請求。對幾何圖形,ShapeComponentEditPolicy可以處理刪除請求,并且返回ShapeDeleteCommand實(shí)例。動(dòng)作執行該命令,從而將圖形從圖中刪除。圖發(fā)送一個(gè)屬性修改事件,DiagramEditPart收到該事件,最終使得代表被刪除圖形的矩形或橢圓從顯示中被刪除。

每個(gè)圖形編輯器都是可以發(fā)送選擇事件。你可以建立一個(gè)視圖,并將它作為選擇監聽(tīng)器注冊在workbench site的頁(yè)面上。每次你在圖形編輯器中選擇一個(gè)對象,你的視圖都會(huì )在selectionChanged方法中收到一個(gè)通知。Eclipse的一個(gè)標準視圖,Properties視圖,會(huì )監聽(tīng)選擇事件,并且每次都檢查這個(gè)對象是否實(shí)現了IPropertySource接口。如果是的話(huà),它使用這個(gè)接口的方法來(lái)查詢(xún)所選擇的對象屬性,并以表格的方式顯示出來(lái)。
通過(guò)上面所描述的,在圖形編輯器中提供對象的屬性只要實(shí)現IPropertySource接口就可以了。通過(guò)查看Shape類(lèi),你可以看到對象的位置和大小是如何在Properties視圖中顯示的。
Outline視圖是另一種,常常也是更簡(jiǎn)潔的查看模型對象的方式。在Java編輯器中,它可以用來(lái)顯示一個(gè)類(lèi)所import的類(lèi),所包含的變量,和方法,卻不需要用戶(hù)深入代碼。圖形編輯器也可以使用這個(gè)視圖。圖形編輯器和邏輯電路編輯器類(lèi)似,可以以樹(shù)的方式顯示所編輯的內容(看圖1)。數據庫schema編輯器[7]也提供了類(lèi)似的視圖。
為了將所編輯的內容提供給Outline視圖,你需要重寫(xiě)getAdapter方法,并當adapter類(lèi)為IContentOutlinePage接口時(shí),返回一個(gè)outline實(shí)現。實(shí)現outline的最簡(jiǎn)單的方法是擴展ContentOutlinePage類(lèi),并提供適當的EditPartViewer。
public Object getAdapter(Class type) {
// returns the content outline page for this editor
if (type == IContentOutlinePage.class) {
if (outlinePage == null) {
outlinePage = new ShapesEditorOutlinePage(this, new TreeViewer());
}
return outlinePage;
}
return super.getAdapter(type);
}在我們這個(gè)例子中,編輯部件視圖是有一個(gè)TreeViewer實(shí)現的。你應該和主編輯器一樣提供給它同樣的編輯域。TreeViewer,就象其他EditPartViewer,需要一個(gè)創(chuàng )建子編輯部件的方法。編輯器和DiagramEditPart一樣,都是設置一個(gè)編輯部件工廠(chǎng)。此外,outline中的選擇和主編輯窗口的選擇需要通過(guò)選擇同步器同步起來(lái),選擇同步器是一個(gè)GEF工具類(lèi),它協(xié)調兩個(gè)編輯部件的選擇狀態(tài)。ShapesTreeEditPartFactory根據模型類(lèi)型,返回ShapeTreeEditPart或DiagramTreeEditPart的實(shí)例。通過(guò)這些類(lèi),讀者應該可以輕易地發(fā)現這些模式很熟悉。兩個(gè)編輯部件都實(shí)現了PropertyChangeListener接口,并通過(guò)調整模型的可視化表示來(lái)對屬性變化做出響應。它們都安裝編輯策略來(lái)控制通過(guò)它們所暴露的交互類(lèi)型。
GEF通過(guò)大量使用設計模式來(lái)得到它的靈活性。下面是一下經(jīng)常碰到的模式的小結。詳細內容,請參考 [2]。
IFigure接口??刂频念?lèi)型必須是EditPart或它的子類(lèi)。Commands,這些Commands以鏈的方式組織在一起。createChild方法允許你不使用工廠(chǎng)就創(chuàng )建子編輯部件。我希望能夠對這個(gè)簡(jiǎn)單圖形編輯器的大多數方面作詳細的描述。提供足夠的信息使得人們能夠讀完這篇文章,去看更大的例子,比如邏輯電路編輯器。通過(guò)理解象CircuitEditPart,AndGateFigure和其他類(lèi)的角色,你可以關(guān)注其他例子的更復雜的方面。在GEF的眾多領(lǐng)域和技術(shù)中,有很多我甚至都沒(méi)有涉及過(guò)。然而,這些技術(shù)只有在很好地理解基礎內容的情況下,才可能去學(xué)習。畢竟,如果你為了使Select All菜單項工作都要花數小時(shí),那么設計一個(gè)拖反饋的目的又是什么呢?
我想感謝Randy Hudson,他的意見(jiàn)幫助提高了本文結構和準確性。我也感謝Jill Sueoka仔細檢查我所寫(xiě)一個(gè)又一個(gè)版本。
聯(lián)系客服