隔了将近一个月,我终于可以在家上网了——我又回来了。由于搬家后没有网上,本来这篇应该早就写完的文章也只能拖了这么久才“发布”。
在这一章,我说一下如何去利用布局,以及其他一些关于EditPolicy的用法和Palette的实现。说明一下,示例代码和本章内容中的代码有出入,这是因为以前我是做一点写一点,这次我完成得比较多,但是由于涉及技术有一些差异,所以我将分成两篇文章讲完,我建议大家看完第5章后再下代码。代码下载.
1.重新显示TableFigure
上一章中,我们简单地绘制了一个矩形,充当我们的数据库表的图形,现在让我们重新想想,如何显示这个TableFigure。
让我们看看Visio里面是怎么显示的吧:
我们也按照它的样子做一个类似的!
首先,我们需要显示我们的表名,其次,我们要将数据字段显示在表名下放的区域中,然后再在表名和存放数据库字段的区域之间画一条线,分割开。看下图:
看来我们以前写的TableFigure还差太远,仅仅一个矩形还不够。
先考虑一下如何显示表名。在TableFigure中显示一条文字很容易的,简单的方法就是在绘制它的时候,利用Graphics的drawText方法即可,但是这样做很繁琐,需要不停的计算表名显示位置,并且,当我们的表名发生改变的时候,表自身大小该如何调节呢。所以我们需要利用Label来显示,因为Label可以很容易地对显示的表名进行维护,包括计算文字长度获得Figure该具有的大小等;此外,当我们使用Label来显示的话, TableFigure还可以通过布局管理器来计算自己当前的大小,这又省得我们自己计算了。打开TableFigure类,增加以下代码:
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构造函数中增加以下代码:
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方法:
return ((TableFigure)getFigure()).getContainerFigure();
}
这是什么意思呢?
在前面的章节我好像说过了,GEF中对于某一个EditPart,它的子EditPart的图形是绘制在该EditPart的图形之上的——一个递归的过程。但是EditPart并不是把它的图形直接默认为子Fiuger的容器Figure,而是通过getContentPane方法来获得承载子 EditPart图形的Figure,当然,如果直接继承GraphicalEditPart的话,getContentPane方法直接返回的就是 getFigure,所以,当我们要重新定义EditPart的容器Figure的时候,就需要复写getContentPane。
2.显示出ColumnFigure
现在我们来实现字段图形。
字段图形很简单,只要实现字段名以及一个能表示字段的图标即可,所以我们将它继承自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方法返回它:
// TODO Auto-generated method stub
return new ColumnFigure((Column)getModel());
}
ok,现在让我们在DbEditor中修改一下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大小自己不能计算获得,连字段都没显示出来。肯定有人要说了:不是说有了布局管理器就可以了吗!
等等,还记得 上一章中,我们为了能移动矩形,而复写了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的停靠啊,border的使用啊,渐变矩形的绘制啊,这些我就没有提起了,大家可以自己看代码,如果有疑问可以发贴讨论。
3.重新生成PaletteRoot
在我们以前的例子中,工具面板生成的是一个很简单的空面板,上面光秃秃的,无法通过面板的工具往我们的Viewer中增加Figure图形,使得我们每次都需要在DbEditor中复写修改代码,用硬编码来实现模型的增加。
现在起我们要构造一个可以创建Table和Column的工具面板,让硬编码创建模型见鬼去吧。
先生成一个单态类:PaletteFactory,然后在我们在里面生成一个空的PaletteRoot,再弄两个PaletteDrawer添加到PaletteRoot上:
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()分别是返回创建的模型和模型的类型。
来看看我们的对该工厂的实现:
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 TableToolEntry() {
super( " Table " , " Create a table " , new DbCreationFactory(Table. class ), ImageProvider.TABLE_ICON, null );
}
}
public ColumnToolEntry(){
super( " Column " , " Create a column " , new DbCreationFactory(Column. class ), ImageProvider.COLUMN_ICON, null );
}
}< /span>
大家看出来了,我们传入的DbCreationFactory 都是对应了他们所要生成模型需要的参数。
现在,我们的ToolEntry类创建好了,让我们把它们添加到 dbTools的PaletteDrawer上:
修改 PaletteFactory 类的 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
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吧:
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部分代码代码:
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事件:
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:
.......
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的一些问题