我自从进入公司后,一直从事有关gef方面的开发工作,在这期间,走过不少弯路,仅仅是把GEF框架弄明白,就费了很大力气,所以,现在想写一点东西出来,供初学者阅读。
GEF(Graphical Editing Framework)是图形化编辑器开发的工具,比较典型的应用就是IBM 的Rose,它是一个模型驱动的MVC框架,控制器(EditPart)作为模型的侦听器,侦听模型的变化,如果模型的属性发生变化,它会通知控制器,控制器就会刷新模型对应的视图(Figure)。可以看出模型和视图之间没有直接联系,它们通过控制器而间接联系,可见控制器在gef框架中有着很重要的重要。
下面我们将从最基本的开始,介绍如何用GEF框架开发出一个流程设计器(开发工具Eclipse3.2.1包含插件包gef3.2.1和draw2d3.2.1)。
我们首先从模型开始,流程设计器顶层模型是流程(WorkflowProcess),流程有活动和链接这些活动的转移组成,其中活动又可以分为开始活动,普通活动,结束活动。理解了模型之间的组成关系,我们就可以设计模型对应的类了。由于上面提到,模型的属性变化了,必须通知控制器,由它来刷新模型对应的视图,所以控制器必须注册为模型的侦听器。由于每个模型都有相应的控制器侦听器侦听它属性的变化,我们把这方面的功能都放在父类中,定义一个
ModelElement
父类,具体代码如下:
package
com.example.workflow.model;
import
java.beans.PropertyChangeListener;
import
java.beans.PropertyChangeSupport;
import
java.io.IOException;
import
java.io.ObjectInputStream;
import
java.
io
.Serializable;
public
class
ModelElement
implements
Serializable{
private
static
final
long
serialVersionUID
= -5117340723140888394L;
private
transient
PropertyChangeSupport
pcsDelegate
=
new
PropertyChangeSupport(
this
);
public
synchronized
void
addPropertyChangeListener(PropertyChangeListener l) {
if
(l ==
null
) {
throw
new
IllegalArgumentException();
}
pcsDelegate
.addPropertyChangeListener(l);
}
protected
void
firePropertyChange(String property, Object oldValue, Object newValue) {
if
(
pcsDelegate
.hasListeners(property)) {
pcsDelegate
.firePropertyChange(property, oldValue, newValue);
}
}
private
void
readObject(ObjectInputStream in)
throws
IOException, ClassNotFoundException {
in.defaultReadObject();
pcsDelegate
=
new
PropertyChangeSupport(
this
);
}
public
synchronized
void
removePropertyChangeListener(PropertyChangeListener l) {
if
(l !=
null
) {
pcsDelegate
.removePropertyChangeListener(l);
}
}
}
接下来我们定义流程,活动,转移模型,让这些模型都继承这个父类ModelElement
,我们注意到活动由开始活动,普通活动,结束活动组成,这三类活动由很多相同的属性,例如活动的位置,名称,大小等等,所以给这三类活动进行抽象,定义一个父类
AbstractActivity
,把这些公共属性都放在这个父类中,父类的代码如下:
package com.example.workflow.model;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
/**
* Abstract prototype of a Activity.
* Has a size (width and height), a location (x and y position) and a list of incoming
* and outgoing connections. Use subclasses to instantiate a specific Activity.
* @see com.example.workflow.model.Activity
* @see com.example.workflow.model.StartActivity
* @see com.example.workflow.model.EndActivity
*/
public class AbstractActivity extends ModelElement{
private static final long serialVersionUID = 3023802629976246906L;
/** Property ID to use when the location of this Activity is modified. */
public static final String LOCATION_PROP = "Activity.Location";
/** Property ID to use then the size of this Activity is modified. */
public static final String SIZE_PROP = "Activity.Size";
/** ID for the Name property value (used for by the corresponding property descriptor). */
public static final String NAME_PROP = "Activity.Name";
/** Property ID to use when the list of outgoing transitions is modified. */
public static final String SOURCE_TRANSITIONS_PROP = "Activity.SourceTran";
/** Property ID to use when the list of incoming transitions is modified. */
public static final String TARGET_TRANSITIONS_PROP = "Activity.TargetTran";
/** ID for the Width property value (used for by the corresponding property descriptor). */
private static final String WIDTH_PROP = "Activity.Width";
/** ID for the X property value (used for by the corresponding property descriptor). */
private static final String XPOS_PROP = "Activity.xPos";
/** ID for the Y property value (used for by the corresponding property descriptor). */
private static final String YPOS_PROP = "Activity.yPos";
/** Name of this Activity. */
private String name = new String("");
/** Location of this Activity. */
private Point location = new Point(0, 0);
/** Size of this Activity. */
private Dimension size = new Dimension(50, 50);
/** List of outgoing Transitions. */
private List sourceTransitions = new ArrayList();
/** List of incoming Transitions. */
private List targetTransitions = new ArrayList();
/**
* Add an incoming or outgoing connection to this Activity.
* @param conn a non-null Transition instance
* @throws IllegalArgumentException if the Transition is null or has not distinct endpoints
*/
void addTransition(Transition tran) {
if (tran == null || tran.getSource() == tran.getTarget()) {
throw new IllegalArgumentException();
}
if (tran.getSource() == this) {
sourceTransitions.add(tran);
firePropertyChange(SOURCE_TRANSITIONS_PROP, null, tran);
} else if (tran.getTarget() == this) {
targetTransitions.add(tran);
firePropertyChange(TARGET_TRANSITIONS_PROP, null, tran);
}
}
/**
* Return the Name of this Activity.
* @return name
*/
public String getName() {
return name;
}
/**
* Return the Location of this Activity.
* @return a non-null location instance
*/
public Point getLocation() {
return location.getCopy();
}
/**
* Return the Size of this Activity.
* @return a non-null Dimension instance
*/
public Dimension getSize() {
return size.getCopy();
}
/**
* Return a List of outgoing Transitions.
*/
public List getSourceTransitions() {
return new ArrayList(sourceTransitions);
}
/**
* Return a List of incoming Transitions.
*/
public List getTargetTransitions() {
return new ArrayList(targetTransitions);
}
/**
* Remove an incoming or outgoing Transition from this Activity.
* @param conn a non-null Transition instance
* @throws IllegalArgumentException if the parameter is null
*/
void removeTransition(Transition tran) {
if (tran == null) {
throw new IllegalArgumentException();
}
if (tran.getSource() == this) {
sourceTransitions.remove(tran);
firePropertyChange(SOURCE_TRANSITIONS_PROP, null, tran);
} else if (tran.getTarget() == this) {
targetTransitions.remove(tran);
firePropertyChange(TARGET_TRANSITIONS_PROP, null, tran);
}
}
/**
* Set the Name of this Activity.
* @param newName
* @throws IllegalArgumentException if the parameter is null
*/
public void setName(String newName) {
if (newName == null) {
throw new IllegalArgumentException();
}
this.name = newName;
firePropertyChange(LOCATION_PROP, null, name);
}
/**
* Set the Location of this Activity.
* @param newLocation a non-null Point instance
* @throws IllegalArgumentException if the parameter is null
*/
public void setLocation(Point newLocation) {
if (newLocation == null) {
throw new IllegalArgumentException();
}
location.setLocation(newLocation);
firePropertyChange(LOCATION_PROP, null, location);
}
/**
* Set the Size of this Activity.
* Will not modify the size if newSize is null.
* @param newSize a non-null Dimension instance or null
*/
public void setSize(Dimension newSize) {
if (newSize != null) {
size.setSize(newSize);
firePropertyChange(SIZE_PROP, null, size);
}
}
}
在这个类中,我们定义两个List对象,分别对应该活动的输入转移和输出转移,因为对于一个完整的流程来说,每个活动都会转移的(或者有输入转移,或者有输出转移,或者两者都有),在这个类中,我们还注意到,每个改变对象属性的方法中,都会调用
firePropertyChange
方法,这个方面就是通知控制器,模型的属性发生发生变化了,让控制器根据相应的属性来刷新视图。
定义了活动的父类之后,我们就可以分别来定义开始活动,普通活动,结束活动对应的类了,具体代码如下:
开始活动:
package
com.example.workflow.model;
public
class
StartActivity
extends
AbstractActivity{
private
static
final
long
serialVersionUID
= 4639994300421360712L;
private
static
final
String
STARTACTIVITY_NAME
=
"START"
;
public
String getName() {
return
STARTACTIVITY_NAME
;
}
public
String toString() {
return
"StartActivity "
+ hashCode();
}
}
普通活动:
package
com.example.workflow.model;
public
class
Activity
extends
AbstractActivity{
private
static
final
long
serialVersionUID
= 3023802629976246906L;
private
static
final
String
ACTIVITY_NAME
=
"ACTIVITY"
;
public
String getName() {
return
ACTIVITY_NAME
;
}
public
String toString() {
return
"Activity "
+ hashCode();
}
}
结束活动:
package
com.example.workflow.model;
public
class
EndActivity
extends
AbstractActivity{
private
static
final
long
serialVersionUID
= 316984190041034535L;
private
static
final
String
ENDACTIVITY_NAME
=
"END"
;
public
String getName() {
return
ENDACTIVITY_NAME
;
}
public
String toString() {
return
"EndActivity "
+ hashCode();
}
}
定义完这些活动之后,我们来定义流程模型,由于流程中包含多个活动,所以里面应该有个列表来维护这些对象,同样,流程中还包含多个转移,由于在活动模型中,已经维护了转移对象,所以这里就不维护这些转移对象了,具体代码如下:
package com.example.workflow.model;
import java.util.ArrayList;
import java.util.List;
/**
* 流程模型,可以包含多个活动和转移模型
* @author Administrator
*
*/
public class WorkflowProcess extends ModelElement{
private static final long serialVersionUID = -5478693636480758659L;
/** Property ID to use when a child is added to this WorkflowProcess. */
public static final String CHILD_ADDED_PROP = "WorkflowProcess.ChildAdded";
/** Property ID to use when a child is removed from this WorkflowProcess. */
public static final String CHILD_REMOVED_PROP = "WorkflowProcess.ChildRemoved";
private List activities = new ArrayList();
/**
* Add a Activity to this WorkflowProcess.
* @param s a non-null Activity instance
* @return true, if the Activity was added, false otherwise
*/
public boolean addChild(Activity a) {
if (a != null && activities.add(a)) {
firePropertyChange(CHILD_ADDED_PROP, null, a);
return true;
}
return false;
}
/** Return a List of Activities in this WorkflowProcess. The returned List should not be modified. */
public List getChildren() {
return activities;
}
/**
* Remove a Activity from this WorkflowProcess.
* @param s a non-null Activity instance;
* @return true, if the Activity was removed, false otherwise
*/
public boolean removeChild(Activity a) {
if (a != null && activities.remove(a)) {
firePropertyChange(CHILD_REMOVED_PROP, null, a);
return true;
}
return false;
}
}
最后我们来定义转移模型,我们知道转移模型是链接两个活动的,所以在转移模型中,应该有个转移的源活动和目标活动,同时如果两个活动之间已经有转移连接时,就不能再在两者之间建立转移了,所以在两个活动之间建立转移时,必须先判断两者之间是否已经建立转移,所以转移模型具体代码如下:
package
com.example.workflow.model;
/**
*
A
Transition
between
two
distinct
activities.
*/
public
class
Transition
extends
ModelElement{
private
static
final
long
serialVersionUID
= 516473924757575767L;
/**
True,
if
the
transition
is
attached
to
its
endpoints.
*/
private
boolean
isConnected
;
/**
Transition's
source
endpoint.
*/
private
AbstractActivity
source
;
/**
Transition's
target
endpoint.
*/
private
AbstractActivity
target
;
/**
*
Create
a
Transition
between
two
distinct
activities.
*
@param
source
a
source
endpoint
for
this
Transition
(non
null)
*
@param
target
a
target
endpoint
for
this
Transition
(non
null)
*
@throws
IllegalArgumentException
if
any
of
the
parameters
are
null
or
source
==
target
*
@see
#setLineStyle(int)
*/
public
Transition(AbstractActivity source, AbstractActivity target) {
reconnect(source, target);
}
/**
*
Disconnect
this
connection
from
the
activities
it
is
attached
to.
*/
public
void
disconnect() {
if
(
isConnected
) {
source
.removeTransition (
this
);
target
.removeTransition (
this
);
isConnected
=
false
;
}
}
/**
*
Returns
the
source
endpoint
of
this
Transition.
*
@return
a
non
-
null
Activity
instance
*/
public
AbstractActivity getSource() {
return
source
;
}
/**
*
Returns
the
target
endpoint
of
this
Transition.
*
@return
a
non
-
null
Activity
instance
*/
public
AbstractActivity getTarget() {
return
target
;
}
/**
*
Reconnect
this
Transition.
*
The
Transition
will
reconnect
with
the
activities
it
was
previously
attached
to.
*/
public
void
reconnect() {
if
(!
isConnected
) {
source
.addTransition (
this
);
target
.addTransition (
this
);
isConnected
=
true
;
}
}
/**
*
Reconnect
to
a
different
source
and/or
target
Activity.
*
The
connection
will
disconnect
from
its
current
attachments
and
reconnect
to
*
the
new
source
and
target.
*
@param
newSource
a
new
source
endpoint
for
this
Transition
(non
null)
*
@param
newTarget
a
new
target
endpoint
for
this
Transition
(non
null)
*
@throws
IllegalArgumentException
if
any
of
the
paramers
are
null
or
newSource
==
newTarget
*/
public
void
reconnect(AbstractActivity newSource, AbstractActivity newTarget) {
if
(newSource ==
null
|| newTarget ==
null
|| newSource == newTarget) {
throw
new
IllegalArgumentException();
}
disconnect();
this
.
source
= newSource;
this
.
target
= newTarget;
reconnect();
}
}
到这儿,模型的定义已经全部完成,下一节我们将定义
GEF
框架中最重要的部分,也是最复杂的部分,控制器。