[Eclipse]GEF入門系列(七、XYLayout和展開/摺疊功能)

前面的帖子裏曾說過如何使用佈局,當時主要集中在ToolbarLayout和FlowLayout(統稱OrderedLayout),還有很多應用程序使用的是可以自由拖動子圖形的佈局,在GEF裏稱爲XYLayout,而且這樣的應用多半會需要在圖形之間建立一些連接線,比如下圖所示的情景。連接的出現在一定程度上增加了模型的複雜度,連接線的刷新也是GEF關注的一個問題,這裏就主要討論這類應用的實現,並將特別討論一下展開/摺疊(expand/collapse)功能的實現。請點這裏下載本篇示例代碼。

xylayout.gif
圖1 使用XYLayout的應用程序

還是從模型開始說起,使用XYLayout時,每個子圖形對應的模型要維護自身的座標和尺寸信息,這就在模型裏引入了一些與實際業務無關的成員變量。爲了解決這個問題,一般我們是讓所有需要具有這些界面信息的模型元素繼承自一個抽象類(如Node),而這個類裏提供如point、dimension等變量和getter/setter方法:

None.gif public class Node extends Element implements IPropertySource {
None.gif    protected Point location 
=   new  Point( 0 0 ); // 位置
None.gif
    protected Dimension size  =   new  Dimension( 100 150 ); // 尺寸
None.gif
    protected String name  =   " Node " ; // 標籤
None.gif
    protected List outputs  =   new  ArrayList( 5 ); // 節點作爲起點的連接
None.gif    
protected List inputs  =   new  ArrayList( 5 ); // 節點作爲終點的連接
None.gif

None.gif}

EditPart方面也是一樣的,如果你的應用程序裏有多個需要自由拖動和改變大小的EditPart,那麼最好提供一個抽象的EditPart(如NodePart),在這個類裏實現propertyChange()、createEditPolicy()、active()、deactive()和refreshVisuals()等常用方法的缺省實現,如果子類需要擴展某個方法,只要先調用super()再寫自己的擴展代碼即可,典型的NodePart代碼如下所示,注意它是NodeEditPart的子類,後者是GEF專爲具有連接功能的節點提供的EditPart:

None.gif public abstract class NodePart extends AbstractGraphicalEditPart implements PropertyChangeListener, NodeEditPart {
None.gif    public 
void  propertyChange(PropertyChangeEvent evt) {
None.gif        
if  (evt.getPropertyName().equals(Node.PROP_LOCATION))
None.gif            refreshVisuals();
None.gif        
else   if  (evt.getPropertyName().equals(Node.PROP_SIZE))
None.gif            refreshVisuals();
None.gif        
else   if  (evt.getPropertyName().equals(Node.PROP_INPUTS))
None.gif            refreshTargetConnections();
None.gif        
else   if  (evt.getPropertyName().equals(Node.PROP_OUTPUTS))
None.gif            refreshSourceConnections();
None.gif    }

None.gif    protected 
void  createEditPolicies() {
None.gif        installEditPolicy(EditPolicy.COMPONENT_ROLE, 
new  NodeEditPolicy());
None.gif        installEditPolicy(EditPolicy.GRAPHICAL_NODE_ROLE, 
new  NodeGraphicalNodeEditPolicy());
None.gif    }

None.gif    public 
void  activate() {…}
None.gif    public 
void  deactivate() {…}

None.gif    protected 
void  refreshVisuals() {
None.gif        Node node 
=  (Node) getModel();
None.gif        Point loc 
=  node.getLocation();
None.gif        Dimension size 
=   new  Dimension(node.getSize());
None.gif        Rectangle rectangle 
=   new  Rectangle(loc, size);
None.gif        ((GraphicalEditPart) getParent()).setLayoutConstraint(
this , getFigure(), rectangle);
None.gif    }

None.gif    
// 以下是NodeEditPart中抽象方法的實現
None.gif
    public ConnectionAnchor getSourceConnectionAnchor(ConnectionEditPart connection) {
None.gif        
return   new  ChopBoxAnchor (getFigure());
None.gif    }
None.gif    public ConnectionAnchor getSourceConnectionAnchor(Request request) {
None.gif        
return   new  ChopBoxAnchor (getFigure());
None.gif    }
None.gif    public ConnectionAnchor getTargetConnectionAnchor(ConnectionEditPart connection) {
None.gif        
return   new  ChopBoxAnchor (getFigure());
None.gif    }
None.gif    public ConnectionAnchor getTargetConnectionAnchor(Request request) {
None.gif        
return   new  ChopBoxAnchor(getFigure());
None.gif    }
None.gif    protected List getModelSourceConnections() {
None.gif        
return  ((Node)  this .getModel()).getOutgoingConnections();
None.gif    }
None.gif    protected List getModelTargetConnections() {
None.gif        
return  ((Node)  this .getModel()).getIncomingConnections();
None.gif    }
None.gif}

