圖形和圖形之間的連線是怎么實(shí)現(xiàn)的?模型的屬性又是怎么顯示在屬性頁(yè)上的?這一章會(huì)通過(guò)我們的Database creator例子一一解答。代碼下載在第4章。
1.Connection和ConnectionEditPart
大家都知道,數(shù)據(jù)庫(kù)表之間可能存在著某種約束關(guān)系,在大多數(shù)DB Visual Tool中是用線之間的連接來(lái)表示的,最典型的莫過(guò)于SQL Server了,很多開(kāi)發(fā)人員一定不會(huì)陌生。
讓我們?cè)贒atabaseCreator中來(lái)實(shí)現(xiàn)這種效果。
在GEF 中,專門有AbstractConnectionEditPart這么一個(gè)類,它就之專門維護(hù)圖形連接的。和其他的EditPart一樣,也需要有自己的模型和Figure。一個(gè)連接并不是獨(dú)立存在的,它并不僅僅是一條線,在它線段兩端都是有EditPart分為作為連接的“連接源”(Source)和 “連接目標(biāo)”(Target),我們現(xiàn)在暫時(shí)稱這兩種EditPart為SourceEidtPart和TargetEditPart。Source是指,連接是從該EditPart出發(fā)的,到達(dá)另一個(gè)EditPart結(jié)束;Target相反,表示從別的EditPart出發(fā),到自己這里作為結(jié)束。
我們的DBCreator需要連接的是Column,因?yàn)槲艺J(rèn)為,表之間的約束其實(shí)就是Column的聯(lián)系,所以在這里我就將Column作為了連接的對(duì)象。
讓我們看看生成的Connection和ConnectionEditPart先:
???? 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 );
????}
??}
?
???? 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是一個(gè)PolylineConnection,然后又安裝了GEF 提供的ConnectionEndpointEditPolicy。該類沒(méi)有太多特殊的地方,僅僅是一個(gè)很普通的連接EditPart。
解讀一下Connection類。剛才說(shuō)了,連接分為source和target,所以,一個(gè)連接需要有兩個(gè)Column作為參數(shù),并且一個(gè)作為 source另一個(gè)作為target。在構(gòu)造函數(shù)中,我們調(diào)用了Column的addOut和addIncome,這兩個(gè)方法是新加進(jìn)去的,分別是為了存放該Column作為source或target時(shí)的Connection模型,為什么要去保存Connection呢?
一個(gè) EditPart可能會(huì)和許多EditPart進(jìn)行連接,有時(shí)候作為“連接源”,有時(shí)候是“連接目標(biāo)”,每一對(duì)Source和Target組成一個(gè)連接。在EditPart中有兩個(gè)方法:?getModelSourceConnections()和? getModelTargetConnections(),這兩個(gè)方法分別讓EditPart返回其作為“連接源”和“連接目標(biāo)”時(shí)的連接模型。
是不是很繞啊?呵呵,想想看,以前我們說(shuō)過(guò),EditPart為了能夠得到自己的子EditPart,我們需要復(fù)寫getModelChildren方法,讓該方法返回EditPart所具有的子EditPart對(duì)應(yīng)的模型,其實(shí)可以這么認(rèn)為:EditPart為了能夠得知自己的子EditPart是什么,所以需要通過(guò)取得他們的模型,然后再利用我們實(shí)現(xiàn)的EditPartFactory來(lái)構(gòu)造這些子EditPart。
同樣, EditPart怎么知道它的連接的呢?而且還必須知道自己是作為Source還是Target?當(dāng)然就是通過(guò) getModelSourceConnections()和getModelTargetConnections() 得到Connection模型,然后再利用工廠生成EditPart的了。
這下明白了吧,我們?cè)贑olumn中去保存連接是有計(jì)劃、有目的D~:
???????? // ?TODO?Auto-generated?method?stub
???????? return ?((Column)getModel()).getOuts();
????}
???? public ?List?getModelTargetConnections()?{
???????? // ?TODO?Auto-generated?method?stub
???????? return ?((Column)getModel()).getIncomes();
????}
2.錨點(diǎn)(Anchor)
要想成為能夠進(jìn)行連接的EditPart,還需要實(shí)現(xiàn)NodeEditPart接口,NodeEditPart接口提供了一些和連接相關(guān)的接口方法讓 EditPart去實(shí)現(xiàn),實(shí)際上,有大部分接口方法都已經(jīng)在AbstraceGraphicalEditPart中實(shí)現(xiàn)了,我們只要實(shí)現(xiàn)它的這四個(gè)方法:
??? public ConnectionAnchor getSourceConnectionAnchor(ConnectionEditPart connection);
???
??? public ConnectionAnchor getTargetConnectionAnchor(ConnectionEditPart connection);
???
??? public ConnectionAnchor getSourceConnectionAnchor(Request request) ;
??
??? public ConnectionAnchor getTargetConnectionAnchor(Request request) ;
看得出來(lái),這四個(gè)方法都需要讓我們返回ConnectionAnchor類型的對(duì)象,這是連接的錨點(diǎn)。
連接其實(shí)就是一條線,它繪制在畫布上的時(shí)候,是從某一個(gè)圖形開(kāi)始,然后到另一個(gè)圖形結(jié)束,那它從圖形的什么位置開(kāi)始,到另個(gè)圖形的什么位置結(jié)束呢??jī)牲c(diǎn)確定一條直線,這兩點(diǎn)具體是什么位置呢?
連接錨點(diǎn)就是為我們的Connection提供其圖形在“連接源”或者在“連接目標(biāo)”圖形上的點(diǎn)坐標(biāo)位置。?
錨點(diǎn)的類型很多,它通過(guò)我們輸入的圖形,然后根據(jù)錨點(diǎn)的特點(diǎn)計(jì)算出連接線的起始和結(jié)束的具體位置。常用的錨點(diǎn)這幾個(gè)類:ChopboxAnchor、EllipseAnchor、LabelAnchor。
我們需要實(shí)現(xiàn)的連接,是從Column的一端(左右都可以)開(kāi)始到另一個(gè)Column圖形的一端,連接點(diǎn)不能出現(xiàn)在Column的上方或者下方,并且,由于我們的Column圖形是在TableFigure里面的,為了美觀,我們還不能讓連接線畫到TableFigure里面,只能在它矩形之外。所以我們需要自己來(lái)寫這個(gè)Anchor:
???? 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;
????}
}
其實(shí)這段代碼是我無(wú)意中搜索出來(lái)的,也不知道是誰(shuí)寫的,就拿來(lái)用了,:)
這里返回的Point是在ColumnFigure的左右兩邊,并且分別增多移出了8個(gè)象素,這是為了讓連接點(diǎn)在TableFigure外面。
錨點(diǎn)生成了,我們實(shí)現(xiàn)上述的getSourceConnectionAnchor()等四個(gè)方法中都返回這個(gè)錨點(diǎn)。
3.生成連接的Command、ToolEntry以及連接用的EditPolicy
ToolEntry 就不多說(shuō)了,實(shí)現(xiàn)很簡(jiǎn)單:創(chuàng)建一個(gè)繼承自CreationToolEntry的類,然后復(fù)寫它的createTool方法,讓它返回GEF提供的 ConnectionCreationTool,然后把該ToolEntry添加到PaletteDrawer中即可。
Command也很簡(jiǎn)單,如上面所說(shuō)的,一個(gè)連接需要Source和Target,我們只要指定好Source和Target,然后在Command的執(zhí)行方法(excute)中,實(shí)例化我們的Connection模型即可。
EditPolicy是給ColumnEditPart安裝上的,先看代碼:
????/*?(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,是當(dāng)我們開(kāi)始進(jìn)行連接時(shí),點(diǎn)擊了連接源的時(shí)候觸發(fā)的。我們?cè)诜椒ㄖ袑?shí)力化了我們的 CreateConnectionCommand,然后把連接源指定到Command中,跟著我們?cè)侔言揅ommand放置到request的 setStartCommand中去。為什么要設(shè)置到setStartCommand中呢?看另一個(gè)方法。
getConnectionCompleteCommand是我們完成一次連接,也就是連接到了連接目標(biāo)后觸發(fā)的。這時(shí)候,我們就可以從request中取出剛才設(shè)置的Command了,呵呵,這就是為什么要在連接開(kāi)始前把command設(shè)置進(jìn)request中。
取出command后把target設(shè)置好,然后返回。這時(shí)候command才開(kāi)始被執(zhí)行。剛才說(shuō)了command執(zhí)行是實(shí)力化我們的Connection,看看Connection的構(gòu)造函數(shù),它自身分別被添加到Source和Target的連接中了。
我已經(jīng)在添加連接的方法中發(fā)送了屬性改變事件,等到ColumnEditPart截獲的時(shí)候會(huì)去調(diào)用refreshTargetConnections或refreshSourceConnections方法。
這樣一來(lái),我們的連接就做好了。看看:
4.屬性顯示
在普通的EclipsePlugin開(kāi)發(fā)中,要顯示屬性頁(yè)很麻煩,需要我們?cè)O(shè)置PropertySheetPage,而且還要給他設(shè)置好 sheetentry,并且為了能顯示出屬性,還要指定好selection,而且這個(gè)selection中攜帶的模型還需要實(shí)現(xiàn) IPropertySource。或者是給SheetPage指定PropertySourceProvider……反正一個(gè)字:麻煩!
有了GEF提供的現(xiàn)成的東西就好了。“自從我用了GEF,腰不酸了,腿不疼了……”
要在GEF中顯示出屬性,只需要讓我們的模型實(shí)現(xiàn)IPropertySource即可,其他的事情您就不要操心了。
IPropertySource中有一些我們必須實(shí)現(xiàn)的方法:
getPropertyDescriptors
這個(gè)方法是返回我們需要顯示的propertydescriptor,也就是屬性視圖中每一條屬性對(duì)應(yīng)的控件。常用的有這幾種Descritor: TextPropertyDescriptor,ComboBoxPropertyDescriptor, ColorDialogPropertyDescriptor。
getPropertyValue
該方法返回給property視圖一個(gè)可編輯的值,這個(gè)值是我們通過(guò)方法傳入的ID來(lái)進(jìn)行識(shí)別并返回的。一會(huì)我們會(huì)有具體實(shí)現(xiàn)。
setPropertyValue
和上面的方法相反,這個(gè)方法是通過(guò)ID讓我們?cè)O(shè)置模型的值。
大家可以看到,我們將Table的tableName和Table在視圖上的坐標(biāo)作為屬性顯示在屬性視圖上,這兩個(gè)屬性對(duì)應(yīng)的 PropetyDescriptor分別是:TextPropertyDescriptor和PropertyDescriptor,而且還對(duì)應(yīng)了各自的 ID。
先看看tableName屬性。在getPropertyValue中,我們對(duì)id進(jìn)行判斷,如果和該屬性對(duì)應(yīng)的id相同,我們就返回Table模型的表名給它;同樣,在setPropertyValue中,如果是設(shè)置表名的話,我們就直接把Table的名字設(shè)為傳入的值。
我們?yōu)閠ableName設(shè)置的PropertyDescriptor是一個(gè)TextPropertyDescriptor,這是一個(gè)專門進(jìn)行文本編輯的屬性editor。下面簡(jiǎn)單說(shuō)說(shuō)它的工作原理。
其實(shí)每一個(gè)PropertyDescriptor里面注冊(cè)有自己的CellEditor,CellEditor就是編輯屬性值的控件。CellEditor 是需要我們給它傳入它能顯示或者說(shuō)能體現(xiàn)該屬性的值的,所以為什么我們?cè)趃etPropertyValue中進(jìn)行判斷id,然后返回值,這些都是給 CellEditor準(zhǔn)備的。getPropertyValue返回的值傳給了這些CellEditor,他們會(huì)對(duì)這些值進(jìn)行顯示,當(dāng)然, CellEditor顯示這些值并不是直接就以String的形式顯示出來(lái),它要通過(guò)自己的LabelProvier來(lái)獲得這些值的顯示方式。但是默認(rèn)的情況下,LabelProvider直接是讓這些值顯示toString方法獲得的字符串。如果我們需要特殊地對(duì)屬性值進(jìn)行顯示的話,就需要給這些 CellEditor重新注冊(cè)ILabelProvider。我在代碼中也是這樣做的,有興趣的朋友可以看看代碼是如何實(shí)現(xiàn)的。
這里有一個(gè)問(wèn)題需要注意,CellEditor獲得值是通過(guò)doSetValue方法來(lái)取得的,當(dāng)我們返回給維護(hù)CellEditor的某個(gè)類(具體哪個(gè)類我不太清楚)值的時(shí)候,它會(huì)調(diào)用CellEditor的doSetValue方法。不同的CellEditor它能接受的值類型也有差別,舉個(gè)例子: TextCellEditor能夠接受的值是String類型的,但是ComboBoxCellEditor只能接受Integer類型的值,同樣 ColorDialogCellEditor只能接受RGB類型。
所以我們?cè)趃etPropertyValue方法中,需要根據(jù)ID以及注冊(cè)的PropertyDescriptor的不同來(lái)返回不同類型的值。當(dāng)然,我們得到的值也和輸入值類型一至。
再看看Point屬性是怎么回事。在返回PropertyDescriptor時(shí),我們給Point屬性返回的是PropertyDescrptor,這是因?yàn)镻oint值是有x,y組成的,我稱這種屬性為“復(fù)合屬性”,讓Descriptor返回propertyDescriptor的目的是為了讓屬性頁(yè)顯示出的是一個(gè)類似樹(shù)型控件那樣可以打開(kāi)的控件,然后我們創(chuàng)建一個(gè)類,實(shí)現(xiàn)IPropertyDescriptor接口,這個(gè)過(guò)程就有點(diǎn)類似于為我們的模型生成屬性一樣,只要在getEditableValue方法中準(zhǔn)確地返回屬性需要的值即可。
類似的“復(fù)合屬性”有很多,最常見(jiàn)的有對(duì)于Point類型和Dimension類型的。我寫了這兩中屬性的實(shí)現(xiàn),大家可以下載參考一下:PointPropertySource?? DimensionPropertySource
在屬性中還有一個(gè)比較實(shí)用的就是驗(yàn)證器了。沒(méi)一個(gè)Descriptor都可以設(shè)置自己的驗(yàn)證器。比如,我們要輸入坐標(biāo),坐標(biāo)輸入只能是數(shù)字,那我們就需要利用驗(yàn)證器幫我們驗(yàn)證。當(dāng)驗(yàn)證器發(fā)現(xiàn)輸入有錯(cuò)誤的時(shí)候就會(huì)在編輯器下放的狀態(tài)欄中提醒我們.
驗(yàn)證器實(shí)現(xiàn)很簡(jiǎn)單:
?/P>
????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?"請(qǐng)輸入數(shù)字"?;
????????}
????}
}
驗(yàn)證器最好是作為一個(gè)單態(tài)類存在,同類屬性Descriptor可以共用一個(gè),以免浪費(fèi)內(nèi)存.
5.結(jié)束語(yǔ)
這章我們簡(jiǎn)單介紹了一下Property頁(yè)和Connection的實(shí)現(xiàn),以后的文章里我們會(huì)更詳細(xì)介紹一下一些特殊技巧的使用,比如制作FontDialogPropertyDescriptor,以及Connection中線的改變等。