[Eclipse]GEF入门系列(七、XYLayout和展开/折叠功能)
前面的帖子里曾說過如何使用布局,當(dāng)時主要集中在ToolbarLayout和FlowLayout(統(tǒng)稱OrderedLayout),還有很多應(yīng)用程序使用的是可以自由拖動子圖形的布局,在GEF里稱為XYLayout,而且這樣的應(yīng)用多半會需要在圖形之間建立一些連接線,比如下圖所示的情景。連接的出現(xiàn)在一定程度上增加了模型的復(fù)雜度,連接線的刷新也是GEF關(guān)注的一個問題,這里就主要討論這類應(yīng)用的實現(xiàn),并將特別討論一下展開/折疊(expand/collapse)功能的實現(xiàn)。請點這里下載本篇示例代碼。
圖1 使用XYLayout的應(yīng)用程序
還是從模型開始說起,使用XYLayout時,每個子圖形對應(yīng)的模型要維護(hù)自身的坐標(biāo)和尺寸信息,這就在模型里引入了一些與實際業(yè)務(wù)無關(guān)的成員變量。為了解決這個問題,一般我們是讓所有需要具有這些界面信息的模型元素繼承自一個抽象類(如Node),而這個類里提供如point、dimension等變量和getter/setter方法:
public?class?Node?extends?Element?implements?IPropertySource?{????protected?Point?location?=?new?Point(0,?0);//位置
????protected?Dimension?size?=?new?Dimension(100,?150);//尺寸
????protected?String?name?=?"Node";//標(biāo)簽
????protected?List?outputs?=?new?ArrayList(5);//節(jié)點作為起點的連接
????protected?List?inputs?=?new?ArrayList(5);//節(jié)點作為終點的連接
…
}
EditPart方面也是一樣的,如果你的應(yīng)用程序里有多個需要自由拖動和改變大小的EditPart,那么最好提供一個抽象的EditPart(如NodePart),在這個類里實現(xiàn)propertyChange()、createEditPolicy()、active()、deactive()和refreshVisuals()等常用方法的缺省實現(xiàn),如果子類需要擴展某個方法,只要先調(diào)用super()再寫自己的擴展代碼即可,典型的NodePart代碼如下所示,注意它是NodeEditPart的子類,后者是GEF專為具有連接功能的節(jié)點提供的EditPart:
public?abstract?class?NodePart?extends?AbstractGraphicalEditPart?implements?PropertyChangeListener,?NodeEditPart?{????public?void?propertyChange(PropertyChangeEvent?evt)?{
????????if?(evt.getPropertyName().equals(Node.PROP_LOCATION))
????????????refreshVisuals();
????????else?if?(evt.getPropertyName().equals(Node.PROP_SIZE))
????????????refreshVisuals();
????????else?if?(evt.getPropertyName().equals(Node.PROP_INPUTS))
????????????refreshTargetConnections();
????????else?if?(evt.getPropertyName().equals(Node.PROP_OUTPUTS))
????????????refreshSourceConnections();
????}
????protected?void?createEditPolicies()?{
????????installEditPolicy(EditPolicy.COMPONENT_ROLE,?new?NodeEditPolicy());
????????installEditPolicy(EditPolicy.GRAPHICAL_NODE_ROLE,?new?NodeGraphicalNodeEditPolicy());
????}
????public?void?activate()?{…}
????public?void?deactivate()?{…}
????protected?void?refreshVisuals()?{
????????Node?node?=?(Node)?getModel();
????????Point?loc?=?node.getLocation();
????????Dimension?size?=?new?Dimension(node.getSize());
????????Rectangle?rectangle?=?new?Rectangle(loc,?size);
????????((GraphicalEditPart)?getParent()).setLayoutConstraint(this,?getFigure(),?rectangle);
????}
????//以下是NodeEditPart中抽象方法的實現(xiàn)
????public?ConnectionAnchor?getSourceConnectionAnchor(ConnectionEditPart?connection)?{
????????return?new?ChopBoxAnchor?(getFigure());
????}
????public?ConnectionAnchor?getSourceConnectionAnchor(Request?request)?{
????????return?new?ChopBoxAnchor?(getFigure());
????}
????public?ConnectionAnchor?getTargetConnectionAnchor(ConnectionEditPart?connection)?{
????????return?new?ChopBoxAnchor?(getFigure());
????}
????public?ConnectionAnchor?getTargetConnectionAnchor(Request?request)?{
????????return?new?ChopBoxAnchor(getFigure());
????}
????protected?List?getModelSourceConnections()?{
????????return?((Node)?this.getModel()).getOutgoingConnections();
????}
????protected?List?getModelTargetConnections()?{
????????return?((Node)?this.getModel()).getIncomingConnections();
????}
}
從代碼里可以看到,NodePart已經(jīng)通過安裝兩個EditPolicy實現(xiàn)關(guān)于圖形刪除、移動和改變尺寸的功能,所以具體的NodePart只要繼承這個類就自動擁有了這些功能,當(dāng)然模型得是Node的子類才可以。在GEF應(yīng)用程序里我們應(yīng)該善于利用繼承的方式來簡化開發(fā)工作。代碼后半部分中的幾個getXXXAnchor()方法是用來規(guī)定連接線錨點(Anchor)的,這里我們使用了在Draw2D那篇帖子里介紹過的ChopBoxAnchor作為錨點,它是Draw2D自帶的。而代碼最后兩個方法的返回值則規(guī)定了以這個EditPart為起點和終點的連接列表,列表中每一個元素都應(yīng)該是Connection類型,這個類是模型的一部分,接下來就要說到。
在GEF里,節(jié)點間的連接線也需要有自己的模型和對應(yīng)的EditPart,所以這里我們需要定義Connection和ConnectionPart這兩個類,前者和其他模型元素沒有什么區(qū)別,它維護(hù)source和target兩個節(jié)點變量,代表連接的起點和終點;ConnectionPart繼承于GEF的AbstractConnectionPart類,請看下面的代碼:
public?class?ConnectionPart?extends?AbstractConnectionEditPart?{????protected?IFigure?createFigure()?{
????????PolylineConnection?conn?=?new?PolylineConnection();
????????conn.setTargetDecoration(new?PolygonDecoration());
????????conn.setConnectionRouter(new?BendpointConnectionRouter());
????????return?conn;
????}
????protected?void?createEditPolicies()?{
????????installEditPolicy(EditPolicy.COMPONENT_ROLE,?new?ConnectionEditPolicy());
????????installEditPolicy(EditPolicy.CONNECTION_ENDPOINTS_ROLE,?new?ConnectionEndpointEditPolicy());
????}
????protected?void?refreshVisuals()?{
????}
????public?void?setSelected(int?value)?{
????????super.setSelected(value);
????????if?(value?!=?EditPart.SELECTED_NONE)
????????????((PolylineConnection)?getFigure()).setLineWidth(2);
????????else
????????????((PolylineConnection)?getFigure()).setLineWidth(1);
????}
}
在getFigure()里可以指定你想要的連接線類型,箭頭的樣式,以及連接線的路由(走線)方式,例如走直線或是直角折線等等。我們?yōu)镃onnectionPart安裝了一個角色為EditPolicy.CONNECTION_ENDPOINTS_ROLE的ConnectionEndpointEditPolicy,安裝它的目的是提供連接線的選擇、端點改變等功能,注意這個類是GEF內(nèi)置的。另外,我們并沒有把ConnectionPart作為監(jiān)聽器,在refreshVisuals()里也沒有做任何事情,因為連接線的刷新是在與它連接的節(jié)點的刷新里通過調(diào)用refreshSourceConnections()和refreshTargetConnections()方法完成的。最后,通過覆蓋setSelected()方法,我們可以定義連接線被選中后的外觀,上面代碼可以讓被選中的連接線變粗。
看完了模型和Editpart,現(xiàn)在來說說EditPolicy。我們知道,GEF提供的每種GraphicalEditPolicy都是與布局有關(guān)的,你在容器圖形(比如畫布)里使用了哪種布局,一般就應(yīng)該選擇對應(yīng)的EditPolicy,因為這些EditPolicy需要對布局有所了解,這樣才能提供拖動feedback等功能。使用XYLayout作為布局時,子元素被稱為節(jié)點(Node),對應(yīng)的EditPolicy是GraphicalNodeEditPolicy,在前面NodePart的代碼中我們給它安裝的角色為EditPolicy.GRAPHICAL_NODE_ROLE的NodeGraphicalNodeEditPolicy就是這個類的一個子類。和所有EditPolicy一樣,NodeGraphicalNodeEditPolicy里也有一系列g(shù)etXXXCommand()方法,提供了用于實現(xiàn)各種編輯目的的命令:
public?class?NodeGraphicalNodeEditPolicy?extends?GraphicalNodeEditPolicy?{????protected?Command?getConnectionCompleteCommand(CreateConnectionRequest?request)?{
????????ConnectionCreateCommand?command?=?(ConnectionCreateCommand)?request.getStartCommand();
????????command.setTarget((Node)?getHost().getModel());
????????return?command;
????}
????protected?Command?getConnectionCreateCommand(CreateConnectionRequest?request)?{
????????ConnectionCreateCommand?command?=?new?ConnectionCreateCommand();
????????command.setSource((Node)?getHost().getModel());
????????request.setStartCommand(command);
????????return?command;
????}
????protected?Command?getReconnectSourceCommand(ReconnectRequest?request)?{
????????return?null;
????}
????protected?Command?getReconnectTargetCommand(ReconnectRequest?request)?{
????????return?null;
????}
}
因為是針對節(jié)點的,所以這里面都是和連接線有關(guān)的方法,因為只有節(jié)點才需要連接線。這些方法名稱的意義都很明顯:getConnectionCreateCommand()是當(dāng)用戶選擇了連接線工具并點中一個節(jié)點時調(diào)用,getConnectionCompleteCommand()是在用戶選擇了連接終點時調(diào)用,getReconnectSourceCommand()和getReconnectTargetCommand()則分別是在用戶拖動一個連接線的起點/終點到其他節(jié)點上時調(diào)用,這里我們返回null表示不提供改變連接端點的功能。關(guān)于命令(Command)本身,我想沒有必要做詳細(xì)說明了,基本上只要搞清了模型之間的關(guān)系,命令就很容易寫出來,請下載例子后自己查看。
下面應(yīng)郭奕朋友的要求說一說如何實現(xiàn)容器(Container)的折疊/展開功能。在有些應(yīng)用里,畫布中的圖形還能夠包含子圖形,這種圖形稱為容器(畫布本身當(dāng)然也是容器),為了讓畫布看起來更簡潔,可以讓容器具有"折疊"和"展開"兩種狀態(tài),當(dāng)折疊時只顯示部分信息,不顯示子圖形,展開時則顯示完整的容器和子圖形,見圖2和圖3,本例中各模型元素的包含關(guān)系是Diagram->Subject->Attribute。
圖2 容器Subject3處于展開狀態(tài)
要為Subject增加展開/折疊功能主要存在兩個問題需要考慮:一是如何隱藏容器里的子圖形,并改變?nèi)萜鞯耐庥^,我采取的方法是在需要折疊/展開的時候改變?nèi)萜鲌D形,將contentPane也就是包含子圖形的那個圖形隱藏起來,從而達(dá)到隱藏子圖形的目的;二是與容器包含的子圖形相連的連接線的處理,因為子圖形有可能與其他容器或容器中的子圖形之間存在連接線,例如圖2中Attribute4與Attribute6之間的連接線,這些連接線在折疊狀態(tài)下應(yīng)該連接到子圖形所在容器上才符合邏輯(例如在Subject3折疊后,原來從Attribute4到Attribute6的連接應(yīng)該變成從Subject3到Atribute6的連接,見圖3)。
圖3 容器Subject3處于折疊狀態(tài)
現(xiàn)在一個一個來解決。首先,不論容器處于什么狀態(tài),都應(yīng)該只是視圖上的變化,而不是模型中的變化(例如折疊后的容器中沒有顯示子圖形不代表模型中的容器不包含子圖形),但在容器模型中要有一個表示狀態(tài)的布爾型變量collapsed(初始值為false),用來指示EditPart刷新視圖。假設(shè)我們希望用戶雙擊一個容器可以改變它的展開/折疊狀態(tài),那么在容器的EditPart(例子里的SubjectPart)里要覆蓋performRequest()方法改變?nèi)萜鞯臓顟B(tài)值:
public?void?performRequest(Request?req)?{????if?(req.getType()?==?RequestConstants.REQ_OPEN)
????????getSubject().setCollapsed(!getSubject().isCollapsed());
}
注意這個狀態(tài)值的改變是會觸發(fā)所有監(jiān)聽器的propertyChange()方法的,而SubjectPart正是這樣一個監(jiān)聽器,所以在它的propertyChange()方法里要增加對這個新屬性變化事件的處理代碼,判斷當(dāng)前狀態(tài)隱藏或顯示contantPane:
public?void?propertyChange(PropertyChangeEvent?evt)?{????if?(Subject.PROP_COLLAPSED.equals(evt.getPropertyName()))?{
????????SubjectFigure?figure?=?((SubjectFigure)?getFigure());
????????if?(!getSubject().isCollapsed())?{
????????????figure.add(getContentPane());
????????}?else?{
????????????figure.remove(getContentPane());
????????}
????????refreshVisuals();
????????refreshSourceConnections();
????????refreshTargetConnections();
????}
????if?(Subject.PROP_STRUCTURE.equals(evt.getPropertyName()))
????????refreshChildren();
????super.propertyChange(evt);
}
為了讓容器顯示不同的圖標(biāo)以反應(yīng)折疊狀態(tài),在SubjectPart的refreshVisuals()方法里要做額外的工作,如下所示:
protected?void?refreshVisuals()?{????super.refreshVisuals();
????SubjectFigure?figure?=?(SubjectFigure)?getFigure();
????figure.setName(((Node)?this.getModel()).getName());
????if?(!getSubject().isCollapsed())?{
????????figure.setIcon(SubjectPlugin.getImage(IConstants.IMG_FILE));
????}?else?{
????????figure.setIcon(SubjectPlugin.getImage(IConstants.IMG_FOLDER));
????}
}
因為折疊后的容器圖形應(yīng)該變小,所以我讓Subject對象覆蓋了Node對象的getSize()方法,在折疊狀態(tài)時返回一個固定的Dimension對象,該值就決定了Subject折疊狀態(tài)的圖形尺寸,如下所示:
protected?Dimension?collapsedDimension?=?new?Dimension(80,?50);public?Dimension?getSize()?{
????if?(!isCollapsed())
????????return?super.getSize();
????else
????????return?collapsedDimension;
}
上面的幾段代碼更改解決了第一個問題,第二個問題要稍微麻煩一些。為了在不同狀態(tài)下返回正確的連接,我們要修改getModelSourceConnections()方法和getModelTargetConnections()方法,前面已經(jīng)說過,這兩個方法的作用是返回與節(jié)點相關(guān)的連接對象列表,我們要做的就是讓它們根據(jù)節(jié)點的當(dāng)前狀態(tài)返回正確的連接,所以作為容器的SubjectPart要做這樣的修改:
protected?List?getModelSourceConnections()?{????if?(!getSubject().isCollapsed())?{
????????return?getSubject().getOutgoingConnections();
????}?else?{
????????List?l?=?new?ArrayList();
????????l.addAll(getSubject().getOutgoingConnections());
????????for?(Iterator?iter?=?getSubject().getAttributes().iterator();?iter.hasNext();)?{
????????????Attribute?attribute?=?(Attribute)?iter.next();
????????????l.addAll(attribute.getOutgoingConnections());
????????}
????????return?l;
????}
}
也就是說,當(dāng)處于展開狀態(tài)時,正常返回自己作為起點的那些連接;否則除了這些連接以外,還要包括子圖形對應(yīng)的那些連接。作為子圖形的AttributePart也要修改,因為當(dāng)所在容器折疊后,它們對應(yīng)的連接也要隱藏,修改后的代碼如下所示:
protected?List?getModelSourceConnections()?{????Attribute?attribute?=?(Attribute)?getModel();
????Subject?subject?=?(Subject)?((SubjectPart)?getParent()).getModel();
????if?(!subject.isCollapsed())?{
????????return?attribute.getOutgoingConnections();
????}?else?{
????????return?Collections.EMPTY_LIST;
????}
}
由于getModelTargetConnections()的代碼和getModelSourceConnections()非常類似,這里就不列出其內(nèi)容了。在一般情況下,我們只讓一個EditPart監(jiān)聽一個模型的變化,但是請記住,GEF框架并沒有規(guī)定EditPart與被監(jiān)聽的模型一一對應(yīng)(實際上GEF中的很多設(shè)計就是為了減少對開發(fā)人員的限制),因此在必要時我們大可以根據(jù)自己的需要靈活運用。在實現(xiàn)展開/折疊功能時,子元素的EditPart應(yīng)該能夠監(jiān)聽所在容器的狀態(tài)變化,當(dāng)collapsed值改變時更新與子圖形相關(guān)的連接線(若不進(jìn)行更新則這些連接線會變成"無頭線")。讓子元素EditPart監(jiān)聽容器模型的變化很簡單,只要在AttributePart的activate()里把自己作為監(jiān)聽器加到容器模型的監(jiān)聽器列表即可,注意別忘記在deactivate()里注銷掉,而propertyChange()方法里是事件發(fā)生時的處理,代碼如下:
public?void?activate()?{????super.activate();
????((Attribute)?getModel()).addPropertyChangeListener(this);
????((Subject)?getParent().getModel()).addPropertyChangeListener(this);
}
public?void?deactivate()?{
????super.deactivate();
????((Attribute)?getModel()).removePropertyChangeListener(this);
????((Subject)?getParent().getModel()).removePropertyChangeListener(this);
}
public?void?propertyChange(PropertyChangeEvent?evt)?{
????if?(evt.getPropertyName().equals(Subject.PROP_COLLAPSED))?{
????????refreshSourceConnections();
????????refreshTargetConnections();
????}
????super.propertyChange(evt);
}
這樣,基本上就實現(xiàn)了容器的展開/折疊功能,之所以說"基本上",是因為我沒有做仔細(xì)的測試(時間關(guān)系),目前的代碼有可能會存在問題,特別是在Undo/Redo以及多重選擇這些情況下;另外,這種方法只適用于容器里的子元素不是容器的情況,如果有多層的容器關(guān)系,則每一層都要做類似的處理才可以。
總結(jié)
以上是生活随笔為你收集整理的[Eclipse]GEF入门系列(七、XYLayout和展开/折叠功能)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [导入]关于怎样通过xslt向.NET扩
- 下一篇: 好消息,MaxtoCode完全支持200