☰
GEF图形编辑器开发实战:MVC架构、EditPart与撤销重做机制详解
2026/10/24 3:10:34 网站建设 项目流程

1. 为什么图形编辑器离不开GEF这套框架

做过桌面端图形编辑工具的人大概都有过这种体会:画布上要放节点、连线、拖拽、缩放、对齐、撤销重做,每一样单独拎出来都不算难,可一旦要把它们揉进同一个应用里,代码很快就会变成一团乱麻。早期我参与过一个流程设计器项目,最初的想法很朴素——用最基础的画布加鼠标事件硬写,结果光是"选中一个节点后拖动它、同时让相连的线跟着动"这一个需求,就写了上千行状态判断,后面加个撤销功能直接推倒重来。

后来接触到GEF(Graphical Editing Framework),才意识到这类问题的本质不是"画不出来",而是编辑行为本身需要一套模型来管理。GEF是Eclipse平台体系里专门用来构建图形编辑器的框架,它把"图形长什么样"和"编辑操作怎么发生"这两件事彻底分开了。你负责定义数据模型和图形呈现,GEF负责处理请求、工具、命令、策略这一整套交互逻辑。

这套框架能解决的核心问题有三个。第一是交互的标准化:拖拽、创建、删除、连线这些操作,GEF都有对应的工具(Tool)和请求(Request)机制,不用自己从鼠标事件开始造轮子。第二是操作的可撤销:所有编辑动作都封装成命令(Command),天然支持撤销重做,这在图形编辑器里几乎是刚需。第三是模型与视图的解耦:数据模型变了,视图通过控制器自动刷新,不用手动去同步每一处界面。

适合读这篇内容的人,大概是这几类:正在用Java做桌面图形工具、流程图编辑器、拓扑图配置界面的开发者;用过GEF但被EditPart、EditPolicy、Request这一堆概念绕晕的人;以及想理解"一个成熟的图形编辑框架到底怎么组织代码"的技术爱好者。我会尽量把GEF里那些看起来抽象的概念,用实际项目里踩过的坑和写过的代码讲清楚,而不是照着文档念一遍。

需要提前说明的是,GEF的版本差异比较大,早期经典版本(GEF 3.x)和后来的GEF4、以及基于新架构的实现,在API和设计思路上有不小区别。这篇内容主要围绕经典GEF 3.x的MVC架构展开,因为它在存量项目和教学场景里仍然最常见,理解了这套思路,再看新版本会顺很多。

2. GEF的MVC不是普通MVC,三个角色各管一摊

2.1 Model、View、EditPart到底谁负责什么

很多人第一次看GEF的架构图会懵,因为它虽然叫MVC,但和Web开发里那套MVC完全不是一回事。在GEF里,三个核心角色是Model(模型)、View(视图)和EditPart(控制器),但它们之间的职责划分非常讲究。

Model是纯粹的数据。比如一个流程节点,它可能就是一个Java对象,里面有名字、坐标、尺寸、输入输出端口列表这些字段。Model不应该知道任何关于图形绘制的事情,它甚至不知道自己是画成矩形还是圆形。这一点很关键,我见过不少项目把颜色、字体这些样式信息塞进Model,结果后来想换一套皮肤,发现数据层和表现层缠死了,改起来极其痛苦。

View是纯粹的图形。在GEF里通常用Draw2D的Figure来表示,一个Figure就是一个可绘制的图形元素,能画矩形、椭圆、文字、图片,也能嵌套组合。View不应该保存业务数据,它只关心"我长什么样、我占多大地方"。

EditPart是中间那个"翻译官",也是整个GEF里最需要花心思的部分。它一头连着Model,一头连着View,负责三件事:把Model的变化反映到View上(刷新)、把用户的鼠标操作翻译成对Model的修改(编辑)、以及决定某个位置上该响应什么操作(策略)。你可以把EditPart理解成一个"双向适配器",它既监听模型,又处理输入。

提示:判断一个字段该放Model还是View,有个简单的标准——如果这个字段在"保存文件"时需要被持久化,它属于Model;如果它只是为了让界面好看,属于View。坐标这种既影响显示又需要保存的,通常放Model,View从Model读取。

2.2 为什么EditPart要拆成这么多层

刚上手时最容易困惑的是:为什么一个节点要对应一个EditPart,节点里的每个小部件还要各自有EditPart?这不是把简单问题复杂化了吗?

其实这是GEF刻意设计的分层编辑能力。设想一个流程节点,它整体可以被拖动、被删除,但它内部的文字标签可以被双击编辑,它边缘的端口可以被拉出连线。这些操作的粒度和作用对象都不一样。如果只有一个EditPart,所有请求都堆在一起,判断逻辑会爆炸。拆成父子EditPart之后,每个EditPart只关心自己那一层的操作,父EditPart处理"整体拖动",子EditPart处理"标签编辑",职责清晰。

这种结构在代码上的体现就是getModelChildren()方法。父EditPart通过它告诉框架"我的子元素有哪些",框架据此为每个子元素创建对应的子EditPart。整个编辑器的EditPart树,和Model树、Figure树是同构的,三棵树一一对应,这是GEF最优雅的地方,也是理解它的钥匙。

2.3 请求、策略、命令:一次点击背后的完整链路

用户在画布上点一下鼠标,GEF内部到底发生了什么?把这条链路走通,很多概念就自然串起来了。

鼠标事件先被工具(Tool)捕获。GEF默认提供选择工具、创建工具、连线工具等,工具决定"这次操作大概是要干什么"。工具根据当前鼠标位置,找到对应的EditPart,然后向它发送一个请求(Request),比如"我想在这里创建一个节点"或者"我想拖动这个元素"。

EditPart收到请求后,不会自己处理,而是交给编辑策略(EditPolicy)。EditPolicy是挂在EditPart上的,一个EditPart可以挂多个策略,每个策略负责一类请求。比如LayoutEditPolicy管子元素的布局和创建,ComponentEditPolicy管删除,GraphicalNodeEditPolicy管连线的起止。策略判断"这个请求我能不能处理",能处理就返回一个命令(Command)。

命令是GEF里所有修改操作的统一封装。它有两个核心方法:execute()执行修改,undo()撤销修改。框架拿到命令后先执行,同时把它压入命令栈,用户按撤销时就从栈里弹出反向执行。这就是撤销重做功能的底层实现——不是记录状态快照,而是记录操作本身。

这条链路的价值在于:每一环都可以替换和扩展。你想自定义一种拖拽行为,改对应的策略就行;想加一种新的编辑操作,写个新命令就行。整个系统是开放的,而不是一个封闭的黑盒。

3. 从零搭一个可运行的GEF编辑器骨架

3.1 环境准备里最容易被忽略的依赖关系

搭GEF项目,第一步不是写代码,而是把依赖理清楚。GEF本身是Eclipse体系的一部分,它的运行依赖Draw2D做图形绘制,依赖EMF(可选)做模型管理,还依赖Eclipse的运行时平台提供插件机制和命令框架。

如果你是在Eclipse插件项目(RCP)里用GEF,通常通过MANIFEST.MF声明依赖,需要引入org.eclipse.gef、org.eclipse.draw2d、org.eclipse.gef.ui等插件。如果你想把GEF用在普通Java应用里(不启动整个Eclipse平台),那就需要手动引入对应的jar包,并且要自己初始化一些平台服务,这一步是新手最容易卡住的地方。

我个人的经验是:如果只是学习和小型工具,优先用Eclipse插件项目模板,因为平台帮你处理了大量初始化工作,你能把精力放在GEF本身。等真正理解了各组件的关系,再考虑脱离平台做独立应用。反过来一上来就追求"纯Java独立运行",往往会在依赖和初始化上耗掉大量时间,还没摸到GEF的门道就先放弃了。

依赖版本上要注意,GEF 3.x和Draw2D的版本要匹配,混用不同版本的jar包经常出现NoClassDefFoundError或者图形渲染异常。稳妥的做法是统一从同一个Eclipse发行版里取插件,别东拼西凑。

