天天看點

GEF 進階,第一部分: Anchor

第一部分: Anchor

GEF(Graphical Editing Framework)是Eclipse Tools的子項目,它在底層使用Draw2D作為布局和渲染引擎,在整體上使用MVC模式管理模型和視圖。利用GEF,開發者可以從應用模型開始,迅速的構造一個可視化編輯環境。正如其名字所說,它隻是一個架構,很多具體的事情仍然要靠開發者完成,但這也是GEF靈活的一方面,隻要你掌握了相關的概念,你就可以對一個GEF應用進行充分的定制。本系列的目的就是介紹GEF的相關概念,并在GEF的一些示例程式的基礎上示範如何定制、擴充自己的GEF應用。這是本系列的第一章,主要介紹了Anchor(錨點)的概念,以及如何自定義一個錨點并替代GEF預設實作。

Anchor(錨點)

在一個典型的GEF程式中,我們通常會在畫闆上放上一些圖形,然後用一些線連接配接這些圖形。這些線的兩個端點就是Anchor(錨點),而錨點所在的圖形叫做錨點的Owner。更細化的說,一條線的起點叫做Source Anchor(源錨點),終結點叫做Target Anchor(目标錨點)。如圖1中的黑色小方塊所示。

圖1. 源錨點和目标錨點

GEF 進階,第一部分: Anchor

不難看出,錨點的具體位置和兩個圖形的位置以及連線的方式有關,這兩個前提确定之後,錨點可以通過一定的方法計算得出。對于圖1的情況,兩個圖形之間的連線是由兩個圖形的中心點确定的,那麼錨點的計算方法就是找到這條中心線和圖形邊界的交點。Draw2D預設為我們提供了一些Anchor的實作,最常用的大概是ChopboxAnchor,它隻是簡單的取兩個圖形的中心線和圖形邊界的交點做為錨點(正是圖1 的情況)。對于簡單的應用來說,ChopboxAnchor可以滿足我們的需要,但是它的錨點計算方法導緻錨點在任何時候都是唯一的,如果這兩個圖形之間存在多條連線,它們會互相重疊使得看上去隻有一條,于是使用者不可能用滑鼠選擇到被覆寫的連線。

解決這個問題的辦法有兩個:

1. 提供一個自定義的Connection Router(連線路由器),以便能盡量避免線之間的重合,甚至也可以每條線都有不同的Router。

2. 實作一個自定義的錨點,可以讓使用者自己拖動錨點到不同的位置,避免線之間的重合

對于方法1,我們在以後的系列中會有介紹。這裡我們考慮方法2。

Shapes Example

GEF的Shapes示例是一個很基礎的GEF程式,使用者可以在其上放置橢圓和長方形的圖形,然後可以用兩種樣式的線連接配接它們。由于其使用了ChopboxAnchor,它不支援在兩個圖形之間建立多條連線,也不能移動錨點。我們将在它的基礎上實作一個可移動的錨點。

第一步,确定錨點的表示政策

設計自定義Anchor的第一個問題是"我想把什麼位置做為Anchor?",比如對于一個矩形,你可以選擇圖形的中心,或者四條邊的中心,或者邊界上的任何點。在我們這個例子裡,我們希望是橢圓邊界的任何點。是以我們可以考慮用角度來表示Anchor的位置,如圖2所示:

圖2. Anchor的表示方式

GEF 進階,第一部分: Anchor

我們可以用一個變量表示角度,進而計算出中心射線與邊界的交點,把這個交點作為圖形的錨點。通過這樣的方式,邊界上的任一點都可以成為錨點,可以通過手工調整錨點,避免連線重疊。

第二步,修改Model

為了表示錨點,我們需要一個表示角度的變量,這個變量應該放到模型中以便能夠把錨點資訊記錄到檔案中。對于一條來說,它有兩個錨點,是以應該在連線對象中添加兩個成員,在Shapes例子中,連線對象是org.eclipse.gef.examples.shapes.model.Connection, 我們修改它添加兩個成員和相應的Getter和Setter方法:

private double sourceAngle;
private double targetAngle;

public double getSourceAngle() {
	return sourceAngle;
}

