使用GEF创建数据库模型编辑器

来源:互联网 发布:小米2s能用4g网络吗 编辑:程序博客网 时间:2024/06/10 08:30

用过Eclipse相关产品的人经常会看到一些涉及到图形应用的插件,比如Struts、UML、DatabaseDesign相关的插件;或者企业中自己要定制自己的图形化的工作流编辑器,组织结构图等。这些都涉及到图形编辑,以前我们大多用Win32API或JFC/Java2D技术自己做框架来实现这些功能,复杂且工作量大!
GEF(Graphical EditorFramework)是现在Eclipse领域流行的图形编辑框架,它可以用来给用户提供图形化编辑模型的功能,从而提升用户体验。它同时具有Windows的美观界面和JFC的跨平台能力,并可以与draw2D(Eclispe领域想要取代java2D的图形API)达到良好的结合。
GEF里用到了很多经典的结构模式和设计模式,如MVC,Policy,Command,Observer等,方便的实现Undo/Redo功能等等,通过学习GEF,也可以更好地掌握这些模式.
在本文中,我们将介绍GEF/draw2D,并简介用GEF如何一步步的做一个数据库模型编辑器。


读者朋友们可能已经看到了前面文章所介绍的Eclipse体系结构了,这里还有一张图对GEF与Eclispe平台之间的关系作了描述:SWT和Draw2D的关系就相当于AWT与Java2D的关系,而GEF这个框架是基于ui.views和RCP(见本系列另一篇关于RCP的文章)概念之上的。
 

说到GEF框架,还是要从我们熟悉的MVC模式说起,MVC是一种通用的涉及到UI交互的模式,通常Model代表数据的显示,View则负责在界面上呈现数据,而Controller的职责是处理用户的输入以及根据Model的修改刷新View。一般情况下,一个UI(View)只被用于呈现一个Model,但是在GEF里面任何View可以被用于呈现任意的Model,也就是说在GEF里View-to-Model的映射并不是1-to-1的,在GEF中MVC的定义如下:
 

Model:任何GEF 应用的起点就是Model。它们需要被显示,编辑和持久化。Model应该对View和Controller的细节一无所知,它仅仅可以把自己的改变负责通知给View和Controller,在GEF中,Model只能被Commands所更新。
View:View包括Draw2D中一种虚拟组件Figure来呈现Model对象,GEF也支持SWT中的TreeItem对象。另外,与View相关的还包括feedback,handle,tooltip等等。所有这些View组件都由Controller来构建和管理它们。
Controller:就是EditPart,EditPart在GEF里面是最关键的组件,负责管理整个View层面的所有组件。后面会有详细 介绍
 

在我们的这个数据库模型编辑器中,我们定义了我们的MVC架构:

我们的Model:
我们的Model分成如下几种:
• Table: 代表一张关系数据库表。 唯一的属性是表名
• Column: 数据库表的列。 我们比较感兴趣的属性是列名和数据类型( 可能是VARCHAR 或整型)。
• Relationship: 表现为两张数据库表之间的主外键关系。
• Schema: 简单地可以代表所有表的分组。
我们的模型很简单, 但至少包括在一个典型的GEF 模型中两种关键的对象关系:
• 在table和schema之间以及table和column之间存在父子关系
• 在不同的节点(Node)之间的连接关系。 在我们的例子中, connection是以主外键       关系的形式

我们的View
在View这一部分的介绍,我们分别介绍Figure和布局管理器这两部分:

Figure
Figure在draw2d 中是一种创建复杂图形的轻量级对象。视图则是由一组反映model的Figures来组建的。 在典型的GEF 应用中, 您通常会创建一组定制的Figure类并实现IFigure 接口。

在我们的应用中有以下的Figure:

EditableLabel: draw2d中Label的子类。 用于为column和table命名。
ColumnsFigure: 所有column的一个容器。
TableFigure: 包含EditableLabel 和ColumnsFigure
SchemaFigure: 在Schema中包含所有TableFigures
另外,我们并未提供任何特别的figure来代表图中的连接线, 我们简单地使用draw2d 的PolylineConnection 类, 来代表一条有0或多个拐点(bend point) 的连接线。

由于表名,列名,列数在一个TableFigure 实例的生命周期内可以增加或减少的, 我们也可以重设我们的ColumnsFigure 和TableFigure的尺寸。这个功能由布局管理器(draw2d 的另一个重要部份)担当。

