繪圖編輯框架(GEF)被設(shè)計用來以圖形而不是文本的方式來編輯用戶數(shù)據(jù),一般被稱為模型(model)。當處理包含多對多,一對多以及其他復(fù)雜關(guān)系的實體時,GEF是一種很有價值的工具。隨著Eclipse Rich Client Platform 的流行,使得編輯器的開發(fā)不僅僅局限于編程,GEF的重要性也與日俱增。比如說,數(shù)據(jù)庫schema編輯器 [7],邏輯電路編輯器和任務(wù)流管理器,這些例子都很好地展示了GEF是一種可以用于各個不同領(lǐng)域的,具有強大功能和靈活性的框架。
然而,任何通用框架都設(shè)計復(fù)雜,難于學(xué)習,GEF也不例外。到現(xiàn)在為止,最小的例子也將涉及75個類。即使對于最勤勉的開發(fā)者來說,要從GEF用戶定義類型和GEF提供的上百種類型之間相互作用來理解GEF的獨特之處,對耐心和智力的都是一種考驗。為了改變這種狀況,一個全新的,規(guī)模更小的編輯器例子被添加進即將到來的Eclipse 3.1(譯:翻譯此文時,Eclipse 3.1已經(jīng)發(fā)布)。這個幾何圖形編輯器(看圖1)允許你創(chuàng)建,編輯簡單的圖。它處理兩種對象,矩形和橢圓。你可以在實線和虛線這兩種連接類型中選擇一種來連接兩個對象。每一個連接都是有方向的,也就是說從一個源對象開始,在目標對象處終止。箭頭用來表示連接方向。連接可以轉(zhuǎn)移,也就是通過拖動它的源點或目標點到一個新的對象上。編輯器中的對象可以點擊選中,也可以通過拖拉一個區(qū)域來選擇。選中的對象可以被刪除。所有的模型操作,比如添加,刪除對象,移動對象,改變大小等等,都可以undo或redo。最后,編輯器集成了兩個Eclipse標準視圖Properties和Outline。這個編輯器的價值不是在于它的可用性,而是作為例子,通過有限的兩種用戶定義類型來演示在一個成熟GEF編輯器中會碰到的大多數(shù)概念和技術(shù)。
GEF幫助你為數(shù)據(jù)構(gòu)造一個可視化的編輯器。數(shù)據(jù)可以是帶有簡單溫度旋鈕的溫度調(diào)節(jié)器,也可以是一個包含幾百個路由器,連接和服務(wù)質(zhì)量策略的虛擬局域網(wǎng)。幸虧GEF設(shè)計者,他們設(shè)法建立一種框架,使得它能夠和任何數(shù)據(jù)一起工作,用GEF的術(shù)語來說,就是任何模型(model)。這是通過嚴格遵循了模型-視圖-控制器模式(MVC)來做到的。模型就是你的數(shù)據(jù)。對于GEF,模式可以是任何普通的Java對象(POJO)。模型不應(yīng)該知道任何有關(guān)于控制器或視圖的信息。視圖(view)是模型或其某一部分在屏幕上的可視化表示。它可以是矩形,線或橢圓這樣的簡單圖形,也可以是彼此嵌套的邏輯電路。同時,視圖也應(yīng)該對模型和控制器一無所知。雖然任何實現(xiàn)IFigure
接口的類都可以作為視圖,但是GEF使用Draw2D可視圖形(figure)。控制器,可稱為編輯部件(edit part),是模型和視圖之間的橋梁。當你開始編輯你的模型時,一個頂層的控制器被創(chuàng)建出來。如果模型由若干個片段組成,頂層控制器就會將這個信息通知GEF。接下來,每個片段的子控制器被創(chuàng)建出來。如果它們又包含子片段,這個過程就會一直繼續(xù)下去,直到所有組成模型的對象都有它們的控制器??刂破鞯牧硪粋€任務(wù)是創(chuàng)建可視圖形來表示模型。一旦模型被設(shè)置到某個控制器,GEF就向控制器要合適的IFigure
對象。既然模型和視圖彼此都不知道對方,控制器負責監(jiān)聽模型的修改,并更新模型的可視化表示。結(jié)果,在許多GEF編輯器中,一個常見的模式就是模型發(fā)PropertyChangeEvent
通知。當一個編輯部件收到事件通知時,它通過調(diào)整模型的外觀或結(jié)構(gòu)上的表示來作相應(yīng)的改變。
可視編輯的另一個方面就是對用戶動作和鼠標,鍵盤事件作出響應(yīng)。這里的挑戰(zhàn)在于提供一種機制,提供合理的缺省行為,并且允許重新定義行為來覆蓋缺省行為,以適應(yīng)所編輯模型。比如鼠標拖動事件,如果我們假設(shè)每次檢測到鼠標拖動事件,所選中對象都被移動的話,我們就限制編輯器開發(fā)者的自由。很有可能有人希望在鼠標拖動的時候,提供放大,縮小的行為。GEF通過使用工具(tool),請求(request)和策略(policy)解決了這個問題。
工具是一種有狀態(tài)的對象,它將象鼠標按鈕被按下,被拖動等低層事件翻譯成高層的由Request
對象表示的請求。發(fā)送哪個請求取決于所激活的工具。例如,連接工具在收到鼠標按鈕被按下這樣的事件時,會發(fā)送一個連接開始或結(jié)束的請求。如果是一個創(chuàng)建工具,我們就會收到一個創(chuàng)建請求。GEF包含了大量預(yù)定義的工具以及創(chuàng)建應(yīng)用特定工具的方法。工具可以由程序控制激活,也可以在用戶實施一個動作后激活。在大多數(shù)情況下,工具將請求發(fā)送給鼠標位置下面的圖形的EditPart
。例如,如果你點擊一個代表widget的矩形,與此相關(guān)的編輯部件就會收到一個選中請求或者直接編輯的請求。有時候,請求會發(fā)送給區(qū)域中的所有可視圖形的編輯部件,比如MarqueeSelectionTool
就是這樣。無論一個或多個編輯部件怎樣被選擇為請求目標,它們自己并不處理請求。而是將這個任務(wù)交給所注冊的編輯策略( edit policies)。每個編輯策略都會為該請求提供一個命令。不希望處理請求的策略將返回一個null
。使用策略而不是編輯部件來響應(yīng)請求的機制使得策略和編輯部件都盡可能短小,功能集中。同時,也意味著調(diào)試和維護代碼變得更容易。GEF的最后一個部分就是命令(command)。GEF并沒有直接修改模型,它要求你使用命令來做實際的修改。每個命令應(yīng)該實現(xiàn)執(zhí)行對模型或模型一部分的修改和撤銷修改。這樣,GEF編輯器自動支持模型修改的undo/redo。
除了能夠提升你的技能以及設(shè)計模式方面的知識外,使用GEF的一個重要的優(yōu)點在于它能夠和Eclipse平臺完全集成在一起。在編輯器中選中的對象可以為標準Properties視圖提供屬性。Eclipse向?qū)Э梢杂脕韯?chuàng)建,初始化GEF編輯器編輯的模型。Edit菜單中的Undo和Redo可以觸發(fā)GEF編輯修改的撤銷和重做。簡單地說,GEF編輯器實現(xiàn)IEditorPart
接口,是Eclipse平臺中的一員,它和文本編輯器或其他workbench編輯器處于同樣的集成層次。
創(chuàng)建GEF編輯器的第一步是創(chuàng)建模型。在我們的例子里,模型由四類對象組成:幾何圖(包含所有的圖形),兩種類型的圖形,和圖形間的連接。在我們?yōu)檫@些類編寫代碼前,我們準備了一些基礎(chǔ)結(jié)構(gòu)。
當你創(chuàng)建模型時,你可以參考下面的內(nèi)容:
java.beans
包中的屬性修改事件通知。上面所列的規(guī)則對于所有模型都是相同的,為基本類建立類層次來強化這些規(guī)則是很有好處的。ModelElement
類繼承了Java的Object
類,并提供了三個功能:持久化,屬性改變和屬性源支持。簡單的模型持久化可以通過實現(xiàn)
java.io.Serializable
接口以及readObject方法來完成。這使得你可以將編輯器的模型以二進制格式存儲。當需要和某種應(yīng)用一起工作時,這并不能提供的格式的可移植性。在復(fù)雜的情況下,你需要實現(xiàn)將模型以XML或類似的格式存儲。模型的改變通過屬性改變事件來通知。這個基本類允許編輯部件
注冊和
撤銷注冊為屬性改變通知的接受者。屬性改變通知是通過調(diào)用
firePropertyChange方法觸發(fā)的。最后,為了幫助和workbench的Properties視圖集成,需要實現(xiàn)IPropertySource接口(細節(jié)在圖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);
}
}
...
}
橢圓和矩形這兩類對象,在許多方面是相同的,它們的公共功能可以被提取出來放在公共類中。尤其是兩者都代表著占據(jù)某個位置,具有一定大小的對象。它們可以彼此連接。這些屬性的任何修改都需要通知監(jiān)聽者。更進一步地說,它們的位置和大小屬性都可以通過IPropertySource
接口暴露,這允許用戶通過Properties視圖來查看,和修改它們。
對象間連接的管理很值得仔細看一下。這里并沒有一個全局的用于存儲所有連接的地方。GEF要求模型部件報告它們之間的連接的情況,是源還是目標。這些信息都以List對象的形式
提供。Shape
類維護了兩個數(shù)組列表,分別存儲
model
包外面的類知道圖形的連接情況。這些方法都會被相關(guān)的圖形(形狀)控制器所使用,具體內(nè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);
}
...
}
通過上面的準備,我們可以開始編寫頂層模型類。Connection
類表示兩個圖形間的連接。它存儲連接的源和目標。通過調(diào)用disconnect
或reconnect
方法可以修改連接。連接含有一個boolean值來表示連接是否存在。命令會使用這個值來驗證某種操作的合法性。源連接和目標連接都保持一個到源圖形的引用,這樣使得被斷開的連接可以很容易地被重新連接。連接包含一個屬性,就是線的類型。EllipticalShape
和RectangularShape
類都擴展了Shape
類,添加了很少的功能。
ShapeDiagram
類是ModelElement
類的子類,它可以作為一種容器。它維護一組圖形,并通知監(jiān)聽器這組圖形的變化。命令可以調(diào)用
addChild
和removeChild
方法,并檢查返回的boolean值來驗證它們的操作。這個類也提供了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
- 圖形的容器細心的讀者一定意識到這個模型創(chuàng)建了一個有向圖的實現(xiàn),圖形作為頂點,連接作為邊,所有圖形,連接構(gòu)成的圖就是圖。這里所形成的表示方式稱為鄰接點列表表示法,它很適合稀疏圖。只要略作修改,這個模型的代碼就可以轉(zhuǎn)變?yōu)橐话愕膱D表示。這里對算法書中的圖實現(xiàn)所需要做的就是添加代碼使得圖,節(jié)點,和邊在發(fā)生改變的時候發(fā)送事件。不象數(shù)學(xué)上的圖,節(jié)點不是零維的點,而是有矩形邊框。最后,圖存儲了所有的邊,而圖形并沒有存儲連接,因為GEF并沒有要求這么做。
值得注意的是,由上面的類所提供的解決方案并不是唯一的方法。那些開發(fā)計算機圖形的人更愿意用另一種方法來存儲連接,安排節(jié)點和邊之間的通信。然而,這些細節(jié)并不是那么重要。設(shè)計者可以自由地選擇他們認為更具普遍性,更快,或者功能更強的模型表示。關(guān)鍵的地方在模型改變的消息通知,模型修改的維護,包括對可視屬性和模型持久化的支持。其余的都取決于你的經(jīng)驗和需要,你可以自由地進行選擇。
由于這個圖形編輯器非常的簡單,我們不必創(chuàng)建可視圖形來表示我們的模型,而是使用預(yù)定義的可視圖形。Figure
類加上FreeformLayout
布局管理器用來表示圖。這允許我們將對象拖放到任何位置。RectangleFigure
和Ellipse
都可以表示對象。使用預(yù)定義的可視圖形來表示部分模型并不是通常的做法。即使你的視圖沒有引用模型或控制器,它都必須為每個用戶可能需要查看或修改的模型重要方面都定義可視化屬性。因此常常會定義擁有大量可視化屬性,比如顏色,文本,嵌套可視圖形等,的復(fù)雜可視圖形,每個屬性都對應(yīng)于它們所表示的模型屬性。有關(guān)創(chuàng)建復(fù)雜可視圖形的詳細處理,請參考 [4]。
對于模型的每個獨立部分,我們都必須定義控制器。所謂“獨立”,指的是這個實體都可以作為用戶操作的對象。一個比較好的原則就是任何可以被選擇,或刪除的對象都應(yīng)該有它自己的編輯部件。
編輯部件知道模型,監(jiān)聽模型改變所產(chǎn)生的事件,然后更新視圖。由于在模型層所做的設(shè)計選擇,所有的編輯部件都必需遵循圖5所示的模式。每個部件
PropertyChangeListener
接口。當它被激活時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();
...
}
}
當編輯器成功載入一個幾何圖,并將它設(shè)置在一個圖形viewer上,就要求ShapesEditPartFactory創(chuàng)建一個編輯部件來控制圖。它創(chuàng)建一個新的DiagramEditPart實例,并將圖設(shè)置為它的模型。當新創(chuàng)建的編輯部件被激活時,它將自己注冊為模型的監(jiān)聽器,并創(chuàng)建一個使用free form布局管理器的可視圖形,這種布局管理器允許通過它們的邊界來定位圖的可視圖形。DiagramEditPart通過 getModelChildren方法來獲取圖中包含的所有圖形。就象前面提到的,GEF為返回的所有子模型對象都會創(chuàng)建編輯部件和可視圖形。
DiagramEditPart
類安裝了三個策略。所有的策略都在AbstractEditPart
類的createEditPolicies
方法中定義,同時所有繼承自AbstractGraphicalEditPart 的實類都必需實現(xiàn)這個方法。編輯部件使用這些策略來處理工具發(fā)出的請求。在最簡單的情況下,策略負責生成許多命令。策略使用String類型的索引字注冊在編輯部件上,這個索引字被稱為策略角色。這些索引字對編輯部件本身來說沒有什么意義。然而,對軟件開放人員,就有意義了,它使得其他人,尤其是擴展你的控制器的人,可以通過這些索引字來關(guān)閉或移除策略。就GEF而言,你的索引字可以是“foobar”。然而,你最好告訴你程序員同伴,當布局管理器改變的時候,為了設(shè)置新的布局策略,需要安裝新的“foobar”策略。由于這可能很有趣,且不是那么顯而易見,所以推薦你使用EditPolicy接口定義索引字,這些名字需要很好的表達該策略在編輯部件中的角色。
安裝的第一個策略
EditPolicy.COMPONENT_ROLE
,它負責阻止模型的根被刪除。它重寫了createDeleteCommand
方法,并返回一個不能被執(zhí)行的命令。第二個策略LAYOUT_ROLE
,它處理創(chuàng)建請求和邊界修改請求。當新的圖形被放置到圖中,第一個請求被發(fā)送出來。布局策略返回一個命令,這個命令添加新的圖形到圖編輯器中,并把它放置在適當?shù)奈恢?。用戶修改圖中已存在的圖形大小或移動它時,都會發(fā)出邊界修改請求。第三個installEditPolicy
調(diào)用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);
}
圖編輯部件監(jiān)視子編輯部件的添加,移除事件。當任何新的圖形添加或移除時,ShapesDiagam
類將發(fā)送這些事件。當圖編輯部件檢測到這兩種屬性修改事件時,圖編輯部件都會調(diào)用AbstractEditPart
類中定義的refreshChildren
方法。這個方法會遍歷所有子模型對象,并相應(yīng)地添加,移除,或重新排序子編輯部件。
ShapeEditPart
類管理所有的圖形。當DiagramEditPart
會返回子模型列表時,ShapeEditPart
由ShapesEditPartFactory
類根據(jù)每個模型對象的類型創(chuàng)建。工廠類創(chuàng)建的每個部件都擁有一個它們所控制的子模型。一旦模型對象被設(shè)置,編輯部件被要求創(chuàng)建可視圖形來表示模型對象。根據(jù)模型對象的類型,返回橢圓或矩形的編輯部件。
這個編輯部件關(guān)注四類屬性修改事件:大小,位置,源連接,和目標連接。如果圖形改變了大小或位置,
refreshVisual
方法會被調(diào)用。這個方法在可視圖形被創(chuàng)建的時候就會由GEF自動調(diào)用。在這個方法中,可視圖形的可視屬性應(yīng)該根據(jù)模型的狀態(tài)做相應(yīng)調(diào)整。重用模型更新方法是 GEF編輯器中經(jīng)常碰到的又一種模式。在我們這個編輯部件類中,新的位置和大小被獲取并儲存在表示圖形的可視圖形中。此外,新的邊界會傳給父控制器的布局管理器。當源連接或目標連接改變時,源連接或目標連接改編輯部件會調(diào)用AbstractGraphicalEditPart
類中的方法刷新。和refreshChildren
方法相似,這些方法會遍歷所有的連接,并相應(yīng)添加,刪除,或重新定位它們的編輯部件。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);
}
}
由于圖形可以連接到其他圖形,圖形編輯部件重寫了
方法和
方法。這兩個方法的任務(wù)就是要通知GEF有關(guān)該圖形的源連接和目標連接。此外,ShapeEditPart
實現(xiàn)了
圖形編輯部件安裝了兩個策略。ShapeComponentEditPolicy
提供命令將一個圖形從圖刪除。第二個策略處理圖形間連接的創(chuàng)建和轉(zhuǎn)移,它的索引字是GRAPHICAL_NODE_ROLE
。連接創(chuàng)建工具創(chuàng)建新的連接需要兩個步驟。當用戶點擊模型元素的可視圖形時,該策略被要求
null
,表示這個連接不能從所給的模型元素開始。如果允許連接的話,將創(chuàng)建新的命令,并作為起始命令存儲在請求中。當用戶點擊另一個可視圖形時,會要求策略提供一個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;
}
...
}
圖形節(jié)點編輯策略的另一個任務(wù)是提供連接的轉(zhuǎn)移命令。連接可以修改連接的源或目標實現(xiàn)轉(zhuǎn)移。連接轉(zhuǎn)移命令和連接創(chuàng)建命令有同樣的規(guī)則。尤其是當一個連接不能轉(zhuǎn)移時,策略返回null。策略也可能通過canExecute
方法返回false來得到一個拒絕執(zhí)行的命令。由于篇幅限制,這些命令的細節(jié)就不多說了,讀者可以參考代碼。
由于連接也是用戶可編輯的模型對象,它們必須有自己的控制器。連接的控制器是由ConnectionEditPart
類實現(xiàn),它繼承自AbstractConnectionEditPart
類。和其他控制器類似,它也實現(xiàn)了
PropertyChangeListener
接口,并注冊自己為模型的監(jiān)聽器。連接部件ConnectionComponentPolicy,它提供刪除命令給Delete菜單項所需要的action。第二個
比較有意思。它含有一個被選擇的連接,這個連接包括起始端和結(jié)束端的標識。沒有這個策略,就不可能轉(zhuǎn)移連接,因為當一個連接被拖動時,GEF沒有辦法獲取連接兩端的標識。GEF的設(shè)計者建議所有的ConnectionEditParts都應(yīng)該有這個策略,即使連接的兩端都不能拖動。至少這個策略提供了一種視覺上的選擇反饋。propertyChange方法可以收到
線風格屬性的變化,并對線figure作相應(yīng)的調(diào)整。
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
類。這個類是圖形編輯器的一種特殊形式,它本身也是一種編輯部件,并可以擁有一個提供工具的面板。使用這個類必須實現(xiàn)兩個方法,getPaletteRoot
和getPalettePreferences
。第一個方法必須返回包含所有工具選項的面板的根節(jié)點。工具選項是一種特殊的面板選項,它將工具安裝在編輯器的編輯域上。它們必須位于面板抽屜中,面板抽屜將工具選項很方便地組合起來。一般推薦有一個工具選項作為整個工具面板的缺省選項。一個典型的解決方法就是直接使用SelectionToolEntry
類的實例。第二個方法返回的面板首選項中包含的內(nèi)容有,報告面板是可見還是被折疊起來了,面板停靠的位置,以及面板的寬度。通常的解決方法是將它們存在plug-in的首選項存儲區(qū)中。
我們上面提到的編輯域起了一個中心控制器的作用。它負責保存工具,載入缺省工具,維護當前激活的工具,并將鼠標和鍵盤事件轉(zhuǎn)發(fā)給當前激活的工具,以及處理命令棧。GEF提供了缺省實現(xiàn),DefaultEditDomain
,你應(yīng)該在編輯器的構(gòu)造函數(shù)中設(shè)置它的實例。
圖形編輯器的另一部分工作是創(chuàng)建并初始化圖形viewer。圖形viewer是一種特殊的EditPartViewer
,它能夠做點擊測試。我們可以使用GraphicalEditor
類提供的缺省viewer。然而,還是需要做一些事。在configureGraphicalViewer
方法
EditPartFactory,這個接口只有一個方法,createEditPart(EditPart, Object)。它的第一個參數(shù)是編輯部件,它一般是所創(chuàng)建的編輯部件的父部件,第二個參數(shù)是新創(chuàng)建的編輯部件所對應(yīng)的模型部件。其他要做的包括設(shè)置鍵處理器,上下文菜單等。
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());
}
一旦工廠類被設(shè)置,你應(yīng)該在圖形viewer中
IEditorInput
實例恢復(fù)得到的對象,IEditorInput
實例通過setInput
方法傳遞給編輯器。這個例子在圖形viewer上添加TemplateTransferDropTargetListener
的子類,它使用CreateRequest
來獲得添加對象到模型的命令,這個模型當然就是拖放動作結(jié)束時所在的編輯部件所表示的模型。除了上面談到的任務(wù),編輯器還負責監(jiān)視命令棧來報告當前編輯的內(nèi)容是否被修改。這是一個比較好的解決方法,因為它可以使這個標記和用戶所做的undo和redo同步起來。注意,命令棧含有上次存儲的位置信息,這個信息在doSave
和doSaveAs
這兩個方法中被標記。編輯器的其他細節(jié),比如模型的實際存儲和恢復(fù),這里就不討論了,因為它們和具體的應(yīng)用相關(guān)。接下來,我們討論編輯器的如何將編輯器內(nèi)容暴露給其視圖,如何將菜單選項和編輯器的action聯(lián)系起來,以及其他workbench協(xié)作的技術(shù)。
到目前為止,我們談的都是這個幾何圖形編輯器如何工作。然而,它沒有和workbench很好地集成。例如,Edit菜單動作,比如Delete,Undo和Redo,就不能工作。其他視圖不能用其他方式顯示編輯器內(nèi)容。換句話說,目前所完成的編輯器沒有很好地利用Eclipse workbench的優(yōu)勢。在下面的三小節(jié),將解釋如何將這個孤立的編輯器變成workbench的一部分。
ShapesEditor
類創(chuàng)建了大量缺省動作,它們在編輯器初始化過程中被createActions
方法中創(chuàng)建。這些動作是undo,redo,select all,delete,save和print。為了將標準菜單選項連接到這些動作,你應(yīng)該在plugin.xml
文件中定義一個action bar contributor。在這個action bar contributor中,你需要實現(xiàn)兩個方法。第一個是
buildActions
,它可以為undo,redo和delete創(chuàng)建可重定位的動作。如果你需要使用鍵盤選擇所有的widget,你需要在第二個方法declareGlobalActionKeys
中為所選擇的動作
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());
}
}
我們來仔細看一下當用戶在Edit菜單中選擇Delete時發(fā)生了些什么(看圖12)。ShapesEditor
類的父類將刪除動作添加到動作注冊表中。當刪除動作被執(zhí)行時,它檢查當前的所選擇的對象是否是EditPart
類的實例。對每個對象,它都從編輯部件中請求一個命令。接下來,每個編輯部件檢查是否有編輯策略可以處理刪除請求。對幾何圖形,ShapeComponentEditPolicy
可以處理刪除請求,并且返回ShapeDeleteCommand
實例。動作執(zhí)行該命令,從而將圖形從圖中刪除。圖發(fā)送一個屬性修改事件,DiagramEditPart
收到該事件,最終使得代表被刪除圖形的矩形或橢圓從顯示中被刪除。
每個圖形編輯器都是可以發(fā)送選擇事件。你可以建立一個視圖,并將它作為選擇監(jiān)聽器注冊在workbench site的頁面上。每次你在圖形編輯器中選擇一個對象,你的視圖都會在selectionChanged
方法中收到一個通知。Eclipse的一個標準視圖,Properties視圖,會監(jiān)聽選擇事件,并且每次都檢查這個對象是否實現(xiàn)了IPropertySource
接口。如果是的話,它使用這個接口的方法來查詢所選擇的對象屬性,并以表格的方式顯示出來。
通過上面所描述的,在圖形編輯器中提供對象的屬性只要實現(xiàn)IPropertySource
接口就可以了。通過查看Shape
類,你可以看到對象的位置和大小是如何在Properties視圖中顯示的。
Outline視圖是另一種,常常也是更簡潔的查看模型對象的方式。在Java編輯器中,它可以用來顯示一個類所import的類,所包含的變量,和方法,卻不需要用戶深入代碼。圖形編輯器也可以使用這個視圖。圖形編輯器和邏輯電路編輯器類似,可以以樹的方式顯示所編輯的內(nèi)容(看圖1)。數(shù)據(jù)庫schema編輯器[7]也提供了類似的視圖。
為了將所編輯的內(nèi)容提供給Outline視圖,你需要重寫getAdapter
方法,并當adapter類為IContentOutlinePage
接口時,返回一個outline實現(xiàn)。實現(xiàn)outline的最簡單的方法是擴展ContentOutlinePage
類,并提供適當?shù)?code>EditPartViewer。
public Object getAdapter(Class type) {
// returns the content outline page for this editorif (type == IContentOutlinePage.class) {
if (outlinePage == null) {
outlinePage = new ShapesEditorOutlinePage(this, new TreeViewer());
}
return outlinePage;
}
return super.getAdapter(type);
}
在我們這個例子中,編輯部件視圖是有一個TreeViewer實現(xiàn)的。你應(yīng)該和主編輯器一樣提供給它同樣的編輯域。TreeViewer,就象其他EditPartViewer
,需要一個創(chuàng)建子編輯部件的方法。編輯器和DiagramEditPart
一樣,都是設(shè)置一個編輯部件工廠。此外,outline中的選擇和主編輯窗口的選擇需要通過選擇同步器同步起來,選擇同步器是一個GEF工具類,它協(xié)調(diào)兩個編輯部件的選擇狀態(tài)。ShapesTreeEditPartFactory
根據(jù)模型類型,返回ShapeTreeEditPart
或DiagramTreeEditPart
的實例。通過這些類,讀者應(yīng)該可以輕易地發(fā)現(xiàn)這些模式很熟悉。兩個編輯部件都實現(xiàn)了PropertyChangeListener
接口,并通過調(diào)整模型的可視化表示來對屬性變化做出響應(yīng)。它們都安裝編輯策略來控制通過它們所暴露的交互類型。
GEF通過大量使用設(shè)計模式來得到它的靈活性。下面是一下經(jīng)常碰到的模式的小結(jié)。詳細內(nèi)容,請參考 [2]。
IFigure
接口??刂频念愋捅仨毷?code>EditPart或它的子類。Commands
,這些Commands
以鏈的方式組織在一起。createChild
方法允許你不使用工廠就創(chuàng)建子編輯部件。我希望能夠?qū)@個簡單圖形編輯器的大多數(shù)方面作詳細的描述。提供足夠的信息使得人們能夠讀完這篇文章,去看更大的例子,比如邏輯電路編輯器。通過理解象CircuitEditPart
,AndGateFigure和其他類的角色,你可以關(guān)注其他例子的更復(fù)雜的方面。在GEF的眾多領(lǐng)域和技術(shù)中,有很多我甚至都沒有涉及過。然而,這些技術(shù)只有在很好地理解基礎(chǔ)內(nèi)容的情況下,才可能去學(xué)習。畢竟,如果你為了使Select All菜單項工作都要花數(shù)小時,那么設(shè)計一個拖反饋的目的又是什么呢?
我想感謝Randy Hudson,他的意見幫助提高了本文結(jié)構(gòu)和準確性。我也感謝Jill Sueoka仔細檢查我所寫一個又一個版本。