public void setSourceAngle(double sourceAngle) {
	this.sourceAngle = sourceAngle;
}

public double getTargetAngle() {
	return targetAngle;
}

public void setTargetAngle(double targetAngle) {
	this.targetAngle = targetAngle;
}
      

sourceAngle儲存了源錨點的角度,targetAngle儲存了目标錨點的角度,使用弧度表示。

第三步,實作ConnectionAnchor接口

錨點的接口是由org.eclipse.draw2d.ConnectionAnchor定義的,我們需要實作這個接口,但是一般來說我們不用從頭開始,可以通過繼承其它類來減少我們的工作。由于存在橢圓和長方形兩種圖形,是以我們還需要實作兩個子類。最終我們定義了基礎類BorderAnchor和RectangleBorderAnchor,EllipseBorderAnchor兩個子類。BorderAnchor的代碼如下:

package org.eclipse.gef.examples.shapes.anchor;

import org.eclipse.draw2d.ChopboxAnchor;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.geometry.Point;

public abstract class BorderAnchor extends ChopboxAnchor {
	protected double angle;
	
	public BorderAnchor(IFigure figure) {
		super(figure);
		angle = Double.MAX_VALUE;
	}
	
	public abstract Point getBorderPoint(Point reference);
	
	public Point getLocation(Point reference) {
		// 如果angle沒有被初始化,使用預設的ChopboxAnchor,否則計算一個邊界錨點
		if(angle == Double.MAX_VALUE)
			return super.getLocation(reference);
		else
			return getBorderPoint(reference);
	}
	
	public double getAngle() {
		return angle;
	}

	public void setAngle(double angle) {
		this.angle = angle;
	}
}
      

重要的是getLocation()方法,它有一個參數"Point reference",即一個參考點,在計算錨點時,我們可以根據參考點來決定錨點的位置,對于ChopboxAnchor來說,參考點就是另外一個圖形的中心點。BorderAnchor類有一個angle成員,儲存了錨點的角度,它會被初始化為Double.MAX_VALUE,是以我們判斷angle是否等于Double.MAX_VALUE,如果是則BorderAnchor相當于一個ChopboxAnchor,如果否則調用一個抽象方法getBorderPoint()來計算我們的錨點。BorderAnchor的兩個子類分别實作了計算橢圓和長方形錨點的算法,EllipseBorderAnchor的代碼如下所示:

package org.eclipse.gef.examples.shapes.anchor;

import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.PrecisionPoint;
import org.eclipse.draw2d.geometry.Rectangle;

public class EllipseBorderAnchor extends BorderAnchor {
	public EllipseBorderAnchor(IFigure figure) {
		super(figure);
	}

	@Override
	public Point getBorderPoint(Point reference) {
		//得到owner矩形,轉換為絕對坐标
		Rectangle r = Rectangle.SINGLETON;
		r.setBounds(getOwner().getBounds());
		getOwner().translateToAbsolute(r);
		
		// 橢圓方程和直線方程,解2元2次方程
		double a = r.width >> 1;
		double b = r.height >> 1;
		double k = Math.tan(angle);		
		double dx = 0.0, dy = 0.0;
		
		dx = Math.sqrt(1.0 / (1.0 / (a * a) + k * k / (b * b)));
		if(angle > Math.PI / 2 || angle < -Math.PI / 2)
			dx = -dx;
		dy = k * dx;
		
		// 得到橢圓中心點,加上錨點偏移,得到最終錨點坐标
		PrecisionPoint pp = new PrecisionPoint(r.getCenter());	
		pp.translate((int)dx, (int)dy);
		return new Point(pp);	
	}
}
      

值的注意的地方是我們可以通過getOwner().getBounds()來得到Owner的邊界矩形,這是我們能夠計算出錨點的重要前提。此外我們要注意的是必須把坐标轉換為絕對坐标,這是通過getOwner().translateToAbsolute(r)來實作的。最後,我們傳回了錨點的絕對坐标,中間的具體計算過程隻不過是根據橢圓方程和射線方程求值而已。在我們的實作中,并沒有用到參考點,如果你想有更多的變數,可以把參考點考慮進去。

