第一部分: 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. 源錨點和目标錨點
不難看出,錨點的具體位置和兩個圖形的位置以及連線的方式有關,這兩個前提确定之後,錨點可以通過一定的方法計算得出。對于圖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的表示方式
我們可以用一個變量表示角度,進而計算出中心射線與邊界的交點,把這個交點作為圖形的錨點。通過這樣的方式,邊界上的任一點都可以成為錨點,可以通過手工調整錨點,避免連線重疊。
第二步,修改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示例 結束語 一個靈活的錨點實作對于複雜的圖形編輯程式來說是必須的,我們所要做的僅僅隻是實作ConnectionAnchor接口。本文實作的BorderAnchor是一個通用的錨點實作,你可以随意應用到自己的GEF程式中。或者在此基礎上實作更為靈活的錨點功能。 注:轉自IBM網站,版權歸原作者所有 | | |