前面的帖子裏曾說過如何使用佈局,當時主要集中在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和展開/摺疊功能),如需轉載請自行聯繫原博主。