隔了將近一個月,我終于可以在家上網了——我又回來了。由于搬家后沒有網上,本來這篇應該早就寫完的文章也只能拖了這么久才“發布”。
在這一章,我說一下如何去利用布局,以及其他一些關于EditPolicy的用法和Palette的實現。說明一下,示例代碼和本章內容中的代碼有出入,這是因為以前我是做一點寫一點,這次我完成得比較多,但是由于涉及技術有一些差異,所以我將分成兩篇文章講完,我建議大家看完第5章后再下代碼。代碼下載.
1.重新顯示TableFigure
上一章中,我們簡單地繪制了一個矩形,充當我們的數據庫表的圖形,現在讓我們重新想想,如何顯示這個TableFigure。
讓我們看看Visio里面是怎么顯示的吧:
我們也按照它的樣子做一個類似的!
首先,我們需要顯示我們的表名,其次,我們要將數據字段顯示在表名下放的區域中,然后再在表名和存放數據庫字段的區域之間畫一條線,分割開??聪聢D:
看來我們以前寫的TableFigure還差太遠,僅僅一個矩形還不夠。
先考慮一下如何顯示表名。在TableFigure中顯示一條文字很容易的,簡單的方法就是在繪制它的時候,利用Graphics的drawText方法即可,但是這樣做很繁瑣,需要不停的計算表名顯示位置,并且,當我們的表名發生改變的時候,表自身大小該如何調節呢。所以我們需要利用Label來顯示,因為Label可以很容易地對顯示的表名進行維護,包括計算文字長度獲得Figure該具有的大小等;此外,當我們使用Label來顯示的話, TableFigure還可以通過布局管理器來計算自己當前的大小,這又省得我們自己計算了。打開TableFigure類,增加以下代碼:
public
?TableFigure(Table?model)?{
????????super();
????????
this
.model?
=
?model;
????????tableNameLabel?
=
?
new
?Label();
????????tableNameLabel.setText(model.getTableName());
????????FontData?fd?
=
?
new
?FontData();
????????fd.setHeight(
10
);
????????fd.setName(
"
Arial
"
);
????????fd.setStyle(SWT.BOLD);
????????tableNameLabel.setFont(
new
?Font(
null
,?fd));
????????tableNameLabel.setIcon(ImageProvider.TABLE_ICON.createImage());
????????tableNameLabel.setLabelAlignment(PositionConstants.MIDDLE);
????????
//
?留出一點邊距,會好看點
????????
this
.setBorder(
new
?MarginBorder(
8
,?
8
,?
8
,?
8
));
????????
this
.add(tableNameLabel);
????????
????????
this
.setOpaque(
true
);
????}
我們在TableFigure創建的時候,給它生成了一個Label,然后將這個Label作為TableFigure的子Figure添加到它上面,這樣一來我們就讓這個Label來顯示數據表名。上面代碼中用到了圖片設置以及字體設置,這里我們只討論GEF,所以就不討論他們了。
名字能顯示了,現在該讓我們的字段容器出場了!
字段的圖形需要有容器去維護顯示它,剛才我們也看到了,Visio中的字段都是在字段容器中自上而下有序地排列在一起的,問題就來了:怎么去讓它們如此聽話地有序排列呢。布局管理器(LayoutManager),只要做過桌面應用的人都知道,不錯,Draw2D也是利用布局管理器來對圖形進行位置大小進行維護的。在Draw2D中有一個名為ToolbarLayout的布局管理器,它實現了讓Figure自上而下有序地排列在一起,這正是我們需要的!
現在讓我們在TableFigure構造函數中增加以下代碼:
??????containerFigure?
=
?
new
?Figure()?;
????????ToolbarLayout?tableLayout?
=
?
new
?ToolbarLayout();
??
????????
//
?承載Column?Figure的容器是ToolbarLayout
????????ToolbarLayout?containerLayout?
=
?
new
?ToolbarLayout();
????????containerLayout.setMinorAlignment(ToolbarLayout.ALIGN_BOTTOMRIGHT);
????????containerFigure.setLayoutManager(containerLayout);
?????
????????
this
.add(containerFigure);
??這樣我們的containerFigure就做好了。
但是光這樣還不夠。containerFigure 和tableNamelable這兩個Figure作為TableFigure的子圖形,應該顯示在哪兒呢?怎么給他們定位?而且,當我們的這兩個子圖形大小發生改變了以后,TableFigure怎么辦?
其實所有的一切問題都是由布局管理器來回答的。
在Figure 類中有一個名位getPreferredSize的方法,這個方法是去獲得當前Figure大小的。默認的情況下,getPreferredSize方法會查看該Figure中是否注冊有布局管理器,如果有,那么就會調用布局管理器的getPreferredSize方法獲得大小。布局管理器的 getPreferredSize方法并不是簡單地去查看當前Figure的Size,而是要通過該布局本身的特點,再通過該Figure中的子圖形位置以及大小,按照一定的算法疊加而取得當前Figure的大小。
簡單說,如果我們使用了ToolbarLayout,那么當我們調用注冊有該 LayoutManager的Figure的getPreferredSize方法時,布局管理器就會將該Figure的高度設置為它所有子Figure 的高度和;寬度設為所有子Figure中寬度最大值。
這樣一來,根據我們剛才圖形所畫的那樣,tableNameFigure和 containerFigure也是自上而下排列的,所有,我們可以在TableFigure中使用ToolbarLayout,讓布局管理器來管理它的大小。寫到這里大家也都看出來了,我們在containerFigure中設置ToolbarLayout的,也是為了當我們增加字段圖形的時候,讓布局管理器去控制它的大小。
我們已經基本完成了TableFigure。但是如果在我們添加了數據庫字段的時候,EditPart并不知道讓字段的Figure是作為TableFigure中containerFigure的子圖形而添加進去的,它默認是把字段圖形直接繪制到 TableFigure上,那這樣我們剛才的設想就完全實現不了了。所以,讓我們復寫TableEditPart的getContentPane方法:
?
public
?IFigure?getContentPane()?{
????????
return
?((TableFigure)getFigure()).getContainerFigure();
????}
這是什么意思呢?
在前面的章節我好像說過了,GEF中對于某一個EditPart,它的子EditPart的圖形是繪制在該EditPart的圖形之上的——一個遞歸的過程。但是EditPart并不是把它的圖形直接默認為子Fiuger的容器Figure,而是通過getContentPane方法來獲得承載子 EditPart圖形的Figure,當然,如果直接繼承GraphicalEditPart的話,getContentPane方法直接返回的就是 getFigure,所以,當我們要重新定義EditPart的容器Figure的時候,就需要復寫getContentPane。
2.顯示出ColumnFigure
現在我們來實現字段圖形。
字段圖形很簡單,只要實現字段名以及一個能表示字段的圖標即可,所以我們將它繼承自Label:
public
?
class
?ColumnFigure?extends?Label?{
????
private
?Column?model;
????
private
?boolean?selected;
????
private
?boolean?hasFocus;
??????
public
?ColumnFigure(Column?model){
??????????super();
??????????
this
.model?
=
?model;
??????????model.setColumnName(model.getColumnName());
??????????
??????????FontData?fd?
=
?
new
?FontData();
??????????
????????????fd.setHeight(
8
);
????????????fd.setName(
"
Arial
"
);
????????????fd.setStyle(SWT.BOLD);
??????????
this
.setIcon(ImageProvider.COLUMN_ICON.createImage());
??????????
this
.setLabelAlignment(PositionConstants.LEFT);
??????????
this
.setFont(
new
?Font(
null
,fd));
??????}
}
然后讓ColumnEditPart的createFigure方法返回它:
????protected?IFigure?createFigure()?{
????????//?TODO?Auto-generated?method?stub
????????return?new?ColumnFigure((Column)getModel());
????}
ok,現在讓我們在DbEditor中修改一下initializeGraphicalViewer方法:
protected?void?initializeGraphicalViewer()?{
????????//?硬編碼生成一個數據庫模型
????????Schema?schema?=?new?Schema();
????????Table?table?=?new?Table();
????????table.setTableName("Test");
????????Column?column?=?new?Column();
????????column.setColumnName("test");
????????table.addChild(column);
????????schema.addChild(table);
????????this.getGraphicalViewer().setContents(schema);
????}
OK,現在我們可以看到一個新的TableFigure了。
但是存在一個問題:我們的TableFigure大小自己不能計算獲得,連字段都沒顯示出來??隙ㄓ腥艘f了:不是說有了布局管理器就可以了嗎!
等等,還記得
上一章中,我們為了能移動矩形,而復寫了refreshVisuals方法吧?現在我們重新寫一遍:
?protected?void?refreshVisuals()?{
????????super.refreshVisuals();
????????//?得到當前TableFigure的大小,由于有Toolbar布局的約束,它會自動計算
???????
????????Dimension?size?=?this.getFigure().getPreferredSize();
????????
????????//?獲得更改后的位置,位置是在Model進行維護的
????????Point?p?=?((Table)?getModel()).getLocation();
????????
????????//?我們只更改Table的位置
????????((GraphicalEditPart)?this.getParent()).setLayoutConstraint(this,?this
????????????????.getFigure(),new?Rectangle(p,?size));
????}
看過以前代碼的朋友一眼就發現了不同:size不再是簡單的去取得當前的bounds大小了,而是通過我們上面說的,利用getPreferredSize方法去讓布局計算!
修改完畢后再看看我們的TableFigure:

補充一下:篇幅問題,我只撿我認為重要的說,其他的一些細節,比如,TableFigure尺寸最大和最小約束啊,怎么畫tableNameLabel下的線啊,還有Label的??堪。琤order的使用啊,漸變矩形的繪制啊,這些我就沒有提起了,大家可以自己看代碼,如果有疑問可以發貼討論。
3.重新生成PaletteRoot
在我們以前的例子中,工具面板生成的是一個很簡單的空面板,上面光禿禿的,無法通過面板的工具往我們的Viewer中增加Figure圖形,使得我們每次都需要在DbEditor中復寫修改代碼,用硬編碼來實現模型的增加。
現在起我們要構造一個可以創建Table和Column的工具面板,讓硬編碼創建模型見鬼去吧。
先生成一個單態類:PaletteFactory,然后在我們在里面生成一個空的PaletteRoot,再弄兩個PaletteDrawer添加到PaletteRoot上:
public?class?PaletteFactory?{
????
????private?PaletteRoot?root;
????
????private?PaletteDrawer?defaultTools;
????
????private?PaletteDrawer?dbTools;
????
????private?static?PaletteFactory?instance?=?null;
????private?PaletteFactory(){}
????
????public?static?PaletteFactory?INSTANCE(){
????????if(instance?==?null)?instance?=?new?PaletteFactory();
????????return?instance;
????}
????
????public?PaletteRoot?createPaletteRoot(){
//????????if(root?!=?null)?return?root;
????????
????????root?=?new?PaletteRoot();
????????root.add(createDefaultToolBox());
????????root.add(createDbToolBox());
????????return?root;
????}
????
????private?PaletteDrawer?createDefaultToolBox(){
????????defaultTools?=?new?PaletteDrawer("Default?tools");
????????
????????defaultTools.add(new?SelectionToolEntry());
????????
????????return?defaultTools;
????}
????
????private?PaletteDrawer?createDbToolBox(){
????????dbTools?=?new?PaletteDrawer("DataBase?tools");????????
????????return?dbTools;
????}
}
我簡單說一下,PaletteDrawer是一種可以隱藏的容器,在它上面可以增加按鈕等ToolEntry。什么是ToolEntry呢?大家可以理解為在Palette上面的任何元素:按鈕、分割線、容器等,ToolEntry對應有一個Tool,通過ToolEntry的createTool返回的,如果我們需要一些特別處理的時候,可以直接去實現ToolEntry和Tool,但是在我們的例子中,我們需要的是可以生成模型的ToolEntry,所以我們就不必去研究ToolEntry和Tool的工作原理。
接著往下說。
上述代碼中,我們給defaultTools容器增加了一個SelectionToolEntry,它是GEF自己提供的一個工具,是為點擊選擇圖形使用的,對于它的作用這里就不廢話了:)
從代碼下面可以看出來,我們還沒有增加創建Table和Column的ToolEntry,現在我們來實現他們。
創建兩個ToolEntry,分別命名為TableToolEntry和ColumnToolEntry,他們都是繼承了 CreationToolEntry。注意一下CreationToolEntry的構造函數,需要傳入一個CreationFactory,這個工廠類就是創建我們所要返回的模型實例工廠,它的兩個方法:getNewObject()和getObjectType()分別是返回創建的模型和模型的類型。
來看看我們的對該工廠的實現:
public?class?DbCreationFactory?implements?CreationFactory?{
????private?Class?type;
????public?DbCreationFactory(Class?type){
????????setType(type);
????}
????public?Object?getNewObject()?{
????????if(type?==?Table.class){
????????????return?new?Table();
????????}
????????if(type?==?Column.class)?return?new?Column();
????????return?null;
????}
????public?Object?getObjectType()?{
????????//?TODO?Auto-generated?method?stub
????????return?getType();
????}
??? .......
}
通過創建這個工 廠類的時候傳入的參數,就可以生成對應的模型了
再看看我們的TableToolEntry和 ColumnToolEntry的實現的:
public?class?TableToolEntry?extends?CreationToolEntry?{
????public?TableToolEntry()?{
????????super("Table",?"Create?a?table",?new?DbCreationFactory(Table.class),?ImageProvider.TABLE_ICON,?null);
????}
}
public?class?ColumnToolEntry?extends?CreationToolEntry?{
????public?ColumnToolEntry(){
????????super("Column",?"Create?a?column",?new?DbCreationFactory(Column.class),?ImageProvider.COLUMN_ICON,?null);
????}
}< /span>
大家看出來了,我們傳入的DbCreationFactory 都是對應了他們所要生成模型需要的參數。
現在,我們的ToolEntry類創建好了,讓我們把它們添加到 dbTools的PaletteDrawer上:
修改?PaletteFactory 類的
createDbToolBox()方法
????private?PaletteDrawer?createDbToolBox(){
????????dbTools?=?new?PaletteDrawer("DataBase?tools");
????????
????????dbTools.add(new?TableToolEntry());
?????????dbTools.add(new ColumnToolEntry());
?????
????????return?dbTools;
????}
運行一下:

