圖形和圖形之間的連線是怎么實現的?模型的屬性又是怎么顯示在屬性頁上的?這一章會通過我們的Database creator例子一一解答。代碼下載在第4章。
1.Connection和ConnectionEditPart
大家都知道,數據庫表之間可能存在著某種約束關系,在大多數DB Visual Tool中是用線之間的連接來表示的,最典型的莫過于SQL Server了,很多開發人員一定不會陌生。
讓我們在DatabaseCreator中來實現這種效果。
在GEF 中,專門有AbstractConnectionEditPart這么一個類,它就之專門維護圖形連接的。和其他的EditPart一樣,也需要有自己的模型和Figure。一個連接并不是獨立存在的,它并不僅僅是一條線,在它線段兩端都是有EditPart分為作為連接的“連接源”(Source)和 “連接目標”(Target),我們現在暫時稱這兩種EditPart為SourceEidtPart和TargetEditPart。Source是指,連接是從該EditPart出發的,到達另一個EditPart結束;Target相反,表示從別的EditPart出發,到自己這里作為結束。
我們的DBCreator需要連接的是Column,因為我認為,表之間的約束其實就是Column的聯系,所以在這里我就將Column作為了連接的對象。
讓我們看看生成的Connection和ConnectionEditPart先:
public
?
class
?Connection?extends?DBBase?{
????
private
?DBBase?source;
????
private
?DBBase?target;
????
????
public
?Connection(DBBase?source,DBBase?target){
????????
this
.source?
=
?source;
????????
this
.target?
=
?target;
????????((Column)source).addOut(
this
);
????????((Column)?target).addIncome(
this
);
????}
??}
?
public
?
class
?ConnectionEditPart?extends?AbstractConnectionEditPart?{
????
protected
?IFigure?createFigure()?{
????????PolylineConnection?conn?
=
?
new
?PolylineConnection();
????????conn.setConnectionRouter(
new
?BendpointConnectionRouter());
????????conn.setTargetDecoration(
new
?PolygonDecoration());
????????
return
?conn;
????}
????
protected
?
void
?createEditPolicies()?{
????????installEditPolicy(EditPolicy.CONNECTION_ENDPOINTS_ROLE,?
new
?ConnectionEndpointEditPolicy());
????}
}
ConnectionEditPart 類繼承了AbstractConnectionEditPart類,返回的Figure是一個PolylineConnection,然后又安裝了GEF 提供的ConnectionEndpointEditPolicy。該類沒有太多特殊的地方,僅僅是一個很普通的連接EditPart。
解讀一下Connection類。剛才說了,連接分為source和target,所以,一個連接需要有兩個Column作為參數,并且一個作為 source另一個作為target。在構造函數中,我們調用了Column的addOut和addIncome,這兩個方法是新加進去的,分別是為了存放該Column作為source或target時的Connection模型,為什么要去保存Connection呢?
一個 EditPart可能會和許多EditPart進行連接,有時候作為“連接源”,有時候是“連接目標”,每一對Source和Target組成一個連接。在EditPart中有兩個方法:?getModelSourceConnections()和? getModelTargetConnections(),這兩個方法分別讓EditPart返回其作為“連接源”和“連接目標”時的連接模型。
是不是很繞啊?呵呵,想想看,以前我們說過,EditPart為了能夠得到自己的子EditPart,我們需要復寫getModelChildren方法,讓該方法返回EditPart所具有的子EditPart對應的模型,其實可以這么認為:EditPart為了能夠得知自己的子EditPart是什么,所以需要通過取得他們的模型,然后再利用我們實現的EditPartFactory來構造這些子EditPart。
同樣, EditPart怎么知道它的連接的呢?而且還必須知道自己是作為Source還是Target?當然就是通過 getModelSourceConnections()和getModelTargetConnections() 得到Connection模型,然后再利用工廠生成EditPart的了。
這下明白了吧,我們在Column中去保存連接是有計劃、有目的D~:
?
public
?List?getModelSourceConnections()?{
????????
//
?TODO?Auto-generated?method?stub
????????
return
?((Column)getModel()).getOuts();
????}
????
public
?List?getModelTargetConnections()?{
????????
//
?TODO?Auto-generated?method?stub
????????
return
?((Column)getModel()).getIncomes();
????}
2.錨點(Anchor)
要想成為能夠進行連接的EditPart,還需要實現NodeEditPart接口,NodeEditPart接口提供了一些和連接相關的接口方法讓 EditPart去實現,實際上,有大部分接口方法都已經在AbstraceGraphicalEditPart中實現了,我們只要實現它的這四個方法:
??? public ConnectionAnchor getSourceConnectionAnchor(ConnectionEditPart connection);
???
??? public ConnectionAnchor getTargetConnectionAnchor(ConnectionEditPart connection);
???
??? public ConnectionAnchor getSourceConnectionAnchor(Request request) ;
??
??? public ConnectionAnchor getTargetConnectionAnchor(Request request) ;
看得出來,這四個方法都需要讓我們返回ConnectionAnchor類型的對象,這是連接的錨點。
連接其實就是一條線,它繪制在畫布上的時候,是從某一個圖形開始,然后到另一個圖形結束,那它從圖形的什么位置開始,到另個圖形的什么位置結束呢?兩點確定一條直線,這兩點具體是什么位置呢?
連接錨點就是為我們的Connection提供其圖形在“連接源”或者在“連接目標”圖形上的點坐標位置。?
錨點的類型很多,它通過我們輸入的圖形,然后根據錨點的特點計算出連接線的起始和結束的具體位置。常用的錨點這幾個類:ChopboxAnchor、EllipseAnchor、LabelAnchor。
我們需要實現的連接,是從Column的一端(左右都可以)開始到另一個Column圖形的一端,連接點不能出現在Column的上方或者下方,并且,由于我們的Column圖形是在TableFigure里面的,為了美觀,我們還不能讓連接線畫到TableFigure里面,只能在它矩形之外。所以我們需要自己來寫這個Anchor:
public
?
class
?LeftOrRightAnchor?extends?ChopboxAnchor?{
????
public
?LeftOrRightAnchor(IFigure?owner)?{
????????super(owner);
????}
????
public
?Point?getLocation(Point?reference)?{
????????Point?p;
????????p?
=
?getOwner().getBounds().getCenter();
????????getOwner().translateToAbsolute(p);
????????
if
?(reference.x?
<
?p.x){
????????????p?
=
?getOwner().getBounds().getLeft();
????????????p.x?
-=
?
8
;
????????}
????????
else
{
????????????p?
=
?getOwner().getBounds().getRight();
????????????p.x?
+=
?
8
;
????????}
????????getOwner().translateToAbsolute(p);
????????
return
?p;
????}
}
其實這段代碼是我無意中搜索出來的,也不知道是誰寫的,就拿來用了,:)
這里返回的Point是在ColumnFigure的左右兩邊,并且分別增多移出了8個象素,這是為了讓連接點在TableFigure外面。
錨點生成了,我們實現上述的getSourceConnectionAnchor()等四個方法中都返回這個錨點。
3.生成連接的Command、ToolEntry以及連接用的EditPolicyToolEntry 就不多說了,實現很簡單:創建一個繼承自CreationToolEntry的類,然后復寫它的createTool方法,讓它返回GEF提供的 ConnectionCreationTool,然后把該ToolEntry添加到PaletteDrawer中即可。
Command也很簡單,如上面所說的,一個連接需要Source和Target,我們只要指定好Source和Target,然后在Command的執行方法(excute)中,實例化我們的Connection模型即可。
EditPolicy是給ColumnEditPart安裝上的,先看代碼:
public?class?ColumnNodeEditPolicy?extends?GraphicalNodeEditPolicy?{
????/*?(non-Javadoc)
?????*?@see?org.eclipse.gef.editpolicies.GraphicalNodeEditPolicy#getConnectionCompleteCommand(org.eclipse.gef.requests.CreateConnectionRequest)
?????*/
????protected?Command?getConnectionCompleteCommand(CreateConnectionRequest?request)?{
????????CreateConnectionCommand?command?=?(CreateConnectionCommand)request.getStartCommand();
????????if(command?==?null)?return?null;
????????command.setTarget((Column)getHost().getModel());
????????
????????return?command;
????}
????protected?Command?getConnectionCreateCommand(CreateConnectionRequest?request)?{
????????CreateConnectionCommand?command?=?new?CreateConnectionCommand();
????????command.setSource((Column)getHost().getModel());
????????request.setStartCommand(command);
????????return?command;
????}
}
方法getConnectionCreateCommand,是當我們開始進行連接時,點擊了連接源的時候觸發的。我們在方法中實力化了我們的 CreateConnectionCommand,然后把連接源指定到Command中,跟著我們再把該Command放置到request的 setStartCommand中去。為什么要設置到setStartCommand中呢?看另一個方法。
getConnectionCompleteCommand是我們完成一次連接,也就是連接到了連接目標后觸發的。這時候,我們就可以從request中取出剛才設置的Command了,呵呵,這就是為什么要在連接開始前把command設置進request中。
取出command后把target設置好,然后返回。這時候command才開始被執行。剛才說了command執行是實力化我們的Connection,看看Connection的構造函數,它自身分別被添加到Source和Target的連接中了。
我已經在添加連接的方法中發送了屬性改變事件,等到ColumnEditPart截獲的時候會去調用refreshTargetConnections或refreshSourceConnections方法。
這樣一來,我們的連接就做好了。看看:

4.屬性顯示在普通的EclipsePlugin開發中,要顯示屬性頁很麻煩,需要我們設置PropertySheetPage,而且還要給他設置好 sheetentry,并且為了能顯示出屬性,還要指定好selection,而且這個selection中攜帶的模型還需要實現 IPropertySource。或者是給SheetPage指定PropertySourceProvider……反正一個字:麻煩!
有了GEF提供的現成的東西就好了。“自從我用了GEF,腰不酸了,腿不疼了……”
要在GEF中顯示出屬性,只需要讓我們的模型實現IPropertySource即可,其他的事情您就不要操心了。
IPropertySource中有一些我們必須實現的方法:
getPropertyDescriptors
這個方法是返回我們需要顯示的propertydescriptor,也就是屬性視圖中每一條屬性對應的控件。常用的有這幾種Descritor: TextPropertyDescriptor,ComboBoxPropertyDescriptor, ColorDialogPropertyDescriptor。
getPropertyValue
該方法返回給property視圖一個可編輯的值,這個值是我們通過方法傳入的ID來進行識別并返回的。一會我們會有具體實現。
setPropertyValue
和上面的方法相反,這個方法是通過ID讓我們設置模型的值。
大家可以看到,我們將Table的tableName和Table在視圖上的坐標作為屬性顯示在屬性視圖上,這兩個屬性對應的 PropetyDescriptor分別是:TextPropertyDescriptor和PropertyDescriptor,而且還對應了各自的 ID。
先看看tableName屬性。在getPropertyValue中,我們對id進行判斷,如果和該屬性對應的id相同,我們就返回Table模型的表名給它;同樣,在setPropertyValue中,如果是設置表名的話,我們就直接把Table的名字設為傳入的值。
我們為tableName設置的PropertyDescriptor是一個TextPropertyDescriptor,這是一個專門進行文本編輯的屬性editor。下面簡單說說它的工作原理。
其實每一個PropertyDescriptor里面注冊有自己的CellEditor,CellEditor就是編輯屬性值的控件。CellEditor 是需要我們給它傳入它能顯示或者說能體現該屬性的值的,所以為什么我們在getPropertyValue中進行判斷id,然后返回值,這些都是給 CellEditor準備的。getPropertyValue返回的值傳給了這些CellEditor,他們會對這些值進行顯示,當然, CellEditor顯示這些值并不是直接就以String的形式顯示出來,它要通過自己的LabelProvier來獲得這些值的顯示方式。但是默認的情況下,LabelProvider直接是讓這些值顯示toString方法獲得的字符串。如果我們需要特殊地對屬性值進行顯示的話,就需要給這些 CellEditor重新注冊ILabelProvider。我在代碼中也是這樣做的,有興趣的朋友可以看看代碼是如何實現的。
這里有一個問題需要注意,CellEditor獲得值是通過doSetValue方法來取得的,當我們返回給維護CellEditor的某個類(具體哪個類我不太清楚)值的時候,它會調用CellEditor的doSetValue方法。不同的CellEditor它能接受的值類型也有差別,舉個例子: TextCellEditor能夠接受的值是String類型的,但是ComboBoxCellEditor只能接受Integer類型的值,同樣 ColorDialogCellEditor只能接受RGB類型。
所以我們在getPropertyValue方法中,需要根據ID以及注冊的PropertyDescriptor的不同來返回不同類型的值。當然,我們得到的值也和輸入值類型一至。
再看看Point屬性是怎么回事。在返回PropertyDescriptor時,我們給Point屬性返回的是PropertyDescrptor,這是因為Point值是有x,y組成的,我稱這種屬性為“復合屬性”,讓Descriptor返回propertyDescriptor的目的是為了讓屬性頁顯示出的是一個類似樹型控件那樣可以打開的控件,然后我們創建一個類,實現IPropertyDescriptor接口,這個過程就有點類似于為我們的模型生成屬性一樣,只要在getEditableValue方法中準確地返回屬性需要的值即可。
類似的“復合屬性”有很多,最常見的有對于Point類型和Dimension類型的。我寫了這兩中屬性的實現,大家可以下載參考一下:
PointPropertySource?? DimensionPropertySource在屬性中還有一個比較實用的就是驗證器了。沒一個Descriptor都可以設置自己的驗證器。比如,我們要輸入坐標,坐標輸入只能是數字,那我們就需要利用驗證器幫我們驗證。當驗證器發現輸入有錯誤的時候就會在編輯器下放的狀態欄中提醒我們.
驗證器實現很簡單:
?/P>
public?class?NumberCellEditorValidator?implements?ICellEditorValidator?{
????private?static??NumberCellEditorValidator?instance?=?new?NumberCellEditorValidator();
????
????private?NumberCellEditorValidator(){}
????
????public?static?NumberCellEditorValidator?INSTANCE(){
????????return?instance;
????}
????public?String?isValid(Object?value)?{
?????????stub
????????try{
????????????new?Integer((String)value);
????????????return?null;
????????}catch(Exception?e){
????????????return?"請輸入數字"?;
????????}
????}
}
驗證器最好是作為一個單態類存在,同類屬性Descriptor可以共用一個,以免浪費內存.
5.結束語
這章我們簡單介紹了一下Property頁和Connection的實現,以后的文章里我們會更詳細介紹一下一些特殊技巧的使用,比如制作FontDialogPropertyDescriptor,以及Connection中線的改變等。