3.2 定义Model:把数据结构和图形彻底分开

先定义模型。假设我们要做一个简单的流程编辑器,核心元素是节点和连线。

public class Node { private String id; private String name; private int x; private int y; private int width = 100; private int height = 60; private List<Connection> outputs = new ArrayList<>(); // getter/setter 省略 } public class Connection { private String id; private Node source; private Node target; // getter/setter 省略 }

注意这里Node里存了坐标和尺寸,因为它们是业务数据的一部分,需要被保存。但没有存颜色、字体、边框样式,那些属于View。Connection里存的是源节点和目标节点的引用,而不是具体的坐标点,因为连线的路径应该由图形层根据两端节点的位置动态计算,模型层不该关心线怎么拐弯。

模型还需要一个"容器"来管理所有节点和连线,通常叫Diagram或者Canvas:

public class Diagram { private List<Node> nodes = new ArrayList<>(); private List<Connection> connections = new ArrayList<>(); // 增删查方法 }

模型层最好保持"哑对象"的状态,不依赖任何GEF或Draw2D的类。这样做的好处是模型可以被单独测试,也可以被序列化成JSON或XML保存,完全不牵扯图形逻辑。

3.3 用Figure画出节点和连线

接下来是View层,用Draw2D的Figure来画。一个节点通常用Figure加一个Label组合:

public class NodeFigure extends Figure { private Label nameLabel; public NodeFigure() { setLayoutManager(new FlowLayout()); setBorder(new LineBorder(ColorConstants.black)); nameLabel = new Label(); add(nameLabel); } public void setName(String name) { nameLabel.setText(name); } }

连线用PolylineConnection,它能自动根据两端锚点计算路径:

public class ConnectionFigure extends PolylineConnection { public ConnectionFigure() { setTargetDecoration(new PolygonDecoration()); setLineWidth(2); } }

Figure层的关键是布局管理器(LayoutManager)。GEF里常用的有XYLayout(绝对定位,适合自由拖拽的画布)、FlowLayout(流式排列)、ToolbarLayout(工具栏式)等。选哪种布局,直接决定了子元素怎么摆放。自由画布一般用XYLayout,因为节点位置由模型里的坐标决定。

注意:Figure的绘制和布局是两回事。绘制决定"长什么样",布局决定"放在哪、占多大"。新手常犯的错误是把位置计算写进paintFigure里,结果布局和绘制打架,图形位置飘忽不定。位置应该交给布局管理器,绘制只管画。

3.4 EditPart:连接模型与图形的关键桥梁

EditPart是重头戏。每个Model元素对应一个EditPart,每个EditPart负责创建自己的Figure,并在模型变化时刷新图形。

public class NodeEditPart extends AbstractGraphicalEditPart { @Override protected IFigure createFigure() { return new NodeFigure(); } @Override protected void createEditPolicies() { installEditPolicy(EditPolicy.COMPONENT_ROLE, new ComponentEditPolicy()); installEditPolicy(EditPolicy.GRAPHICAL_NODE_ROLE, new NodeGraphicalNodeEditPolicy()); } @Override protected void refreshVisuals() { Node node = (Node) getModel(); NodeFigure figure = (NodeFigure) getFigure(); figure.setName(node.getName()); // 通知父容器重新布局该子元素 ((GraphicalEditPart) getParent()).setLayoutConstraint(this, figure, new Rectangle(node.getX(), node.getY(), node.getWidth(), node.getHeight())); } @Override protected List<Node> getModelChildren() { return Collections.emptyList(); } }

createFigure()负责造图形,createEditPolicies()负责装策略,refreshVisuals()负责把模型数据同步到图形。这三个方法是每个EditPart的核心。

setLayoutConstraint这一步特别关键,也特别容易漏。它告诉父容器的布局管理器"这个子元素应该放在这个矩形区域"。如果忘了调用,节点位置就不会更新,拖动后图形不动,但模型里的坐标其实已经变了,造成"数据和显示不一致"的诡异现象。

3.5 编辑器与查看器:把画布装进应用窗口

最后需要一个GraphicalViewer来承载整个画布,以及一个Editor来管理编辑器的生命周期。

public class DiagramEditor extends GraphicalEditor { private Diagram diagram; @Override protected void configureGraphicalViewer() { super.configureGraphicalViewer(); GraphicalViewer viewer = getGraphicalViewer(); viewer.setEditPartFactory(new DiagramEditPartFactory()); viewer.setRootEditPart(new ScalableFreeformRootEditPart()); viewer.setContents(diagram); } @Override protected void initializeGraphicalViewer() { // 初始化画布内容 } }

EditPartFactory是工厂,框架通过它根据Model类型创建对应的EditPart。RootEditPart是根节点,ScalableFreeformRootEditPart支持缩放和自由滚动,适合大画布。setContents()把模型交给查看器,框架会自动构建整棵EditPart树。

到这里,一个最基础的GEF编辑器骨架就搭起来了:能显示节点、能选中、能拖动。虽然功能还很少,但架构已经完整,后面加功能都是在这个骨架上扩展。

4. 让编辑器真正好用:交互、连线与撤销重做

4.1 拖动节点时连线为什么不动

骨架搭好后,第一个要解决的问题通常是拖动。GEF默认的ComponentEditPolicy配合XYLayoutEditPolicy,其实已经能实现基本的拖动,但很多人会发现:节点动了,连着它的线却没跟着动。

原因在于连线的刷新没有触发。连线Figure的位置是根据两端节点的锚点动态计算的,节点位置变了,连线需要重新计算路径。GEF里负责这件事的是ConnectionEditPart的refreshVisuals(),但前提是节点移动时能通知到连线。

标准做法是让节点EditPart在刷新时,通过EditPart.getSourceConnections()和getTargetConnections()找到所有相连的连线,逐个调用它们的refresh()。或者更优雅的方式,是在模型层做监听:节点坐标变化时发出通知,连线EditPart监听节点模型,收到通知就刷新自己。

// 在NodeEditPart的refreshVisuals中 for (Object conn : getSourceConnections()) { ((ConnectionEditPart) conn).refresh(); } for (Object conn : getTargetConnections()) { ((ConnectionEditPart) conn).refresh(); }

这个坑我踩过不止一次。表面看是"线不跟着动",本质是刷新传播链断了。GEF不会自动帮你把变化传播到所有相关元素,该手动通知的地方必须通知。理解这一点后,很多"图形不更新"的问题都能顺藤摸瓜找到原因。

4.2 连线工具与锚点的配合逻辑

连线是图形编辑器里最复杂的交互之一。用户从源节点的某个端口按下鼠标,拖到目标节点松开,生成一条连线。这个过程涉及工具、请求、策略、命令四者的配合。

首先需要一个连线工具,通常继承ConnectionCreationTool,或者用CreationTool配合自定义的CreateConnectionRequest。工具负责在鼠标拖动过程中显示一条"橡皮筋"预览线,松开时确定目标。

然后是锚点(Anchor)。锚点决定连线连在节点的哪个位置。GEF提供ChopboxAnchor(连到节点边框的最近点)、FixedAnchor(固定位置)、XYAnchor(指定坐标)等。如果节点有明确的端口,通常用FixedAnchor挂在端口Figure上;如果只是连到节点整体,ChopboxAnchor最省事。

策略方面,源节点需要装GraphicalNodeEditPolicy,它负责响应"开始连线"的请求,返回一个创建连线的命令。目标节点也要装同样的策略,负责响应"完成连线"的请求。两个策略配合,才能完成一次完整的连线操作。

public class NodeGraphicalNodeEditPolicy extends GraphicalNodeEditPolicy { @Override protected Command getConnectionCompleteCommand( CreateConnectionRequest request) { ConnectionCreateCommand cmd = (ConnectionCreateCommand) request.getStartCommand(); cmd.setTarget((Node) getHost().getModel()); return cmd; } @Override protected Command getConnectionCreateCommand( CreateConnectionRequest request) { ConnectionCreateCommand cmd = new ConnectionCreateCommand(); cmd.setSource((Node) getHost().getModel()); request.setStartCommand(cmd); return cmd; } }

这里有个容易忽略的细节:getConnectionCreateCommand返回的命令会被暂存,等到鼠标松开时,getConnectionCompleteCommand再把它取出来补上目标节点,然后才真正执行。所以命令对象要设计成"可分步设置参数"的,别在构造时就要求所有参数齐全。

4.3 撤销重做不是自动的,命令要写对

GEF的撤销重做依赖命令栈,但前提是你的命令正确实现了undo()。我见过不少项目,命令的execute()写得好好的,undo()随便糊弄甚至留空,结果撤销时要么没反应,要么把界面搞乱。

一个规范的命令应该这样写:

public class CreateNodeCommand extends Command { private Diagram diagram; private Node node; @Override public void execute() { diagram.addNode(node); } @Override public void undo() { diagram.removeNode(node); } @Override public void redo() { execute(); } }

关键原则是:undo()必须是execute()的严格逆操作。execute加了什么,undo就删什么;execute改了什么值,undo就改回去。如果execute里做了多件事,undo里要按相反顺序全部还原。

还有一个坑是命令的合并。比如用户连续拖动节点,如果每次鼠标移动都产生一个命令,撤销时就要按很多次才能回到起点。GEF提供了命令合并机制,通过Command.canExecute()和chain相关方法,可以把连续的同类操作合并成一个。对于拖动这种高频操作,合并几乎是必须的,否则用户体验会很差。

提示:调试撤销功能时,可以临时在命令栈上打印每次push和pop的命令类名,观察操作序列是否符合预期。很多撤销bug其实是命令入栈顺序或合并逻辑出了问题,而不是undo本身写错了。

4.4 选中、对齐与吸附的实用技巧

选中是最基础的交互,GEF默认支持。但要做多选、框选、以及选中后的对齐辅助线,就需要额外工作。

框选靠MarqueeSelectionTool,它允许用户拖出一个矩形,选中区域内所有元素。这个工具默认就有,但要注意它和拖动工具的优先级——如果两者冲突,用户想框选却触发了拖动,体验会很糟。解决办法是通过Tool的激活条件区分,比如按住某个修饰键时才启用框选。

对齐辅助线(俗称"智能参考线")是提升编辑器专业感的关键细节。实现思路是:在拖动过程中,实时计算当前节点与其他节点的边缘、中心线是否接近,如果距离小于某个阈值(比如5像素),就显示一条辅助线并把节点吸附过去。这个逻辑通常写在自定义的拖动策略里,在showTargetFeedback阶段绘制辅助线,在命令执行时修正坐标。

吸附阈值的选择有讲究。太小了用户感觉不到吸附,太大了又会"抢"用户的操作,让精细定位变得困难。实测下来5到8像素是个比较舒服的范围,具体还要看画布的缩放比例——如果画布缩小了,阈值也要相应调整,否则吸附会变得过于敏感。

5. 那些文档里不会写的踩坑记录

5.1 图形不刷新?先查这三处

"我明明改了模型,图形就是不动"——这是GEF新手遇到频率最高的问题。排查时按这个顺序走,基本能定位到原因。

第一处,EditPart有没有调用refreshVisuals()。模型变了不会自动触发刷新,你得在合适的地方(比如命令执行后,或者模型监听器里)显式调用refresh()或refreshVisuals()。很多人以为改了模型框架就会知道,其实不会。

第二处,setLayoutConstraint有没有调用。前面提过,位置类信息必须通过它通知父容器的布局管理器,否则布局不会重算。这个漏掉的表现是:模型坐标变了,但图形纹丝不动。

第三处,刷新有没有传播到关联元素。节点动了,连线要刷新;容器大小变了,子元素可能要重排。这些关联刷新GEF不会自动做,需要你在代码里手动通知。判断方法很简单:如果单独一个元素刷新正常,但一涉及关联就出问题,那基本就是传播链断了。

5.2 EditPart与Model不同步引发的诡异现象

有一类bug特别难查:界面显示的和模型里的数据对不上,或者操作几次之后程序状态错乱。这类问题的根源往往是EditPart和Model的生命周期没对齐。

比如删除一个节点时,如果只从模型里删了,但没让对应的EditPart也销毁,那个EditPart就成了"孤儿",还挂在EditPart树上,继续响应鼠标事件,但它的模型已经不存在了,一访问就抛异常或者拿到脏数据。正确的做法是:模型删除后,通过EditPart.removeChild()或者让父EditPart重新refreshChildren(),把对应的EditPart也清理掉。

反过来也一样,如果动态往模型里加了元素,要记得让父EditPart刷新子列表,否则新元素不会显示。GEF提供了refreshChildren()方法,它会对比模型子元素和EditPart子元素,自动增删。在模型结构变化后调用它,是最省心的同步方式。

5.3 性能问题:节点一多就卡怎么办

当画布上有几百上千个节点时,GEF的性能问题会暴露出来。卡顿通常来自几个方面。

一是刷新过于频繁。每次鼠标移动都触发全量刷新,开销巨大。优化思路是缩小刷新范围,只刷新真正变化的元素,而不是整个画布。GEF的refresh()方法可以指定刷新哪些部分,用好了能省很多。

二是Figure层级过深。每个节点如果嵌套了十几层Figure,绘制和布局的开销会累积。能合并的Figure尽量合并,能用简单图形实现的别用复杂组合。

三是连线路径计算。连线如果用了复杂的路由算法(比如自动避让障碍物),节点一多计算量会爆炸。这种情况下可以考虑懒计算——只在连线可见时才计算路径,或者用简化的直线代替复杂曲线。

还有一个容易被忽视的点是选择反馈。选中元素时如果绘制了复杂的反馈图形,大量选中时也会卡。反馈图形要尽量轻量。

5.4 模型持久化时容易丢的字段

编辑器做完,总要把用户画的图保存下来。持久化时最常见的坑是字段丢失,尤其是那些"看起来不重要"的字段。

坐标和尺寸一般不会忘,但像节点的唯一ID、连线的源目标引用、自定义属性这些,很容易在序列化时漏掉。漏掉ID的后果是重新加载后无法建立连线关系;漏掉自定义属性的后果是用户配置的信息丢失。

我的做法是给模型类写一套完整的序列化和反序列化逻辑,并且写单元测试验证往返一致性——把模型序列化再反序列化,对比两个对象的所有字段是否相等。这个测试能提前发现绝大多数持久化遗漏。另外,模型里如果有循环引用(比如节点引用连线、连线又引用节点),序列化时要特别处理,否则会无限递归。

6. 从能用到好用:几个值得投入的扩展方向

6.1 自定义EditPolicy实现特殊交互

GEF的策略机制是它最强大的扩展点。当你需要一种框架没提供的交互时,写一个自定义EditPolicy往往比改框架代码更合适。

比如要实现"双击节点展开/折叠子节点",可以写一个策略,在getCommand()里判断请求类型是双击,然后返回一个切换折叠状态的命令。再比如"按住Alt拖动复制节点",可以在拖动策略里判断修饰键,如果是Alt就返回一个创建副本的命令而不是移动命令。

写自定义策略的关键是搞清楚请求的类型和时机。GEF的请求有很多种,ChangeBoundsRequest管位置尺寸变化,CreateRequest管创建,SelectionRequest管选择。你的策略只需要处理你关心的那几种,其他的返回null让框架继续找别的策略。理解请求体系后,几乎任何交互都能实现。

6.2 用装饰图层做辅助信息展示

编辑器除了画节点和连线,往往还需要展示一些辅助信息,比如节点状态标记、告警图标、悬浮提示。这些信息如果直接画在节点Figure上,会让节点Figure变得臃肿,而且不好控制层级。

更好的做法是用装饰图层(Layer)。GEF的根EditPart管理着多个图层,比如PRIMARY_LAYER放主要内容,CONNECTION_LAYER放连线,FEEDBACK_LAYER放操作反馈,HANDLE_LAYER放选中手柄。你可以往这些图层里添加自定义的Figure,用来展示辅助信息,它们会浮在主要内容之上,且不影响主内容的布局。

悬浮提示(Tooltip)也是类似思路,通过Figure.setToolTip()挂一个提示对象,鼠标悬停时自动显示。这个功能对提升编辑器的易用性帮助很大,尤其是节点信息比较多的时候。

6.3 键盘快捷键与命令框架的整合

纯鼠标操作的编辑器效率有限,加上键盘快捷键会好用很多。GEF本身不直接管快捷键,但Eclipse平台提供了命令框架(Command Framework),两者可以整合。

思路是把每个编辑操作定义成一个平台命令,绑定快捷键,命令执行时再调用GEF的命令。这样快捷键和菜单、工具栏就能共用同一套逻辑,不用为每个入口单独写代码。Delete键删除选中元素、Ctrl+Z撤销、Ctrl+Y重做,这些标准快捷键都应该支持,用户会形成肌肉记忆。

需要注意的是快捷键的上下文(Context)。有些快捷键只在特定条件下有效,比如"删除"只在有选中元素时可用。命令框架支持通过isEnabled()控制命令的可用状态,把这个逻辑写对,能避免用户按了快捷键却没反应或者误操作。

6.4 多编辑器实例下的资源共享

如果一个应用里同时打开多个图形编辑器,资源管理就变得重要。每个编辑器都有自己的GraphicalViewer和EditPart树,但底层的字体、颜色、图片这些资源可以共享。

Draw2D的字体和颜色对象如果每个编辑器都新建一份,数量一多会占用可观的内存。更好的做法是用一个全局的资源注册表,按需创建、统一管理。图片资源同理,加载一次缓存起来,多个编辑器共用。

另一个要注意的是命令栈的隔离。每个编辑器应该有独立的命令栈,撤销操作只影响当前编辑器,不能串到别的编辑器去。这一点在实现时要注意别把命令栈做成全局单例,否则多编辑器场景下撤销行为会错乱。

7. 我在这套框架上的一些真实体会

用GEF做图形编辑器,最大的感受是前期学习曲线陡,但后期扩展省心。刚开始被EditPart、EditPolicy、Request这些概念绕得头晕,写一个简单功能要碰好几个类,感觉比直接画图麻烦多了。但一旦跨过那道坎,理解了"模型-视图-控制器"三棵树同构的设计,后面加功能就变得很顺——想加一种交互,写个策略;想加一种操作,写个命令;想改显示,改Figure。每个改动都局限在很小的范围内,不会牵一发而动全身。

另一个体会是别和框架对着干。GEF有它自己的设计哲学,比如刷新要靠手动通知、命令要成对实现、布局和绘制要分开。这些约束一开始觉得别扭,但顺着它的思路走,代码会越来越清晰;非要按自己的习惯硬来,最后往往是一堆难以维护的补丁。我早期就干过在paintFigure里直接算位置的事,后来重构时全部推倒重来,教训很深。

最后分享一个调试小技巧:GEF的问题很多时候出在"某一步没触发"或者"触发顺序不对"。遇到诡异现象时,可以在关键的刷新方法、命令执行方法里打日志,把整个调用链打出来,往往一眼就能看出哪一环断了。图形编辑器的调试不像普通业务逻辑那么直观,日志比断点更好用,因为断点会打断交互流程,而日志能完整记录一次操作的来龙去脉。

这套框架不算新,但它在图形编辑这个领域沉淀下来的设计思想,放到今天依然有参考价值。理解了它怎么组织模型、怎么管理操作、怎么解耦显示和逻辑,即使以后换用别的图形库,这些思路也能迁移过去。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询