图形和图形之间的连线是怎么实现的?模型的属性又是怎么显示在属性页上的?这一章会通过我们的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中线的改变等。