[Eclipse]GEF入门系列(七、XYLayout和展开/折叠功能)
前面的帖子里曾說過如何使用布局,當時主要集中在ToolbarLayout和FlowLayout(統稱OrderedLayout),還有很多應用程序使用的是可以自由拖動子圖形的布局,在GEF里稱為XYLayout,而且這樣的應用多半會需要在圖形之間建立一些連接線,比如下圖所示的情景。連接的出現在一定程度上增加了模型的復雜度,連接線的刷新也是GEF關注的一個問題,這里就主要討論這類應用的實現,并將特別討論一下展開/折疊(expand/collapse)功能的實現。請點這里下載本篇示例代碼。
圖1 使用XYLayout的應用程序
還是從模型開始說起,使用XYLayout時,每個子圖形對應的模型要維護自身的坐標和尺寸信息,這就在模型里引入了一些與實際業務無關的成員變量。為了解決這個問題,一般我們是讓所有需要具有這些界面信息的模型元素繼承自一個抽象類(如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";//標簽
????protected?List?outputs?=?new?ArrayList(5);//節點作為起點的連接
????protected?List?inputs?=?new?ArrayList(5);//節點作為終點的連接
…
}
EditPart方面也是一樣的,如果你的應用程序里有多個需要自由拖動和改變大小的EditPart,那么最好提供一個抽象的EditPart(如NodePart),在這個類里實現propertyChange()、createEditPolicy()、active()、deactive()和refreshVisuals()等常用方法的缺省實現,如果子類需要擴展某個方法,只要先調用super()再寫自己的擴展代碼即可,典型的NodePart代碼如下所示,注意它是NodeEditPart的子類,后者是GEF專為具有連接功能的節點提供的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中抽象方法的實現
????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已經通過安裝兩個EditPolicy實現關于圖形刪除、移動和改變尺寸的功能,所以具體的NodePart只要繼承這個類就自動擁有了這些功能,當然模型得是Node的子類才可以。在GEF應用程序里我們應該善于利用繼承的方式來簡化開發工作。代碼后半部分中的幾個getXXXAnchor()方法是用來規定連接線錨點(Anchor)的,這里我們使用了在Draw2D那篇帖子里介紹過的ChopBoxAnchor作為錨點,它是Draw2D自帶的。而代碼最后兩個方法的返回值則規定了以這個EditPart為起點和終點的連接列表,列表中每一個元素都應該是Connection類型,這個類是模型的一部分,接下來就要說到。
在GEF里,節點間的連接線也需要有自己的模型和對應的EditPart,所以這里我們需要定義Connection和ConnectionPart這兩個類,前者和其他模型元素沒有什么區別,它維護source和target兩個節點變量,代表連接的起點和終點;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()里可以指定你想要的連接線類型,箭頭的樣式,以及連接線的路由(走線)方式,例如走直線或是直角折線等等。我們為ConnectionPart安裝了一個角色為EditPolicy.CONNECTION_ENDPOINTS_ROLE的ConnectionEndpointEditPolicy,安裝它的目的是提供連接線的選擇、端點改變等功能,注意這個類是GEF內置的。另外,我們并沒有把ConnectionPart作為監聽器,在refreshVisuals()里也沒有做任何事情,因為連接線的刷新是在與它連接的節點的刷新里通過調用refreshSourceConnections()和refreshTargetConnections()方法完成的。最后,通過覆蓋setSelected()方法,我們可以定義連接線被選中后的外觀,上面代碼可以讓被選中的連接線變粗。
看完了模型和Editpart,現在來說說EditPolicy。我們知道,GEF提供的每種GraphicalEditPolicy都是與布局有關的,你在容器圖形(比如畫布)里使用了哪種布局,一般就應該選擇對應的EditPolicy,因為這些EditPolicy需要對布局有所了解,這樣才能提供拖動feedback等功能。使用XYLayout作為布局時,子元素被稱為節點(Node),對應的EditPolicy是GraphicalNodeEditPolicy,在前面NodePart的代碼中我們給它安裝的角色為EditPolicy.GRAPHICAL_NODE_ROLE的NodeGraphicalNodeEditPolicy就是這個類的一個子類。和所有EditPolicy一樣,NodeGraphicalNodeEditPolicy里也有一系列getXXXCommand()方法,提供了用于實現各種編輯目的的命令:
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;
????}
}
因為是針對節點的,所以這里面都是和連接線有關的方法,因為只有節點才需要連接線。這些方法名稱的意義都很明顯:getConnectionCreateCommand()是當用戶選擇了連接線工具并點中一個節點時調用,getConnectionCompleteCommand()是在用戶選擇了連接終點時調用,getReconnectSourceCommand()和getReconnectTargetCommand()則分別是在用戶拖動一個連接線的起點/終點到其他節點上時調用,這里我們返回null表示不提供改變連接端點的功能。關于命令(Command)本身,我想沒有必要做詳細說明了,基本上只要搞清了模型之間的關系,命令就很容易寫出來,請下載例子后自己查看。
下面應郭奕朋友的要求說一說如何實現容器(Container)的折疊/展開功能。在有些應用里,畫布中的圖形還能夠包含子圖形,這種圖形稱為容器(畫布本身當然也是容器),為了讓畫布看起來更簡潔,可以讓容器具有"折疊"和"展開"兩種狀態,當折疊時只顯示部分信息,不顯示子圖形,展開時則顯示完整的容器和子圖形,見圖2和圖3,本例中各模型元素的包含關系是Diagram->Subject->Attribute。
圖2 容器Subject3處于展開狀態
要為Subject增加展開/折疊功能主要存在兩個問題需要考慮:一是如何隱藏容器里的子圖形,并改變容器的外觀,我采取的方法是在需要折疊/展開的時候改變容器圖形,將contentPane也就是包含子圖形的那個圖形隱藏起來,從而達到隱藏子圖形的目的;二是與容器包含的子圖形相連的連接線的處理,因為子圖形有可能與其他容器或容器中的子圖形之間存在連接線,例如圖2中Attribute4與Attribute6之間的連接線,這些連接線在折疊狀態下應該連接到子圖形所在容器上才符合邏輯(例如在Subject3折疊后,原來從Attribute4到Attribute6的連接應該變成從Subject3到Atribute6的連接,見圖3)。
圖3 容器Subject3處于折疊狀態
現在一個一個來解決。首先,不論容器處于什么狀態,都應該只是視圖上的變化,而不是模型中的變化(例如折疊后的容器中沒有顯示子圖形不代表模型中的容器不包含子圖形),但在容器模型中要有一個表示狀態的布爾型變量collapsed(初始值為false),用來指示EditPart刷新視圖。假設我們希望用戶雙擊一個容器可以改變它的展開/折疊狀態,那么在容器的EditPart(例子里的SubjectPart)里要覆蓋performRequest()方法改變容器的狀態值:
public?void?performRequest(Request?req)?{????if?(req.getType()?==?RequestConstants.REQ_OPEN)
????????getSubject().setCollapsed(!getSubject().isCollapsed());
}
注意這個狀態值的改變是會觸發所有監聽器的propertyChange()方法的,而SubjectPart正是這樣一個監聽器,所以在它的propertyChange()方法里要增加對這個新屬性變化事件的處理代碼,判斷當前狀態隱藏或顯示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);
}
為了讓容器顯示不同的圖標以反應折疊狀態,在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));
????}
}
因為折疊后的容器圖形應該變小,所以我讓Subject對象覆蓋了Node對象的getSize()方法,在折疊狀態時返回一個固定的Dimension對象,該值就決定了Subject折疊狀態的圖形尺寸,如下所示:
protected?Dimension?collapsedDimension?=?new?Dimension(80,?50);public?Dimension?getSize()?{
????if?(!isCollapsed())
????????return?super.getSize();
????else
????????return?collapsedDimension;
}
上面的幾段代碼更改解決了第一個問題,第二個問題要稍微麻煩一些。為了在不同狀態下返回正確的連接,我們要修改getModelSourceConnections()方法和getModelTargetConnections()方法,前面已經說過,這兩個方法的作用是返回與節點相關的連接對象列表,我們要做的就是讓它們根據節點的當前狀態返回正確的連接,所以作為容器的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;
????}
}
也就是說,當處于展開狀態時,正常返回自己作為起點的那些連接;否則除了這些連接以外,還要包括子圖形對應的那些連接。作為子圖形的AttributePart也要修改,因為當所在容器折疊后,它們對應的連接也要隱藏,修改后的代碼如下所示:
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()非常類似,這里就不列出其內容了。在一般情況下,我們只讓一個EditPart監聽一個模型的變化,但是請記住,GEF框架并沒有規定EditPart與被監聽的模型一一對應(實際上GEF中的很多設計就是為了減少對開發人員的限制),因此在必要時我們大可以根據自己的需要靈活運用。在實現展開/折疊功能時,子元素的EditPart應該能夠監聽所在容器的狀態變化,當collapsed值改變時更新與子圖形相關的連接線(若不進行更新則這些連接線會變成"無頭線")。讓子元素EditPart監聽容器模型的變化很簡單,只要在AttributePart的activate()里把自己作為監聽器加到容器模型的監聽器列表即可,注意別忘記在deactivate()里注銷掉,而propertyChange()方法里是事件發生時的處理,代碼如下:
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);
}
這樣,基本上就實現了容器的展開/折疊功能,之所以說"基本上",是因為我沒有做仔細的測試(時間關系),目前的代碼有可能會存在問題,特別是在Undo/Redo以及多重選擇這些情況下;另外,這種方法只適用于容器里的子元素不是容器的情況,如果有多層的容器關系,則每一層都要做類似的處理才可以。
總結
以上是生活随笔為你收集整理的[Eclipse]GEF入门系列(七、XYLayout和展开/折叠功能)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [导入]关于怎样通过xslt向.NET扩
- 下一篇: 好消息,MaxtoCode完全支持200