先简单回顾一下Request在GEF里的作用。Request是GEF里一个比较重要的角色,Tool将原始的鼠标事件转换为EditPart可以识别的请求,Request则承载了这些请求信息。举例来说,用户在调色板(Palette)里选择了创建节点工具(CreationTool),然后在画布区域按下鼠标左键,这时产生在画布上的鼠标单击事件将被CreationTool转换为一个CreateRequest,它里面包含了要创建的对象,坐标位置等信息。 EditPart上如果安装了能够处理CreateRequest的EditPolicy,则相应的EditPolicy会根据这个 CreateRequest创建一个Command,由后者实际执行创建新对象的必要操作。
GEF已经为我们提供了很多种类的Request,其中最常用的是CreateRequest及其子类 CreateConnectionRequest,其他比较常见的还有SelectionRequest,ChangeBoundsRequest和 ReconnectRequest等等。要实现一个典型的图形化应用程序,例如UML类图编辑器,这些预定义的Request基本够用了。然而各种稀奇古怪的需求我相信大家也见过不少,很多需求不太符合约定俗成的使用习惯,因此实现起来更多依赖开发人员的编码,而不是开发框架带来的便利。在这种时候,我们唯一的期望就是开发框架提供足够的扩展机制,以便让我们额外编写的代码能和其他代码和平共处,幸好GEF是具有足够的扩展性的。有点跑题了,再回到Request的问题上,为了说明什么情况下需要自定义 Request,我在前文“应用实例”里的示例应用基础上假设一个新的需求:
在Palette里增加三个工具,作用分别是把选中节点的背景颜色改变为红色、绿色和蓝色。
假如你用过Photoshop或类似软件,这个需求很像给节点上色的“油漆桶”或“上色工具”,当然在用户界面的背后,实际应用里这些颜色可能代表一个节点的重要程度,优先级或是异常信息等等。现在,让我们通过创建一个自定义的Request来实现这个需求,还是以前文中的示例项目为基础。
一、首先,原来的模型里节点(Node)类里没有反映颜色的成员变量,所以先要在Node类里添加一个color属性,以及相应的 getter/setter方法,注意这个setter方法里要和其他成员变量的setter方法一样传递模型改变的消息。仿照其他成员变量,还应该有一个静态字符串变量,用来区分消息对应哪个属性。
final public static String PROP_COLOR = "COLOR"; protected RGB color = new RGB(255, 255, 255); public RGB getColor() { return color; } public void setColor(RGB color) { if (this.color.equals(color)) { return; } this.color = color; firePropertyChange(PROP_COLOR, null, color); }
二、然后,要让Node的color属性变化能够反映到图形上,因此要修改NodePart里的propertyChanged()和 refreshVisuals()方法,在前者里增加对color属性的响应,在后者里将NodeFigure的背景颜色设置为Node的color属性对应的颜色。(注意,Color对象是系统资源对象,实际使用里需要缓存以避免系统资源耗尽,为节约篇幅起见,示例代码直接new Color()了)
public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals(Node.PROP_COLOR))//Response to color change refreshVisuals(); } protected void refreshVisuals() { ((NodeFigure) this.getFigure()).setBackgroundColor(new Color(null, node.getColor()));//TODO cache color instances }
三、现在来创建我们自己的Request,因为目的是改变颜色,所以不妨叫做ChangeColorRequest。它应当继承自org.eclipse.gef.Request,我们需要ChangeColorRequest上带有两样信息:1.需要改变颜色的节点;2.目标颜色。因此它应该有这两个成员变量。
import org.eclipse.gef.Request; import org.eclipse.swt.graphics.RGB; import com.example.model.Node; public class ChangeColorRequest extends Request{ final static public String REQ_CHANGE_COLOR="REQ_CHANGE_COLOR"; private Node node; private RGB color; public ChangeColorRequest(Node node, RGB color) { super(); this.color = color; this.node = node; setType(REQ_CHANGE_COLOR); } public RGB getColor() { return color; } public Node getNode() { return node; } public void setNode(Node node) { this.node = node; } public void setColor(RGB color) { this.color = color; } }
ChangeColorRequest看起来和一个JavaBean差不多,的确如此,因为Request的作用就是传递翻译后的鼠标事件。如果你看一下org.eclipse.gef.Request的代码,你会发现Request还有一个type属性,这个属性一般是一个字符串(在gef的RequestConstants里预定义了一些,如RequestConstants.REQ_SELECTION_HOVER), EditPolicy可以根据它决定是否处理这个Request。在我们的例子里,顺便定义了这样一个常量字符串REQ_CHANGE_COLOR,在后面的 ChangeColorEditPolicy里会用到它。
四、现在有一个问题,这个Request的实例应该在哪里生成?答案是在Tool里,用户在画布区域按下鼠标左键时,当前 Palette里被选中的Tool负责创建一个Request。我们现在面对的这个需求需要我们创建一种新的Tool:ChangeColorTool。我们让ChangeColorTool继承org.eclipse.gef.tools.SelectionTool,因为“上色工具”的用法和“选择工具”基本上差不多。显然,我们需要覆盖的是handleButtonDown()方法,用来告诉gef如果用户当前选择了这个工具,在画布区域按下鼠标会发生什么事情。代码如下:
import org.eclipse.gef.EditPart; import org.eclipse.gef.commands.Command; import org.eclipse.gef.tools.SelectionTool; import org.eclipse.swt.graphics.RGB; import com.example.model.Node; import com.example.parts.NodePart; public class ChangeColorTool extends SelectionTool { private RGB color; public ChangeColorTool(RGB color) { super(); this.color = color; } /** * If target editpart is an {@link NodePart}, create a {@link ChangeColorRequest} instance, * get command from target editpart with this request and execute. */ @Override protected boolean handleButtonDown(int button) { //Get selected editpart EditPart editPart = this.getTargetEditPart(); if (editPart instanceof NodePart) { NodePart nodePart = (NodePart) editPart; Node node = (Node) nodePart.getModel(); //Create an instance of ChangeColorRequest ChangeColorRequest request = new ChangeColorRequest(node, color); //Get command from the editpart Command command = editPart.getCommand(request); //Execute the command this.getDomain().getCommandStack().execute(command); return true; } return false; } }
五、有了Tool,还需要用ToolEntry把它包装起来添加到Palette里。所以我们创建一个名为 ChangeColorToolEntry并继承org.eclipse.gef.palette.ToolEntry的类,覆盖createTool ()方法,让它返回我们的ChangeColorTool实例。这个ChangeColorToolEntry代码应该很容易理解:
import org.eclipse.gef.SharedCursors; import org.eclipse.gef.Tool; import org.eclipse.gef.palette.ToolEntry; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.graphics.RGB; public class ChangeColorToolEntry extends ToolEntry { private RGB color; public ChangeColorToolEntry(RGB color, String label, String shortDesc, ImageDescriptor iconSmall, ImageDescriptor iconLarge) { super(label, shortDesc, iconSmall, iconLarge); this.color = color; } @Override public Tool createTool() { ChangeColorTool tool = new ChangeColorTool(color); tool.setUnloadWhenFinished(false);//Switch to selection tool after performed? tool.setDefaultCursor(SharedCursors.CROSS);//Any cursor you like return tool; } }
六、要把三个这样的ToolEntry添加到Palette里,当然是通过修改原来的PaletteFactory类。为节约篇幅,这里就不帖它的代码了,可以下载并参考示例代码PaletteFactory.java里的createCategories()和 createColorDrawer()方法。
到目前为止,ChangeColorRequest已经可以发出了,接下来要解决的问题是如何让EditPart处理这个请求。
七、我们知道,gef里任何对模型的修改都是通过command完成的,因此一个ChangeColorCommand肯定是需要的。它的execute()方法和undo()方法如下所示:
public class ChangeColorCommand extends Command{ private RGB oldColor; @Override public void execute() { oldColor = node.getColor(); node.setColor(color); } @Override public void undo() { node.setColor(oldColor); } }
八、EditPolicy负责接收所有的Request,所以还要创建一个ChangeColorEditPolicy。在下面列出的代码里,你会看到我们定义了一个新的“Role”字符串,过一会儿我们在EditPart上安装这个EditPolicy的时候要以这个字符串作为Key,以避免覆盖EditPart上已有的其他EditPolicy。
import org.eclipse.gef.Request; import org.eclipse.gef.commands.Command; import org.eclipse.gef.editpolicies.AbstractEditPolicy; import org.eclipse.swt.graphics.RGB; import com.example.model.Node; public class ChangeColorEditPolicy extends AbstractEditPolicy { final static public String CHANGE_COLOR_ROLE = "CHANGE_COLOR_ROLE"; @Override public Command getCommand(Request request) { //Judge whether this request is intersting by its type if (request.getType() == ChangeColorRequest.REQ_CHANGE_COLOR) { ChangeColorRequest theRequest = (ChangeColorRequest) request; //Get information from request Node node = theRequest.getNode(); RGB color = theRequest.getColor(); //Create corresponding command and return it ChangeColorCommand command = new ChangeColorCommand(node, color); return command; } return null; } }
九、最后还是回到EditPart,前面在第二个步骤里我们曾经修改过的NodePart里还有最后一处需要添加,那就是在installEditPolicies()方法里添加刚刚创建的ChangeColorEditPolicy:
protected void createEditPolicies() { //Add change color editpolicy installEditPolicy(ChangeColorEditPolicy.CHANGE_COLOR_ROLE, new ChangeColorEditPolicy()); }
现在我们已经完成了所有必要的修改,来看一下运行结果。
总结一下,需要创建的类有:ChangeColorRequest, ChangeColorTool, ChangeColorToolEntry, ChangeColorCommand, ChangeColorEditPolicy;需要修改的类有:Node, NodePart, PaletteFactory。在实例项目里,为了方便大家浏览,所有新创建的类都放在com.example.request包里,实际项目里还是建议分别放在对应的包里。
下载示例代码(在eclipse3.2.1和gef3.2下编译通过)