程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> [Eclipse]GEF入門系列(七、XYLayout和展開/折疊功能)

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

編輯:關於JAVA

前面的帖子裡曾說過如何使用布局,當時主要集中在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以及多重選擇這些 情況下;另外,這種方法只適用於容器裡的子元素不是容器的情況,如果有多層的容器關系 ,則每一層都要做類似的處理才可以。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved