简介:这是一份基于JavaFx开发的流程图设计器源码,属于Java课程设计/期末大作业项目,适合正在学习Java图形界面开发、需要完成类似课题的学生参考和使用。项目利用JavaFx的Canvas绘图、Stage/Scene界面搭建及事件处理机制,实现了流程图元素的创建、编辑、拖拽与连线,并涵盖保存加载、撤销重做等常见功能,可帮助读者理解JavaFx客户端应用的整体结构与交互逻辑。压缩包共21个文件,以13个Java源文件为主,辅以6个XML工程配置、1个说明文档和1个Git忽略配置文件,并采用Maven工程结构,便于导入;整包仅26KB,轻量简洁。目前已有65人学习使用。通过这份资源,读者不仅可以获得一个可运行的流程图设计器示例,还能从中梳理界面布局、图形绘制与交互响应的实现思路,为后续独立开发图形类工具打下扎实基础,积累实战经验。
1. 期末大作业为什么绕不开JavaFX流程图设计器:它到底解决什么问题
其实这门作业在导师眼里并不难——把一段业务逻辑用“图形”表达出来:节点可拖、可连线、可保存,课程里学过的面向对象、集合、I/O、事件处理全都能展示出来。从学生的角度来讲,几乎每个做Java期末大作业的人手里都有一份“流程图设计器”的选题,因为它可视化结果直接,演示效果好,而且JavaFX从JDK 8开始就具备成熟的场景图模型,做桌面端图形应用几乎是成本最低的路径。这个标题背后是一个包含源码、配置和运行说明的课程设计交付包,下面我会把JavaFX流程图设计器的核心架构、可复现代码和常见坑讲透。适合读者:正在选型的期末学生,以及想在课程设计里补一个完整图形交互项目的同学。
2. JavaFX选型与整体架构:用场景图思维拆解流程图设计器
2.1 JavaFX为什么比Swing更适合流程图场景
在JDK 8之前,Swing基本是官方唯一的桌面GUI方案。Swing的组件树和重绘机制,在高频重绘场景下表现得比较迟钝——拖拽一个节点往往触发整个容器刷新,复杂画布上明显能感觉到闪烁。流程图设计器需要大量图形和连续交互,拿Swing做不是做不出来,而是后期排错非常痛苦。
JavaFX的核心优势在于场景图(Scene Graph)模型:图形组件、容器、文本全部是Node节点,天然支持鼠标事件、CSS样式表和坐标变换。写流程图设计器,核心交互是“拖拽”和“连线”,这两点在JavaFX里几乎不需要额外封装图形引擎。另一个实际行动的理由是JavaFX自带绑定(Binding)机制,连线的坐标可以用声明式方式绑定到节点上,代码量比Swing少很多,期末项目后期改动也轻松。
2.2 核心数据模型:FlowNode、FlowLine与DiagramModel
设计器里的“图”并不是JavaFX的Pane,而是你自己的数据模型。一个常见的做法是先定义三个纯Java实体类,分别代表节点、连线和整张图:
public class FlowNode { private String id; // 全局唯一,连线时引用 private String name; // 节点显示名称 private String type; // start / process / decision / end private double x, y; // 画布左上角坐标 private double width = 140; // 默认宽 private double height = 56; // 默认高 public FlowNode() { // Jackson 反序列化需要无参构造 } // getters / setters 省略 }public class FlowLine { private String id; private String fromNodeId; // 起点节点 id private String toNodeId; // 终点节点 id private String label; // “是” / “否”等分支文字 private String style; // straight / curve public FlowLine() { } // getters / setters 省略 }这里有个容易被忽略的设计点:id必须独立于内存对象存在。很多人一开始用Java对象引用去连线,程序没重启时一切正常,保存到文件再加载就全断了,所以序列化模型里id是连线关系的唯一线索,别用对象引用。FlowLine里的label字段代表条件分支语义,比如判断节点后引出的“是/否”,没有这个字段,画出来的图只是一堆线,无法表达业务判断。
2.3 包结构分层:为什么序列化和视图必须解耦
期末大作业三层分包就够了,不要堆Servlet、Controller那套东西:
com.example.flowchart ├── model # 纯Java实体类:FlowNode、FlowLine、DiagramModel ├── view # JavaFX组件:NodeView、LineView、CanvasPane ├── controller # 交互控制:MainController、SaveLoadService └── Main.java # 启动入口对应地说一下职责:view层持有JavaFX组件引用,监听鼠标事件并更新模型;model层只包含纯Java类,不依赖任何javafx包;controller层负责把模型变化映射回视图。这个分层的实际价值在保存加载时最能体现——JSON序列化和JavaFX组件完全解耦,存文件不用关心舞台上放的是圆还是矩形。
提示:期末项目里最容易扣分的不是画不出来,而是保存后加载失败。视图里用完即丢的临时状态,应全部放到model类里,不要放在JavaFX组件属性中。
3. 跑通第一个JavaFX窗口:依赖配置、入口类与可拖拽节点
3.1 JDK版本与Maven依赖:pom.xml怎么配才不会报错
这里有个非常常见的卡点:JavaFX从JDK 11开始不再随JDK一起发布,需要单独引入依赖。很多人解压项目后mvn compile报“程序包javafx.application不存在”,就是因为本地只有系统JDK,没有声明OpenJFX依赖。常见做法是在pom.xml里这样配:
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <javafx.version>17.0.2</javafx.version> </properties> <dependencies> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-controls</artifactId> <version>${javafx.version}</version> </dependency> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-fxml</artifactId> <version>${javafx.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> </dependencies>参数说明:javafx.version要和你本地的JDK大版本匹配。JDK 17配JavaFX 17.0.2是经得起验证的组合,JDK 21就配JavaFX 21;版本不对,启动时容易出现UnsupportedClassVersionError或模块化相关的异常。Jackson的2.15.2是稳定版本,不需要追最新。如果不用Maven,要手动把JavaFX的lib目录加进模块路径,启动命令还得带--module-path和--add-modules,比Maven麻烦不少,所以项目里能用Maven就用Maven。
3.2 Main入口与画布初始化
Maven搭好之后,第一步是先弹出一个空窗口。标准的JavaFX入口是继承Application类:
public class Main extends Application { @Override public void start(Stage primaryStage) { CanvasPane canvas = new CanvasPane(); // 自定义画布 canvas.setPrefSize(1280, 800); Scene scene = new Scene(canvas); primaryStage.setTitle("JavaFX流程图设计器"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }逻辑说明:launch()会拉起JavaFX运行时,最终回调start(),所有UI对象必须在start()里创建。如果你在main里直接new Pane(),再想塞进场景就会报线程错误,这是新手最容易踩的启动问题。实际项目中通常用BorderPane做整体布局:画布放center,工具栏放top。
3.3 NodeView最小实现:从零开始写一个能拖的节点
画布有了,接下来是单个节点的可视化。用一个StackPane作为节点容器,里面放一个圆角矩形和一行文字:
public class NodeView extends StackPane { private double dragOffsetX = 0; private double dragOffsetY = 0; private final FlowNode model; public NodeView(FlowNode model) { this.model = model; setPrefSize(140, 56); Rectangle rect = new Rectangle(140, 56); rect.setArcWidth(10); rect.setArcHeight(10); rect.setFill(Color.WHITE); rect.setStroke(Color.DODGERBLUE); rect.setStrokeWidth(1.5); Label nameLabel = new Label(model.getName()); nameLabel.setFont(new Font("Arial", 13)); getChildren().addAll(rect, nameLabel); setLayoutX(model.getX()); setLayoutY(model.getY()); setOnMousePressed(event -> { dragOffsetX = event.getSceneX() - getLayoutX(); dragOffsetY = event.getSceneY() - getLayoutY(); toFront(); }); setOnMouseDragged(event -> { double newX = event.getSceneX() - dragOffsetX; double newY = event.getSceneY() - dragOffsetY; setLayoutX(newX); setLayoutY(newY); model.setX(newX); model.setY(newY); }); } }逻辑说明:拖动时最关键的是偏移量计算。如果不用dragOffsetX,节点会直接跳到鼠标中心,看起来像“抓不稳”。这里按下时记录鼠标场景坐标和节点左上角的差值,拖动时用当前场景坐标减掉偏移量,得到的就是新的左上角坐标。toFront()保证节点被拖到最上层,不然容易被连线条遮挡。
参数说明:矩形尺寸140×56和布局尺寸保持一致,节点在1280×800的画布上不会显得局促。圆角弧宽10只是外观,不影响命中区域。坐标写回model.setX/newY,是为了后面保存文件时有准确数据。这里还有一个细节:用setLayoutX而不是setTranslateX,因为layoutX是布局坐标,后续做网格吸附和连线绑定时语义更清晰。
4. 连线、吸附与右键菜单:流程图交互的四个关键实现
4.1 拖拽优化与坐标同步:让连线和节点实时联动
上面3.3的版本能拖,但有个性能隐患:每次MouseDragged都把最新坐标写回model,事件频率接近JavaFX的渲染脉冲,节点数量多时主线程容易被拖慢。常见优化是拖动过程中的中间坐标只更新视图,直到鼠标松开才一次性写回model。更能省心的是把连线的坐标用绑定(Binding)联动起来,避免在拖动事件里手动重绘每一条线。
public class LineView extends Group { private final Line line = new Line(); public LineView(FlowLine model, NodeView fromView, NodeView toView) { getChildren().add(line); // 连线起点绑定到起点节点中心 line.startXProperty().bind(fromView.layoutXProperty() .add(fromView.widthProperty().divide(2))); line.startYProperty().bind(fromView.layoutYProperty() .add(fromView.heightProperty().divide(2))); // 连线终点绑定到终点节点中心 line.endXProperty().bind(toView.layoutXProperty() .add(toView.widthProperty().divide(2))); line.endYProperty().bind(toView.layoutYProperty() .add(toView.heightProperty().divide(2))); line.setStroke(Color.GRAY); line.setStrokeWidth(1.5); } }逻辑说明:这里把线的四个端点坐标全部绑定到节点属性的计算值上,节点一移动,线自动跟着走。之所以加widthProperty().divide(2),是因为layoutX是左上角坐标,而连线应该从节点中心出发。绑定是惰性更新的,比在MouseDragged里手动重算每条线要高效得多。
实际项目里,节点被删除后,这条绑定还会引用一个不存在的视图对象。处理办法是删除节点时先移除所有关联的LineView,再移除NodeView,顺序不能反。
4.2 连线绘制:直线、贝塞尔曲线与正交折线的取舍
直线最简单,但对于“判断”节点分出的“是/否”两条线,直线很容易重叠看不清。比较常见的处理是用CubicCurve画贝塞尔曲线,把两个控制点偏移一定距离:
public class CurvedLineView extends Group { private final CubicCurve curve = new CubicCurve(); public CurvedLineView(NodeView fromView, NodeView toView) { getChildren().add(curve); curve.setFill(Color.TRANSPARENT); curve.setStroke(Color.GRAY); curve.setStrokeWidth(1.5); double fromX = fromView.getLayoutX() + fromView.getPrefWidth() / 2; double fromY = fromView.getLayoutY() + fromView.getPrefHeight() / 2; double toX = toView.getLayoutX() + toView.getPrefWidth() / 2; double toY = toView.getLayoutY() + toView.getPrefHeight() / 2; curve.setStartX(fromX); curve.setStartY(fromY); curve.setEndX(toX); curve.setEndY(toY); // 控制点X各偏移40,让曲线呈水平方向的S形 curve.setControlX1(fromX + 40); curve.setControlY1(fromY); curve.setControlX2(toX - 40); curve.setControlY2(toY); } }参数说明:控制点偏移量40决定了曲线弯曲程度,40在1280宽度的画布上比较均衡,改大曲线更弯,改小接近直线。这个版本没有做坐标绑定,适合静态图;需要拖动联动的版本,把4.1的绑定逻辑同样加到这些startXProperty上即可。
如果你想把连线做成类似Visio的直角折线,Polyline并不好用,因为拐点要根据两个节点的相对位置动态生成,还要判断从哪个方向出线。期末项目里我一般建议用曲线方案,演示效果足够,代码量还少。
4.3 网格吸附:用20px对齐让画布自动整齐
流程图画久了,节点位置会越来越凌乱。给拖动加一个网格吸附是最简单有效的办法:
private double snapToGrid(double value) { return Math.round(value / 20.0) * 20.0; } setOnMouseDragged(event -> { double newX = snapToGrid(event.getSceneX() - dragOffsetX); double newY = snapToGrid(event.getSceneY() - dragOffsetY); setLayoutX(newX); setLayoutY(newY); model.setX(newX); model.setY(newY); });逻辑说明:snapToGrid用四舍五入把任意坐标对齐到20的倍数上,拖拽手感会有一点“顿挫感”,但节点在视觉上会整齐很多。网格值20是个经验参数,节点宽140、高56时用20比较协调;如果节点缩小到100以内,网格改成10更顺手。更讲究一点可以把网格值做成常量,在界面上提供开关。
4.4 右键菜单与节点删除:悬空连线怎么处理
流程图操作里,右键弹出菜单属于标配。JavaFX用ContextMenu就能实现:
ContextMenu menu = new ContextMenu(); MenuItem deleteItem = new MenuItem("删除节点"); MenuItem renameItem = new MenuItem("重命名"); menu.getItems().addAll(deleteItem, renameItem); setOnContextMenuRequested(event -> { menu.show(this, event.getScreenX(), event.getScreenY()); event.consume(); }); deleteItem.setOnAction(e -> { // 清理节点时同时清理所有关联连线 model.removeNode(getNodeId()); canvasPane.refreshAllLines(); });逻辑说明:这里两个细节值得注意。一是ContextMenu.show用的是getScreenX()/getScreenY(),屏幕坐标,如果传成鼠标在节点内部的局部坐标,菜单位置会完全错乱;二是删除节点不只是移除节点本身,还得把以它为起点或终点的连线一并清掉,否则画布上会出现指向“无处”的悬空线。refreshAllLines()典型实现是清空连线图层重画一遍,节点数量少时足够流畅。
5. 保存加载与常见问题排查:JSON序列化及五个踩坑记录
5.1 保存与加载:为什么JSON序列化比Java原生序列化更稳
期末项目里最怕的事是“图存了,但程序升级后读不回来”。Java默认的ObjectOutputStream会把整个类结构写进文件,类里加一个字段就报InvalidClassException,跨版本兼容很差。换成Jackson做JSON序列化,字段增减时用默认值兜底,文件还可读可手改:
public class DiagramModel { private List<FlowNode> nodes = new ArrayList<>(); private List<FlowLine> lines = new ArrayList<>(); public void saveToFile(String path) throws IOException { ObjectMapper mapper = new ObjectMapper(); mapper.writerWithDefaultPrettyPrinter() .writeValue(new File(path), this); } public static DiagramModel loadFromFile(String path) throws IOException { ObjectMapper mapper = new ObjectMapper(); return mapper.readValue(new File(path), DiagramModel.class); } }逻辑说明:writerWithDefaultPrettyPrinter()把JSON格式化成带缩进的多行,方便用编辑器直接查看和修改。readValue要求DiagramModel有无参构造,内部集合用默认初始化,Jackson才能完成绑定。直接把整个DiagramModel写入文件的好处是节点和连线在同一个事务里,要么都保存成功,要么都失败,不会出现只存了节点丢了连线的半截文件。
5.2 加载数据后的校验:悬空连线与孤儿节点处理
从文件加载回来的数据不能直接信任。手动改过JSON、或者删除节点时漏清理,都会出现连线引用不存在的节点。加载后做一次校验:
public void validate() { Set<String> nodeIds = nodes.stream() .map(FlowNode::getId) .collect(Collectors.toSet()); lines.removeIf(line -> !nodeIds.contains(line.getFromNodeId()) || !nodeIds.contains(line.getToNodeId())); }逻辑说明:validate把现有节点id收集成集合,然后过滤掉任何起点或终点不在集合里的连线。调用时机放在loadFromFile返回之前,保证UI层拿到的模型一定是完整的。这个方法代码很短但价值很高,期末答辩时如果评委当场修改JSON制造脏数据,程序也不至于崩溃。
5.3 五个必踩的坑:现象、原因与解决方案
坑一:运行报UnsupportedClassVersionError
现象:解压项目后在IDE里直接跑,控制台立刻报版本错误。 原因:项目编译字节码版本高于当前JDK支持级别。比如项目用的是JDK 17编译,本地环境变量配置指向的却是JDK 11。 解决:确认java -version实际版本,安装匹配的JDK并配置JAVA_HOME;或者在pom.xml里把maven.compiler.source/target改成当前JDK支持的版本后重新编译。
坑二:中文节点名乱码
现象:代码里写的节点名是中文,运行时窗口里显示成问号。 原因:源码文件编码不是UTF-8,而JavaFX默认按平台编码读取,Windows中文系统容易踩中。 解决:在pom.xml里显式加上<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>,同时把IDE的源码编码统一为UTF-8。JSON文件里的中文是原字符还是Unicode转义不影响解析,不用纠结。
坑三:拖拽时节点“跳”到鼠标位置
现象:按下节点,节点中心立刻跳到鼠标光标上,拖拽时像在抓一个很滑的物体。 原因:MouseDragged里直接用事件坐标设置布局坐标,没有保存按下时的偏移量。 解决:按3.3的方式,在setOnMousePressed里记录dragOffsetX = event.getSceneX() - getLayoutX(),拖动时用event.getSceneX() - dragOffsetX计算新坐标。
坑四:鼠标事件被节点内部控件吞掉
现象:节点拖到一半突然不动了,或者点到文字区域时事件无响应。 原因:节点内写入了TextField、ComboBox等控件,这些控件消费掉了鼠标事件,父节点的拖拽监听收不到。 解决:不需要响应用户输入的内部控件设置setMouseTransparent(true);像Label这类本身没有交互需求的组件也建议加上。不要把拖拽监听挂在内部控件上,挂到StackPane本身。
坑五:关掉窗口但Java进程不退
现象:点右上角关闭窗口,界面消失了,但任务管理器里Java进程还在。 原因:Stage关闭不等于JavaFX运行时退出,后台还有非守护线程存活。 解决:在start方法里设置primaryStage.setOnHidden(e -> Platform.exit()),并保证所有后台线程都是setDaemon(true)。期末项目里最常见的是用Thread.sleep做轮询的线程没退出。
5.4 排查速查表
| 现象 | 先查哪里 | 常见原因 |
|---|---|---|
| 启动报包javafx.application不存在 | pom.xml依赖 | JavaFX 11之后未单独引入依赖 |
| 拖拽节点连线不动 | LineView绑定代码 | 只画了静态线,没有绑定坐标属性 |
| 保存后JSON是空文件 | DiagramModel构造方法 | 缺少无参构造,Jackson反序列化失败 |
| 右键菜单弹不出来 | ContextMenu.show参数 | 使用了组件局部坐标而非屏幕坐标 |
| 窗口文字模糊 | JDK版本 | Windows高DPI下JavaFX 11以下栅格渲染问题 |
6. 期末验收前最后一步:演示路径与三个加分小功能
如果时间还有余,建议给设计器加三个小的进阶功能:撤销重做、键盘微调、导出PNG。这三个功能代码量都不大,但答辩观感完全不同。
撤销重做最简单的方式不是记录每一步操作,而是用ArrayDeque<DiagramModel>保存整张图的快照,每次操作完成后压栈。深拷贝用Jackson把当前对象序列化再反序列化,是最省事的实现:
public class HistoryManager { private final ArrayDeque<DiagramModel> undoStack = new ArrayDeque<>(); private final ArrayDeque<DiagramModel> redoStack = new ArrayDeque<>(); public void pushSnapshot(DiagramModel model) throws IOException { ObjectMapper mapper = new ObjectMapper(); DiagramModel copy = mapper.readValue( mapper.writeValueAsString(model), DiagramModel.class); undoStack.push(copy); redoStack.clear(); } }键盘微调是在画布上监听KeyEvent,按方向键让选中节点移动1个像素,按住Shift移动10个像素,评委现场看会觉得很专业。导出PNG用scene.snapshot()一行就能拿到WritableImage,再写进文件即可,注意导出前把节点选中态的边框颜色恢复成默认。
演示路径我一般这样安排:先建一个“开始→登录→验证→结束”的流程,拖一个节点说明可拖,连两条线标注“是/否”说明分支,点保存后当场把JSON文件打开给评委看,再重启程序加载回来。这一套动作完整覆盖了模型、视图、交互、持久化四个课程知识点,哪怕代码里有细节不完美,评委也会觉得你理解了整个系统结构。
我个人在演示时翻过车:当时为了省事把保存动作放在窗口关闭事件里,结果现场程序崩溃,一整张图全丢了。后来改成显式的Ctrl+S加状态栏提示,程序启动时自动尝试加载同名备份文件,相当于给崩溃留了一颗后悔药。答辩讲“数据不丢”比讲“画得多快”更打动人,这是我用一次真实的教训换来的习惯,希望帮到你。
本文还有配套的精品资源,点击获取