4.新的Command; FlowEditPolicy的用法
完成了上面的工作后,我們離從工具面板上創建模型還差點:EditPolicy中沒有對應的Command。
以前的章節里,我們都知道了,GEF中的所有事件都封裝成了Request向外發送,然后找到EditPolicy處理,EditPolicy再去索取Command來執行。我們在上一章就已經生成了一個TableMoveCommand,所以相信大家對Command應該不陌生了。
我們生成這樣一個Command:DbItemCreationCommand
public?class?DbItemCreateCommand?extends?Command?{
????private?DBBase?parent;
????private?DBBase?child;
????private?int?index?=?-1;
????
????public?void?execute()?{
????????Assert.isNotNull(parent);
????????Assert.isNotNull(child);
????????parent.addChild(index,child);
????}
????public?void?redo()?{
????????execute();
????}
????public?void?undo()?{
????????Assert.isNotNull(parent);
????????Assert.isNotNull(child);
????????parent.removeChild(child);
????}
......
??......
}
現在,有了這個Command,我們就要考慮一下,看講它交給誰的EditPolicy去返回。
通常情況下,如果我們要生成一個模型,那么我們就應該在它的父容器的EditPolicy注冊一個 Command,因為絕大多數的容器類型的EditPart,都有安裝有ContainerEditPolicy或者LayoutEditPolicy,而這兩種EditPolicy恰恰就能對CreateRequest進行截獲并進行處理。所以,結合我們的例子,要生成Table模型,就需要在他的父 EditPart——SchemaEditPart的SchemaLayoutEditPolicy里做文章。
打開這個類,發現里面有一個方法:getCreateCommand,我們就在這里面返回DbItemCreateCommand吧:
protected?Command?getCreateCommand(CreateRequest?request)?{
????????Object?obj?=?request.getNewObject();
????????if(obj?!=?null?&&?request.getNewObjectType()?==?Table.class){
????????????DbItemCreateCommand?command?=?new?DbItemCreateCommand();
????????????command.setParent((DBBase)this.getHost().getModel());
????????????command.setChild((DBBase)obj);
????????????((Table)obj).setLocation(request.getLocation());
????????????return?command;
????????}
????????return?null;
????}
看看上面的代碼,發現了嗎?CreateRequest攜帶有我們要生成的對象的類型以及實例,并且連我們在創建時點擊在Viewer上的位置也有,所以,我們只需要設置一下DbItemCreateCommand中的父模型以及子模型即可,當然,我們還需要在生成模型的時候,將生成該模型時鼠標所點擊的位置給Table模型,好讓他一創建就處在該位置。太cool了!
但是光是添加了模型,EditPart是不知道的,所以我需要去通知EditPart刷新一下。還記得
上一章中,矩形位置更改后是怎么通知EditPart的嗎?我再羅嗦一下吧:利用我們在模型中的PropertyChangeSupport發出屬性更改通知,然后然EditPart截獲后去做相應的動作即可:
更改DBBase部分代碼代碼:
public?void?addChild(int?index?,?DBBase?child){
????????if(index?==?-1){
????????getChildren().add(child);
????????}else{
????????????getChildren().add(index,child);
????????}
????????child.setParent(this);
????????this.fireChildenChange(child);
????}
????
????public?void?removeChild(DBBase?child){
????????child.setParent(null);
????????getChildren().remove(child);
????????this.fireChildenChange(child);
????}
????public?void?fireChildenChange(DBBase?child){
????????support.firePropertyChange(PRO_CHILD,null,child);
????}
讓DBEditPartBase去截獲PRO_CHILD事件:
?public?void?propertyChange(PropertyChangeEvent?evt)?{
???????String?pName?=?evt.getPropertyName();
???????if(pName.equals(DBBase.PRO_CHILD)){
???????????this.refreshChildren();
???????????this.refreshVisuals();
???????}
????}
最后讓我們把以前在DbEditor中生成模型的代碼刪除掉,就讓Viewer的Content設置為一個Schema即可,
好了,運行一下吧,是不是可以創建Table了?:)
5.FlowLayoutEditPolicy的應用以及Column的創建我們已經能夠創建Table了,現在需要創建一個Column。
在上面我們已經創造了生成Column模型的條件:ColumnToolEntry、DbCreateFactory還有DbItemCreateCommand,就差一樣:我們把這個Command往哪兒放呢?
以前的經驗告訴我們,這個Command是需要讓ColumnEditPart的父EditPart的EditPolicy去返回的,但是它的父 EditPart,也是就TableEditPart,目前沒有安裝能夠維護創建子模型的EditPolicy,所以我們需要創建一個給他安裝上。
我們選用一個名為FlowLayoutEditPolicy的類作為我們新建EditPolicy的父類,這是因為 FlowLayoutEditPolicy專門針對具有FlowLayout以及ToolbarLayout布局管理器的Figure而做的,它可以對子 Figure的位置移動做出一些維護,比如當我們在TableFigure中拖動了ColumnFigure,FlowLayoutEditPolicy 可以在它可以插入的位置顯示一條黑線:

我們的這個類就命名為TableFlowLayoutEditPolicy:
public?class?TableFlowLayoutEditPolicy?extends?FlowLayoutEditPolicy?{
.......
?
????protected?Command?getCreateCommand(CreateRequest?request)?{
????????Object?obj?=?request.getNewObject();
????????if(obj?!=?null?&&?request.getNewObjectType()?==?Column.class){
????????????DbItemCreateCommand?command?=?new?DbItemCreateCommand();
????????????command.setParent((DBBase)this.getHost().getModel());
????????????command.setChild((DBBase)obj);
????????????
????????????EditPart?after?=?getInsertionReference(request);
????????????int?index?=?getHost().getChildren().indexOf(after);
????????????command.setIndex(index);
????????????return?command;
????????}
????????return?null;
????}
? .......
?............
????protected?boolean?isHorizontal()?{
????????IFigure?figure?=?((GraphicalEditPart)getHost()).getContentPane();
????????LayoutManager?layout?=?figure.getLayoutManager();
????????if(layout?instanceof?FlowLayout)
????????return?((FlowLayout)figure.getLayoutManager()).isHorizontal();
????????if(layout?instanceof?ToolbarLayout)
????????????return?((ToolbarLayout)figure.getLayoutManager()).isHorizontal();
????????return?false;
????}
}
我們在getCreateCommand中返回了DbItemCreateCommand,將parent設為Table,child即為生成request攜帶的Column對象,因為Column是沒有位置這個概念的,我們就不必把位置參數給它(給了也沒變量保存?。┧豢赡苁窃谶@個表中的第幾個而已,并且,我們在上面也提到了,在移動ColumnFigure的時候 FlowLayoutEditPolicy可以繪制一條黑線,顯示當前插入的位置,所以我們通過然后我們getInsertionReference方法得到當前黑線所在索引所對應的EditPart,然后得到該EditPart在TableEditPart的子EditPart中的位置,再傳遞給 Command,讓我們新生成的EditPart添加到該位置上。
但是,要讓FlowLayoutEditPolicy顯示出黑線來,我們還需要復寫它的?isHorizontal方法,因為默認情況下,FlowLayoutEidtPolicy的? isHorizontal方法在運行時,認為安裝該EditPolicy的EditPart使用的是FlowLayout,但是我們這里用的是 ToolbarLayout,所以如果不復寫的話,將會拋出類型轉換的異常。
我們只要簡單的進行一下修改即可,或者直接讓他返回true得了 :)
由于我們已經在Column以及ColumnEditPart的基類中增加了添加子節點后的屬性更改代碼,所以這里就不用寫了。
現在我們就可以通過點擊工具欄的column工具在Table中創建Column了。
讓我們看看整體效果:

6 . 結束語
這次我們基本上解決了數據庫表以及數據庫字段的實現問題,下一章我們會接著往下講,其實代碼里面已經有了,有興趣的朋友可以看看。
下一章我會講一下如何去在GEF應用中實現PropertyPage,以及Connection的一些問題