布局管理器
GEF 提供与Swing和SWT 的LayoutManager不同的一个布局管理框架: 它的职责是处理实现draw2d IFigure的Figure的布局。应用开发者将决定用某个LayoutManager来布局每一个包含子Figure的Figure。

宽泛地讲, 有三类型LayoutManager:
• 结构化布局管理器,比如FlowLayout 和ToolbarLayout, 可以根据子figure的次序来垂直或水平地布局
• 基于约束的布局管理器,譬如XYLayout 和DelegatingLayout 。为每个子figure设置一个Constraint对象。 在XYLayout 情况下, 这个对象是一个Rectangle对象来指定位置和尺寸。
• 使用几何算法的布局管理器, 这种布局为子figure运用一系列的相当复杂算法来计算布局。这种算法采取一种特殊的数据结构作为譬如Node的安置和path的路由之类几何问题的解决方案。GEF 由DirectedGraphLayout和CompoundDirectedGraphLayout提供算法。
 

GEF 开发者需要理解特定的LayoutManager被应用于特定的情况。在我们的应用中ColumnsFigure使用FlowLayout来布局它的Label组件。TableFigure 使用ToolbarLayout 来布局它的子组件(简单地垂直摆放)。大家可以看一下TableFigure的初始化过程:

java 代码
  1. public TableFigure(EditableLabel name, List colums)   
  2. {   
  3. nameLabel = name;   
  4. ToolbarLayout layout = new ToolbarLayout();   
  5. layout.setVertical(true);   
  6. layout.setStretchMinorAxis(true);   
  7. setLayoutManager(layout);   
  8. setBorder(new LineBorder(ColorConstants.black, 1));   
  9. setBackgroundColor(tableColor);   
  10. setForegroundColor(ColorConstants.black);   
  11. setOpaque(true);   
  12.   
  13. name.setForegroundColor(ColorConstants.black);   
  14. add(name);   
  15. add(columnsFigure);   
  16. }   
  17.   

我们的Controller:
当我们开始谈论控制器才算真正走入GEF领域。 GEF 提供了EditPart来避免Model知道关于View Part(Figure)的细节, 并且反之亦然。

EditParts:

一般情况下,我们需要为每个Model创建一个EditPart实例(通常我们都是通过继承AbstractEditPart来创建我们自己的EditPart),所以我们必须使我们的EditParts的继承结构和Model的继承结构相匹配。每个EditPart(也就是GraphicalEditPart), 会处理一个Model组件, 并且关联一个视图组件。 由于模型和视图完全地被分离,所有在模型和视图之间的协调必须由EditPart 处理。

在我们的模型编辑器的实现中,有如下的EditPart实现:
• SchemaDiagramPart: 表现为一个Schema实例及其相关的 SchemaFigure
• TablePart: 表现为一个Table并且管理这个Table相关的TableFigure及其子Figure
• ColumnPart: 能够让我们编辑 column label
• RelationshipPart: 表现为一个主外键关系. 一个 RelationshipPart关联两个 TableParts
我们可以用一个EditPartFactory来提供EditPart:
java 代码
  1. public class SchemaEditPartFactory implements EditPartFactory   
  2. {   
  3.     public EditPart createEditPart(EditPart context, Object model)   
  4.     {   
  5.         EditPart part = null;   
  6.         if (model instanceof Schema)   
  7.             part = new SchemaDiagramPart();   
  8.         else if (model instanceof Table)   
  9.             part = new TablePart();   
  10.         else if (model instanceof Relationship)   
  11.             part = new RelationshipPart();   
  12.         else if (model instanceof Column)   
  13.             part = new ColumnPart();   
  14.         part.setModel(model);   
  15.         return part;   
  16.     }   
  17. }   
SchemaDiagramPart, TablePart and ColumnPart都将继承GraphicalEditPart.另外,TablePart 是一个主外键关系中的一个node, 因此它必须实现NodeEditPart. 最后,RelationshipPart表现为关系中的一个connection part,所以它必须实现AbstractConnectionEditPart.
在GEF应用中,有以下一些任务EditPart的子类必须实现:
1.提供一个EditPart相关的Figure实例
java 代码
  1. protected IFigure createFigure()   
  2. {   
  3. Table table = getTable();   
  4. EditableLabel label = new EditableLabel(table.getName());   
  5. TableFigure tableFigure = new TableFigure(label);   
  6. return tableFigure;   
  7. }   
2.在继承结构中代表父对象的EditParts必须重写getModelChildren()
java 代码
  1. protected List getModelChildren()   
  2. {   
  3.     return getTable().getColumns();   
  4. }   

3.如果代表父EditPart的Figure并非子EditPart的Figure的直接父类,那么需要实现AbstractGraphicalEditPart.getContentPane(),返回子EditPart的Figure
在我们的例子中可以从一个TableFigure的实例中得到ColumnPart的ColumnFugure
java 代码
  1. public IFigure getContentPane()   
  2. {   
  3.     TableFigure figure = (TableFigure) getFigure();   
  4.     return figure.getColumnsFigure();   
  5. }   

Request:
Request是处理GEF 应用中的一个编辑动作的起点。GEF 能用面向对象的方式处理用户交互和转发这些requests。例如,当我们从“NewColumn”这个调色板(palette)按钮拖放一个column到图中的一个表中时,相当于用户与GEF应用的交互,GEF应用在后台将创建request对象。 比如创建column时, GEF 将产生CreateRequest对象。
不同的用户操作将会产生不同的Request类型,一般情况下我们主要有三种类型的request:CreateRequests,GroupRequests and LocationRequests.在GEF API和文档中有详细的描述。这些request对象将用户操作对模型改变的信息巧妙地封装起来。

EditPolicies and Roles
一个EditPolicy 是EditPart 的扩展, 事实上, 和某些编辑的相对任务是EditPart传递request给它的委托者(delegate)-EditPolicy,类似于我们在J2EE开发中经常设计一些特定的RequestProcessor(class或method)来处理前端Servlet传过来的特定的request,。Policy是一种非常好的面向对象设计技巧,可以这么说,EditPolicy承担了绝大部分EditPart的细节工作。另外,每个EditPolicy对应一个role,为了理解EditPolicy,我们看在TablePart中的createEditPolicies()
java 代码
  1. protected void createEditPolicies()   
  2. {   
  3.     installEditPolicy(EditPolicy.GRAPHICAL_NODE_ROLE, new TableNodeEditPolicy());   
  4.     installEditPolicy(EditPolicy.LAYOUT_ROLE, new TableLayoutEditPolicy());   
  5.     installEditPolicy(EditPolicy.CONTAINER_ROLE, new TableContainerEditPolicy());   
  6.     installEditPolicy(EditPolicy.COMPONENT_ROLE, new TableEditPolicy());   
  7.     installEditPolicy(EditPolicy.DIRECT_EDIT_ROLE, new TableDirectEditPolicy());   
  8. }  

这个方法的作用是简单地装饰TablePart的编辑功能。每一次调用installPolicy都是为EditPart注册一个EditPolicy。例如,EditPolicy.CONTAINER_ROLE是与TablePart相关的,之所以role叫Container是因为我们知道Table包含column,并且我们的应用程序要创建新的column以及添加这些column到已存在的table。

很多抽象的EditPolicy类提供了一个getCommand(Request request)方法的实现。在ContainerEditPolicy类中我们能够发现如下代码:
java 代码
  1. public Command getCommand(Request request) {   
  2.   if (REQ_CREATE.equals(request.getType()))   
  3. return getCreateCommand((CreateRequest)request);   
  4.   if (REQ_ADD.equals(request.getType()))   
  5. return getAddCommand((GroupRequest)request);   
  6.   if (REQ_CLONE.equals(request.getType()))   
  7. return getCloneCommand((ChangeBoundsRequest)request);   
  8.   if (REQ_ORPHAN_CHILDREN.equals(request.getType()))   
  9. return getOrphanChildrenCommand((GroupRequest)request);   
  10.   return null;   
  11. }   
这里getCommand()使用request类型来决定哪个getXXXCommand() 方法将被调用。 但是在ContainerEditPolicy中, 这些方法是抽象的- 我们必须在具体的EditPolicy类中提供具体的fa方法实现:
java 代码
  1. public class TableContainerEditPolicy extends ContainerEditPolicy   
  2. {   
  3.     protected Command getCreateCommand(CreateRequest request)   
  4.     {   
  5.      //从request得到新对象并确认它是Column的实例   
  6. Object newObject = request.getNewObject();//从request得到新对象并确认它是   
  7.         if (!(newObject instanceof Column))   
  8.         {   
  9.             return null;   
  10.         }   
  11.         Column column = (Column) newObject;   
  12. //得到与TablePart相关的Table对象   
  13.      TablePart tablePart = (TablePart) getHost();   
  14.         Table table = tablePart.getTable();   
  15. //创建相关的Command并设置Table和Column信息   
  16. ColumnCreateCommand command = new ColumnCreateCommand();   
  17.         command.setTable(table);   
  18.         command.setColumn(column);   
  19.         return command;   
  20.     }   
  21. }   
多数情况下,我们的EditPolicy的实现就是简单地使用Request对象生成一个command.
我们为不同的role有不同的EditPolicy实现。例如在我们的应用程序中TableEditPolicy继承ComponentEditPolicy来实现EditPolicy.COMPONENT_ROLE.并通过实现createDeleteCommand(GroupRequest request)来处理REQ_DELETE.

Commands
相信了解设计模式的人都会对Command这个模式耳熟能详,每一个具体的Command负责对Model的修改,这些Command都继承GEF中的一个抽象基类:org.eclipse.gef.commands.Command,它简单地封装了我们对request的响应,其主要方法有excute(),undo(),redo(),canExcute(),canUndo(),canRedo().
大多数情况下,Command的子类需要实现excute()和undo(),而其他方法是可选的,我们可以看看ColumnCreateCommand的实现:
java 代码
  1. public class ColumnCreateCommand extends Command   
  2. {   
  3.     private Column column;   
  4.     private Table table;   
  5.     public void setColumn(Column column)   
  6.     {   
  7. //为column设置名字和类型   
  8.         this.column = column;   
  9.         this.column.setName("COLUMN " + (table.getColumns().size() + 1));   
  10.         this.column.setType(Column.VARCHAR);   
  11.     }   
  12.   
  13.     public void setTable(Table table)   
  14.     {   
  15.         this.table = table;   
  16.     }   
  17.   
  18.     public void execute()   
  19.     {   
  20. //将新column添加到table   
  21.      table.addColumn(column);   
  22.     }   
  23.   
  24.     public void undo()   
  25.     {   
  26.      table.removeColumn(column);   
  27.     }   
  28. }   

使用Commands比起直接使用EditPolicies来改变model有两个好处:
• Command是很好的OO设计模式 ;
• Command框架支持undo和redo功能;
Command的实现不能够容纳任何对GEF特有的组件如EditParts或EditPolicies的引用。要注意保持Commands和UI逻辑干净的分离

 

传播机制
一旦我们改变model,我们的GEF编辑器需要将这些改变传播到UI。我们需要协调模型,视图和控制器来完成这个工作!
迄今为止,我们讨论了的GEF中Model,View和Controller的功能,但是为了做一个模型编辑器,我们需要让我们的EditPart做更多的事情:
• 需要一个listener来更新model。而model也需要传播EditPart能够接收到的事件响应
• 需要维持与其子EditPart以及Connection之间的关系,并同步其与model的改变。
• 需要更新它管理的Figures以及布局,符合model的改变。
最后要做的事情
最后我们要为这个模型编辑器定义自己的扩展点:
xml 代码
  1. <extension  
  2.          point="org.eclipse.ui.editors">  
  3.       <editor  
  4.             name="%editor.name"  
  5.             icon="icons/editor.gif"  
  6.             extensions="schema"  
  7.             class="com.realpersist.gef.editor.SchemaDiagramEditor"  
  8.             contributorClass="com.realpersist.gef.action.SchemaActionBarContributor"  
  9.             id="Schema Editor">  
  10.       editor>  
  11. extension>  

同样要为它定义一个Wizard来帮助我们创建数据库Schema图
  
xml 代码
  1. <extension  
  2.          point="org.eclipse.ui.newWizards">  
  3.       <category  
  4.             name="GEF (Graphical Editing Framework)"  
  5.             parentCategory="org.eclipse.ui.Examples"  
  6.             id="org.eclipse.gef.examples">  
  7.       category>  
  8.       <wizard  
  9.             availableAsShortcut="true"  
  10.             name="Schema Diagram Editor"  
  11.             icon="icons/editor.gif"  
  12.             category="org.eclipse.ui.Examples/org.eclipse.gef.examples"  
  13.             class="com.realpersist.gef.editor.wizard.SchemaDiagramWizard"  
  14.             id="com.realpersist.gef.editor.wizard.wizard.new.file">  
  15.          <description>  
  16.            Wizard to create an empty or pre-populated schema diagram file   
  17.          description>  
  18.          <selection  
  19.                class="org.eclipse.core.resources.IResource">  
  20.          selection>  
  21.       wizard>  
  22.    extension>  

除了这些内容,我们还有好多东西没能在这里一一呈现,比如Palette, PropertySheet,Outline