同樣,RectangleBorderAnchor也是如此,隻不過求長方形邊界點的方法稍微不一樣而已,我們就不一一解釋了,代碼如下:

package org.eclipse.gef.examples.shapes.anchor;

import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.PrecisionPoint;
import org.eclipse.draw2d.geometry.Rectangle;

public class RectangleBorderAnchor extends BorderAnchor {

	public RectangleBorderAnchor(IFigure figure) {
		super(figure);
	}

	@Override
	public Point getBorderPoint(Point reference) {
		// 得到owner矩形,轉換為絕對坐标
		Rectangle r = Rectangle.SINGLETON;
		r.setBounds(getOwner().getBounds());
		getOwner().translateToAbsolute(r);
		
		// 根據角度,計算錨點相對于owner中心點的偏移
		double dx = 0.0, dy = 0.0;
		double tan = Math.atan2(r.height, r.width);
		if(angle >= -tan && angle <= tan) {
			dx = r.width >> 1;
			dy = dx * Math.tan(angle);
		} else if(angle >= tan && angle <= Math.PI - tan) {
			dy = r.height >> 1;
			dx = dy / Math.tan(angle);
		} else if(angle <= -tan && angle >= tan - Math.PI) {
			dy = -(r.height >> 1);
			dx = dy / Math.tan(angle);
		} else {
			dx = -(r.width >> 1);
			dy = dx * Math.tan(angle);
		}

		// 得到長方形中心點,加上偏移,得到最終錨點坐标
		PrecisionPoint pp = new PrecisionPoint(r.getCenter());	
		pp.translate((int)dx, (int)dy);
		return new Point(pp);	
	}
}
      

這樣我們就完成了自定義的錨點實作。在ConnectionAnchor接口中,還有其他4個方法,雖然我們沒有用到,但是有必要了解一下它們:

void addAnchorListener(AnchorListener listener);
void removeAnchorListener(AnchorListener listener);
Point getReferencePoint();
IFigure getOwner();
      

addAnchorListener()和removeAnchorListener()可以添加或删除一個錨點監聽器,這樣我們可以知道錨點何時發生了移動。getOwner()則是傳回錨點的Onwer圖形,顯然我們可以指定另外一個圖形為錨點的Owner,雖然這種需求可能不太多。而getReferencePoint()則是傳回一個參考點,要注意的是,這個參考點不是給自己用的,而是給另外一個錨點用的。比如對于源錨點來說,它會調用目标錨點的getReferencePoint()方法,而對于目标錨點來說,它會調用源錨點的getReferencePoint()方法。我們可以看看ChopboxAnchor的getReferencePoint()實作,它傳回的就是它的Owner的中心。

第四步,修改EditPart

錨點實作完成後,我們需要修改ShapeEditPart使它能夠使用我們定義的錨點。EditPart中的getSourceConnectionAnchor(ConnectionEditPart connection)和getTargetConnectionAnchor(ConnectionEditPart connection)是決定使用哪種錨點的關鍵方法。它們還有一個重載版本,用來處理Reconnect時的錨點更新。這四個方法我們都需要修改,同時為了減少對象建立的次數,我們可以在ConnectionEditPart裡面添加兩個成員用來儲存源錨點對象和目标錨點對象,如下:

/* In ConnectionEditPart.java */

private BorderAnchor sourceAnchor;
private BorderAnchor targetAnchor;

public BorderAnchor getSourceAnchor() {
	return sourceAnchor;
}

public void setSourceAnchor(BorderAnchor sourceAnchor) {
	this.sourceAnchor = sourceAnchor;
}

public BorderAnchor getTargetAnchor() {
	return targetAnchor;
}

public void setTargetAnchor(BorderAnchor targetAnchor) {
	this.targetAnchor = targetAnchor;
}
      
這樣的話,在ShapeEditPart中應該檢查一下ConnectionEditPart中的成員是否有效,如果有效則直接傳回,無效則建立一個新的錨點對象。而Reconnect時的代碼稍微複雜一些,我們需要根據滑鼠的目前位置,重新計算angle的值,滑鼠的目前位置是包含在ReconnectRequest裡面的。我們給出getSourceConnectionAnchor()的代碼,對于getTargetConnectionAnchor(),隻要将Source換成Target即可。
/* In ShapeEditPart.java */