從代碼裏可以看到,NodePart已經通過安裝兩個EditPolicy實現關於圖形刪除、移動和改變尺寸的功能,所以具體的NodePart只要繼承這個類就自動擁有了這些功能,當然模型得是Node的子類纔可以。在GEF應用程序裏我們應該善於利用繼承的方式來簡化開發工作。代碼後半部分中的幾個getXXXAnchor()方法是用來規定連接線錨點(Anchor)的,這裏我們使用了在Draw2D那篇帖子裏介紹過的ChopBoxAnchor作爲錨點,它是Draw2D自帶的。而代碼最後兩個方法的返回值則規定了以這個EditPart爲起點和終點的連接列表,列表中每一個元素都應該是Connection類型,這個類是模型的一部分,接下來就要說到。

在GEF裏,節點間的連接線也需要有自己的模型和對應的EditPart,所以這裏我們需要定義Connection和ConnectionPart這兩個類,前者和其他模型元素沒有什麼區別,它維護source和target兩個節點變量,代表連接的起點和終點;ConnectionPart繼承於GEF的AbstractConnectionPart類,請看下面的代碼:

None.gif public class ConnectionPart extends AbstractConnectionEditPart {
None.gif    protected IFigure createFigure() {
None.gif        PolylineConnection conn 
=   new  PolylineConnection();
None.gif        conn.setTargetDecoration(
new  PolygonDecoration());
None.gif        conn.setConnectionRouter(
new  BendpointConnectionRouter());
None.gif        
return  conn;
None.gif    }

None.gif    protected 
void  createEditPolicies() {
None.gif        installEditPolicy(EditPolicy.COMPONENT_ROLE, 
new  ConnectionEditPolicy());
None.gif        installEditPolicy(EditPolicy.CONNECTION_ENDPOINTS_ROLE, 
new  ConnectionEndpointEditPolicy());
None.gif    }

None.gif    protected 
void  refreshVisuals() {
None.gif    }

None.gif    public 
void  setSelected( int  value) {
None.gif        super.setSelected(value);
None.gif        
if  (value  !=  EditPart.SELECTED_NONE)
None.gif            ((PolylineConnection) getFigure()).setLineWidth(
2 );
None.gif        
else
None.gif            ((PolylineConnection) getFigure()).setLineWidth(
1 );
None.gif    }
None.gif}

在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()方法,提供了用於實現各種編輯目的的命令:

None.gif public class NodeGraphicalNodeEditPolicy extends GraphicalNodeEditPolicy {
None.gif    protected Command getConnectionCompleteCommand(CreateConnectionRequest request) {
None.gif        ConnectionCreateCommand command 
=  (ConnectionCreateCommand) request.getStartCommand();
None.gif        command.setTarget((Node) getHost().getModel());
None.gif        
return  command;
None.gif    }

None.gif    protected Command getConnectionCreateCommand(CreateConnectionRequest request) {
None.gif        ConnectionCreateCommand command 
=   new  ConnectionCreateCommand();
None.gif        command.setSource((Node) getHost().getModel());
None.gif        request.setStartCommand(command);
None.gif        
return  command;
None.gif    }

None.gif    protected Command getReconnectSourceCommand(ReconnectRequest request) {
None.gif        
return   null ;
None.gif    }

None.gif    protected Command getReconnectTargetCommand(ReconnectRequest request) {
None.gif        
return   null ;
None.gif    }
None.gif}

因爲是針對節點的,所以這裏面都是和連接線有關的方法,因爲只有節點才需要連接線。這些方法名稱的意義都很明顯:getConnectionCreateCommand()是當用戶選擇了連接線工具並點中一個節點時調用,getConnectionCompleteCommand()是在用戶選擇了連接終點時調用,getReconnectSourceCommand()和getReconnectTargetCommand()則分別是在用戶拖動一個連接線的起點/終點到其他節點上時調用,這裏我們返回null表示不提供改變連接端點的功能。關於命令(Command)本身,我想沒有必要做詳細說明了,基本上只要搞清了模型之間的關係,命令就很容易寫出來,請下載例子後自己查看。

下面應郭奕朋友的要求說一說如何實現容器(Container)的摺疊/展開功能。在有些應用裏,畫布中的圖形還能夠包含子圖形,這種圖形稱爲容器(畫布本身當然也是容器),爲了讓畫布看起來更簡潔,可以讓容器具有"摺疊"和"展開"兩種狀態,當摺疊時只顯示部分信息,不顯示子圖形,展開時則顯示完整的容器和子圖形,見圖2和圖3,本例中各模型元素的包含關係是Diagram->Subject->Attribute。

expand.gif
圖2 容器Subject3處於展開狀態

要爲Subject增加展開/摺疊功能主要存在兩個問題需要考慮:一是如何隱藏容器裏的子圖形,並改變容器的外觀,我採取的方法是在需要摺疊/展開的時候改變容器圖形,將contentPane也就是包含子圖形的那個圖形隱藏起來,從而達到隱藏子圖形的目的;二是與容器包含的子圖形相連的連接線的處理,因爲子圖形有可能與其他容器或容器中的子圖形之間存在連接線,例如圖2中Attribute4與Attribute6之間的連接線,這些連接線在摺疊狀態下應該連接到子圖形所在容器上才符合邏輯(例如在Subject3摺疊後,原來從Attribute4到Attribute6的連接應該變成從Subject3到Atribute6的連接,見圖3)。

collapse.gif
圖3 容器Subject3處於摺疊狀態

現在一個一個來解決。首先,不論容器處於什麼狀態,都應該只是視圖上的變化,而不是模型中的變化(例如摺疊後的容器中沒有顯示子圖形不代表模型中的容器不包含子圖形),但在容器模型中要有一個表示狀態的布爾型變量collapsed(初始值爲false),用來指示EditPart刷新視圖。假設我們希望用戶雙擊一個容器可以改變它的展開/摺疊狀態,那麼在容器的EditPart(例子裏的SubjectPart)裏要覆蓋performRequest()方法改變容器的狀態值:

None.gif public  void  performRequest(Request req) {
None.gif    
if  (req.getType()  ==  RequestConstants.REQ_OPEN)
None.gif        getSubject().setCollapsed(
! getSubject().isCollapsed());
None.gif}

注意這個狀態值的改變是會觸發所有監聽器的propertyChange()方法的,而SubjectPart正是這樣一個監聽器,所以在它的propertyChange()方法裏要增加對這個新屬性變化事件的處理代碼,判斷當前狀態隱藏或顯示contantPane:

None.gif public  void  propertyChange(PropertyChangeEvent evt) {
None.gif    
if  (Subject.PROP_COLLAPSED.equals(evt.getPropertyName())) {
None.gif        SubjectFigure figure 
=  ((SubjectFigure) getFigure());
None.gif        
if  ( ! getSubject().isCollapsed()) {
None.gif            figure.add(getContentPane());
None.gif        } 
else  {
None.gif            figure.remove(getContentPane());
None.gif        }
None.gif        refreshVisuals();
None.gif        refreshSourceConnections();
None.gif        refreshTargetConnections();
None.gif    }
None.gif    
if  (Subject.PROP_STRUCTURE.equals(evt.getPropertyName()))
None.gif        refreshChildren();
None.gif    super.propertyChange(evt);
None.gif}

爲了讓容器顯示不同的圖標以反應摺疊狀態,在SubjectPart的refreshVisuals()方法裏要做額外的工作,如下所示:

None.gif protected  void  refreshVisuals() {
None.gif    super.refreshVisuals();
None.gif    SubjectFigure figure 
=  (SubjectFigure) getFigure();
None.gif    figure.setName(((Node) 
this .getModel()).getName());
None.gif    
if  ( ! getSubject().isCollapsed()) {
None.gif        figure.setIcon(SubjectPlugin.getImage(IConstants.IMG_FILE));
None.gif    } 
else  {
None.gif        figure.setIcon(SubjectPlugin.getImage(IConstants.IMG_FOLDER));
None.gif    }
None.gif}

因爲摺疊後的容器圖形應該變小,所以我讓Subject對象覆蓋了Node對象的getSize()方法,在摺疊狀態時返回一個固定的Dimension對象,該值就決定了Subject摺疊狀態的圖形尺寸,如下所示:

None.gif protected Dimension collapsedDimension  =   new  Dimension( 80 50 );
None.gifpublic Dimension getSize() {
None.gif    
if  ( ! isCollapsed())
None.gif        
return  super.getSize();
None.gif    
else
None.gif        
return  collapsedDimension;
None.gif}

上面的幾段代碼更改解決了第一個問題,第二個問題要稍微麻煩一些。爲了在不同狀態下返回正確的連接,我們要修改getModelSourceConnections()方法和getModelTargetConnections()方法,前面已經說過,這兩個方法的作用是返回與節點相關的連接對象列表,我們要做的就是讓它們根據節點的當前狀態返回正確的連接,所以作爲容器的SubjectPart要做這樣的修改:

None.gif protected List getModelSourceConnections() {
None.gif    
if  ( ! getSubject().isCollapsed()) {
None.gif        
return  getSubject().getOutgoingConnections();
None.gif    } 
else  {
None.gif        List l 
=   new  ArrayList();
None.gif        l.addAll(getSubject().getOutgoingConnections());
None.gif        
for  (Iterator iter  =  getSubject().getAttributes().iterator(); iter.hasNext();) {
None.gif            Attribute attribute 
=  (Attribute) iter.next();
None.gif            l.addAll(attribute.getOutgoingConnections());
None.gif        }
None.gif        
return  l;
None.gif    }
None.gif}

也就是說,當處於展開狀態時,正常返回自己作爲起點的那些連接;否則除了這些連接以外,還要包括子圖形對應的那些連接。作爲子圖形的AttributePart也要修改,因爲當所在容器摺疊後,它們對應的連接也要隱藏,修改後的代碼如下所示:

None.gif protected List getModelSourceConnections() {
None.gif    Attribute attribute 
=  (Attribute) getModel();
None.gif    Subject subject 
=  (Subject) ((SubjectPart) getParent()).getModel();
None.gif    
if  ( ! subject.isCollapsed()) {
None.gif        
return  attribute.getOutgoingConnections();
None.gif    } 
else  {
None.gif        
return  Collections.EMPTY_LIST;
None.gif    }
None.gif}

由於getModelTargetConnections()的代碼和getModelSourceConnections()非常類似,這裏就不列出其內容了。在一般情況下,我們只讓一個EditPart監聽一個模型的變化,但是請記住,GEF框架並沒有規定EditPart與被監聽的模型一一對應(實際上GEF中的很多設計就是爲了減少對開發人員的限制),因此在必要時我們大可以根據自己的需要靈活運用。在實現展開/摺疊功能時,子元素的EditPart應該能夠監聽所在容器的狀態變化,當collapsed值改變時更新與子圖形相關的連接線(若不進行更新則這些連接線會變成"無頭線")。讓子元素EditPart監聽容器模型的變化很簡單,只要在AttributePart的activate()裏把自己作爲監聽器加到容器模型的監聽器列表即可,注意別忘記在deactivate()裏註銷掉,而propertyChange()方法裏是事件發生時的處理,代碼如下:

None.gif public  void  activate() {
None.gif    super.activate();
None.gif    ((Attribute) getModel()).addPropertyChangeListener(
this );
None.gif    ((Subject) getParent().getModel()).addPropertyChangeListener(
this );
None.gif}
None.gifpublic 
void  deactivate() {
None.gif    super.deactivate();
None.gif    ((Attribute) getModel()).removePropertyChangeListener(
this );
None.gif    ((Subject) getParent().getModel()).removePropertyChangeListener(
this );
None.gif}
None.gifpublic 
void  propertyChange(PropertyChangeEvent evt) {
None.gif    
if  (evt.getPropertyName().equals(Subject.PROP_COLLAPSED)) {
None.gif        refreshSourceConnections();
None.gif        refreshTargetConnections();
None.gif    }
None.gif    super.propertyChange(evt);
None.gif}

這樣,基本上就實現了容器的展開/摺疊功能,之所以說"基本上",是因爲我沒有做仔細的測試(時間關係),目前的代碼有可能會存在問題,特別是在Undo/Redo以及多重選擇這些情況下;另外,這種方法只適用於容器裏的子元素不是容器的情況,如果有多層的容器關係,則每一層都要做類似的處理纔可以。

本文轉自博客園八進制的博客,原文鏈接:[Eclipse]GEF入門系列(七、XYLayout和展開/摺疊功能),如需轉載請自行聯繫原博主。