/*
 * (non-Javadoc)
 * @see org.eclipse.gef.NodeEditPart#getSourceConnectionAnchor
 (org.eclipse.gef.ConnectionEditPart)
 */
public ConnectionAnchor getSourceConnectionAnchor
(ConnectionEditPart connection) {
	org.eclipse.gef.examples.shapes.parts.ConnectionEditPart con =
	(org.eclipse.gef.examples.shapes.parts.ConnectionEditPart)connection;
	BorderAnchor anchor = con.getSourceAnchor();
	if(anchor == null || anchor.getOwner() != getFigure()) {
		if(getModel() instanceof EllipticalShape)
			anchor = new EllipseBorderAnchor(getFigure());
		else if(getModel() instanceof RectangularShape)
			anchor = new RectangleBorderAnchor(getFigure());
		else
			throw new IllegalArgumentException("unexpected model");
		
		Connection conModel = (Connection)con.getModel();
		anchor.setAngle(conModel.getSourceAngle());
		con.setSourceAnchor(anchor);
	}
	return anchor;
}

/*
 * (non-Javadoc)
 * @see org.eclipse.gef.NodeEditPart#getSourceConnectionAnchor
 (org.eclipse.gef.Request)
 */
public ConnectionAnchor getSourceConnectionAnchor(Request request) {
	if(request instanceof ReconnectRequest) {
		ReconnectRequest r = (ReconnectRequest)request;
		org.eclipse.gef.examples.shapes.parts.ConnectionEditPart con =
		(org.eclipse.gef.examples.shapes.parts.ConnectionEditPart)r.
		getConnectionEditPart();
		Connection conModel = (Connection)con.getModel();
		BorderAnchor anchor = con.getSourceAnchor();
		GraphicalEditPart part = (GraphicalEditPart)r.getTarget();
		if(anchor == null || anchor.getOwner() != part.getFigure()) {
			if(getModel() instanceof EllipticalShape)
				anchor = new EllipseBorderAnchor(getFigure());
			else if(getModel() instanceof RectangleBorderAnchor)
				anchor = new RectangleBorderAnchor(getFigure());
			else
				throw new IllegalArgumentException("unexpected model");
			
			anchor.setAngle(conModel.getSourceAngle());
			con.setSourceAnchor(anchor);
		}
		
		Point loc = r.getLocation();
		Rectangle rect = Rectangle.SINGLETON;
		rect.setBounds(getFigure().getBounds());
		getFigure().translateToAbsolute(rect);
		Point ref = rect.getCenter();
		double dx = loc.x - ref.x;
		double dy = loc.y - ref.y;
		anchor.setAngle(Math.atan2(dy, dx));	
		conModel.setSourceAngle(anchor.getAngle());
		return anchor;		
	} else {
		if(getModel() instanceof EllipticalShape)
			return new EllipseBorderAnchor(getFigure());
		else if(getModel() instanceof RectangularShape)
			return new RectangleBorderAnchor(getFigure());
		else
			throw new IllegalArgumentException("unexpected model");
	}
}
      

到這裡我們的修改就完成了,但是由于Shapes示例不允許建立多條連線,是以我們還需要把ConnectionCreateCommand和ConnectionReconnectCommand中的一些代碼注釋掉,這個内容就不做更多介紹了,大家可以下載下傳本文附帶的代碼檢視具體的修改。最終,我們修改後的Shapes可以建立多條連線,并且可以手動調整它們的錨點以避免重疊,如圖3所示:

圖3. 新的Shapes示例

GEF 進階,第一部分: Anchor

結束語

一個靈活的錨點實作對于複雜的圖形編輯程式來說是必須的,我們所要做的僅僅隻是實作ConnectionAnchor接口。本文實作的BorderAnchor是一個通用的錨點實作,你可以随意應用到自己的GEF程式中。或者在此基礎上實作更為靈活的錨點功能。

注:轉自IBM網站,版權歸原作者所有

GEF 進階,第一部分: Anchor
GEF 進階,第一部分: Anchor

繼續閱讀