简介:这份资源是面向Java桌面应用开发者与课程设计学习者的JavaFX家谱管理系统完整项目源码,围绕Family-Tree核心功能展开,帮助读者理解如何用Java与JavaFX构建可视化的家族关系管理工具。压缩包共239个文件,约3.45MB,以java源码、class字节码、fxml界面布局、xml配置、sample示例及data数据文件为主,另含jar包、docx说明与md文档,覆盖从代码到界面的完整工程结构。项目通过Person与Family类建模成员信息与亲属关系,借助TreeView与自定义TreeItem呈现家谱层次,并集成SQLite与JDBC实现数据持久化,同时利用CSS与动画特效优化交互体验。目前已有1619人学习下载,适合希望掌握JavaFX界面开发、树形结构展示与轻量级数据库集成的读者参考,也可作为课程设计或二次开发的基础工程,便于在此基础上扩展搜索、导出报告等功能。
1. 从一份 Java 家谱管理系统源码说起:它到底能跑出什么
前阵子有个做企业内训的朋友找我,说想给新入职的 Java 开发练手,需求很朴素:能录入家族成员、能看血缘关系、能导出数据,最好界面别太丑。我翻了一圈,最后丢给他一个叫 Family-Tree.zip 的包。他第一反应是「这玩意儿能跑吗」,第二反应是「JavaFX 现在还有人用?」——这两个问题恰好是这份资源最值得聊的地方。
它是一套基于 Java + JavaFX 的家谱管理系统源码,核心解决的是「树形结构数据怎么可视化 + 怎么持久化」这件事。适合三类人:刚学完 Java 基础想找个完整项目练手的在校生、需要快速搭一个关系图谱 Demo 的开发者、以及想拿它当课程设计底稿的人。它不是企业级产品,但结构完整、依赖干净,拿来拆解比拿来直接用价值更大。下面我按「先跑起来、再改得动、最后避坑」的顺序,把这份源码拆开讲。
2. 环境搭建与首次运行:JDK、JavaFX 与依赖的版本对齐
2.1 为什么 JavaFX 项目最容易卡在第一步
JavaFX 从 JDK 11 开始就不再随 JDK 一起分发了,这是绝大多数人拿到源码后第一脚就踩空的原因。你javac能过,一运行就报Error: JavaFX runtime components are missing。这不是代码问题,是运行时缺模块。所以第一件事不是打开 IDE,而是确认三样东西的版本:JDK 版本、JavaFX SDK 版本、构建工具版本。
我一般建议用 JDK 17 或 JDK 21 这两个 LTS 版本,JavaFX 选对应的 17 或 21。别用 JDK 8 去配 JavaFX 21,模块系统对不上,报错信息还特别绕。构建工具优先 Maven,因为这份源码的目录结构是标准的src/main/java+src/main/resources,Maven 能直接识别,省得你手动配 classpath。
2.2 用 Maven 把 JavaFX 依赖挂上
如果源码里没有现成的pom.xml,或者你想自己重建一份,核心就是加javafx-controls和javafx-fxml两个依赖。下面是我常用的最小配置:
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <javafx.version>17.0.6</javafx.version> </properties> <dependencies> <!-- JavaFX 控件库,按钮、表格、树视图都靠它 --> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-controls</artifactId> <version>${javafx.version}</version> </dependency> <!-- FXML 解析,界面和逻辑分离时必需 --> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-fxml</artifactId> <version>${javafx.version}</version> </dependency> </dependencies>逻辑说明:javafx-controls提供了TreeView、TableView这些家谱系统必用的控件;javafx-fxml负责把.fxml文件解析成场景图。参数上,javafx.version必须和你的 JDK 大版本对齐,JDK 17 就配 17.x,别跨大版本。如果你的机器是 Apple Silicon 或 ARM 架构,Maven 会自动拉对应平台的 classifier,不用手动指定。
2.3 运行入口与模块化配置
JavaFX 项目有两种跑法:模块化(有module-info.java)和非模块化。这份源码大概率是非模块化的,因为模块化对新手不友好。非模块化的情况下,直接跑主类会报错,需要用 Maven 插件启动:
# 用 javafx-maven-plugin 启动,避免手动拼 --module-path mvn clean javafx:run如果你坚持用命令行,那就得手动指定模块路径:
java --module-path /path/to/javafx-sdk/lib \ --add-modules javafx.controls,javafx.fxml \ -cp target/classes com.example.MainApp参数说明:--module-path指向 JavaFX SDK 的 lib 目录,--add-modules声明要用哪些模块,-cp是编译输出目录。这里最容易翻车的是路径里有空格或中文,Windows 下尤其明显,建议把 SDK 放在纯英文无空格路径下。跑起来之后你应该能看到一个主窗口,左侧是树形结构,右侧是成员详情,这就说明环境通了。
3. 家谱树的数据结构:节点模型与关系存储怎么设计
3.1 用组合模式表达「人」和「关系」
家谱的本质是一棵树,但又不是标准二叉树——一个人可能有多个子女,还涉及配偶。这份源码里常见的做法是定义一个Person类,再用一个TreeNode包装它,形成组合结构。核心字段一般包括id、name、gender、birthDate、parentId、spouseId。注意parentId是自关联,这是整棵树能串起来的关键。
public class Person { private String id; private String name; private String gender; private LocalDate birthDate; private String parentId; // 指向父节点,根节点为 null private String spouseId; // 配偶,可为空 // 省略 getter/setter }逻辑说明:parentId用字符串而不是对象引用,是为了序列化方便——存 JSON 或数据库时不会出现循环引用。参数上,id建议用 UUID 而不是自增数字,因为家谱数据可能来自多个分支合并,自增 ID 会冲突。birthDate用LocalDate而不是Date,省去时区处理的麻烦。
3.2 从扁平列表构建树:一次遍历的写法
数据存的时候是扁平的List<Person>,展示的时候要变成树。常见做法是用一个Map<String, List<Person>>按parentId分组,然后递归挂载。下面这个写法我用了很多次,比双重循环快一个量级:
public TreeNode buildTree(List<Person> persons) { // 按 parentId 分组,key 为 null 的是根节点 Map<String, List<Person>> childrenMap = persons.stream() .collect(Collectors.groupingBy( p -> p.getParentId() == null ? "ROOT" : p.getParentId() )); // 先找根,再递归挂子节点 Person root = childrenMap.get("ROOT").get(0); TreeNode rootNode = new TreeNode(root); attachChildren(rootNode, childrenMap); return rootNode; } private void attachChildren(TreeNode node, Map<String, List<Person>> map) { List<Person> children = map.get(node.getPerson().getId()); if (children == null) return; for (Person child : children) { TreeNode childNode = new TreeNode(child); node.addChild(childNode); attachChildren(childNode, map); // 递归下去 } }逻辑说明:groupingBy把 O(n²) 的查找降成 O(n)。参数上,"ROOT"是个哨兵值,因为groupingBy不允许 key 为 null。递归深度取决于家族代数,一般不会超过 20 层,不用担心栈溢出。如果你的数据里有环(比如错误录入导致 A 是 B 的父、B 又是 A 的父),这段代码会死循环,所以录入校验里必须挡掉这种情况。
3.3 持久化选型:JSON 文件还是嵌入式数据库
这份源码默认多半是用 JSON 或 XML 存本地文件,优点是零配置、方便拷贝。但数据量一过几千条,每次全量读写就会卡。我的建议是:练手阶段用 JSON 够了,真要长期用就换 SQLite。下面是对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| JSON 文件 | 零依赖、可读、易备份 | 全量读写、无并发 | 演示、课程设计 |
| XML | 结构清晰、可校验 | 冗余大、解析慢 | 需要 schema 约束时 |
| SQLite | 增量读写、支持查询 | 需引入 JDBC 驱动 | 数据量上千、多人用 |
如果换 SQLite,表结构就一张person表,parent_id建索引即可。查询某人的所有后代用递归 CTE,SQLite 3.8 以上都支持。这一步不用急着做,先把树跑通再说。
4. JavaFX 界面与交互:TreeView 绑定与增删改查落地
4.1 TreeView 和 TreeItem 的绑定关系
JavaFX 里展示树用的是TreeView<T>,它的数据源是TreeItem<T>。很多人第一次用会把Person直接塞进去,结果界面上显示的是对象地址。正确做法是给TreeView设一个cellFactory,控制每个节点怎么渲染。
TreeView<Person> treeView = new TreeView<>(); treeView.setCellFactory(tv -> new TreeCell<>() { @Override protected void updateItem(Person person, boolean empty) { super.updateItem(person, empty); if (empty || person == null) { setText(null); } else { // 显示姓名 + 出生年份,比只显示姓名信息量大 setText(person.getName() + " (" + person.getBirthDate().getYear() + ")"); } } });逻辑说明:updateItem是单元格复用的回调,empty为 true 时必须清空文本,否则滚动时会出现残影——这是 JavaFX 新手最常见的玄学 bug。参数上,setText里拼什么完全自由,但别在updateItem里做耗时操作,它会被高频调用。
4.2 增删改查的事件绑定
界面上的「添加成员」「删除」「编辑」按钮,本质是修改底层List<Person>然后刷新TreeView。刷新有个坑:直接改TreeItem的子节点列表,界面不一定更新,得用ObservableList。
// 添加:先改数据源,再挂到当前选中节点下 Person newPerson = new Person(UUID.randomUUID().toString(), nameField.getText(), ...); personList.add(newPerson); TreeItem<Person> selected = treeView.getSelectionModel().getSelectedItem(); selected.getChildren().add(new TreeItem<>(newPerson)); selected.setExpanded(true); // 自动展开,否则用户以为没加上逻辑说明:getChildren()返回的是ObservableList,add 之后界面自动刷新,不用手动调 refresh。参数上,setExpanded(true)很关键,不展开的话新节点藏在折叠的父节点里,用户会以为操作失败。删除时记得同时从personList和TreeItem里移除,只删一边会导致数据不一致。
4.3 表单校验与数据一致性
录入界面必须挡掉三类脏数据:空姓名、重复 ID、循环父子关系。前两个好办,第三个要在保存前做一次祖先链检查。
// 检查把 childId 挂到 parentId 下会不会成环 private boolean wouldCreateCycle(String childId, String parentId, Map<String, Person> index) { String current = parentId; while (current != null) { if (current.equals(childId)) return true; // 祖先里出现了自己 current = index.get(current).getParentId(); } return false; }逻辑说明:从目标父节点往上追,如果追到自己,说明成环。参数上,index是id -> Person的映射,提前建好避免每次查列表。这个校验放在保存按钮的回调里,不通过就弹 Alert,别让脏数据进内存。
5. 避坑与排查:这份源码最容易翻车的五个地方
5.1 现象:启动报「JavaFX runtime components are missing」
原因:JDK 11 以后 JavaFX 被剥离,运行时找不到模块。解决:要么用mvn javafx:run,要么手动加--module-path和--add-modules。别去改JAVA_HOME指向 JDK 8,那是倒退。
5.2 现象:中文姓名显示成方块或乱码
原因:源码文件编码和 JVM 默认编码不一致,Windows 下默认 GBK,源码是 UTF-8。解决:编译时加-encoding UTF-8,运行时加-Dfile.encoding=UTF-8。Maven 里在maven-compiler-plugin配<encoding>UTF-8</encoding>一劳永逸。
5.3 现象:TreeView 滚动后节点文字错乱
原因:cellFactory里没处理empty分支,单元格复用导致旧文本残留。解决:updateItem开头判断empty || item == null就setText(null),这是硬性规矩。
5.4 现象:保存后重启数据丢失
原因:持久化只写了内存没落盘,或者写到了相对路径下,换个工作目录就找不到文件。解决:用绝对路径或System.getProperty("user.home")拼路径,写完立即 flush 并关流。别依赖deleteOnExit。
5.5 现象:删除父节点后子节点变成孤儿
原因:删除时只移了当前节点,没处理子孙。解决:删除前先递归收集所有后代 ID,一起从数据源移除,或者改成「软删除」——加个deleted标记,展示时过滤。后者更安全,后悔药好吞。
6. 二次开发与验证:把家谱数据导出成可视化图谱
跑通之后,最有价值的改动是加一个导出功能,把树转成 Graphviz 的 DOT 格式,这样能生成一张真正的血缘关系图,比 TreeView 直观得多。下面是我常用的导出方法:
public String toDot(TreeNode root) { StringBuilder sb = new StringBuilder("digraph FamilyTree {\n"); sb.append(" node [shape=box];\n"); traverse(root, sb); sb.append("}\n"); return sb.toString(); } private void traverse(TreeNode node, StringBuilder sb) { Person p = node.getPerson(); // 每个节点一行,标签用姓名 sb.append(" \"").append(p.getId()).append("\" [label=\"") .append(p.getName()).append("\"];\n"); for (TreeNode child : node.getChildren()) { // 父子之间连边 sb.append(" \"").append(p.getId()).append("\" -> \"") .append(child.getPerson().getId()).append("\";\n"); traverse(child, sb); } }逻辑说明:DOT 语法里节点用"id" [label="显示名"]定义,边用->连接。参数上,节点 ID 用 UUID 保证唯一,label 用姓名保证可读。导出后用dot -Tpng family.dot -o family.png就能出图,前提是本地装了 Graphviz。验证方法很简单:拿一个三代同堂的测试数据跑一遍,看生成的图里节点数和边数对不对——边数应该等于节点数减一,多了说明有环,少了说明有断链。
我一般还会加一个「数据自检」按钮,遍历整棵树检查三件事:有没有孤立节点(parentId 指向不存在的 ID)、有没有环、有没有重复 ID。这三项过了,数据基本就干净了。从那以后我每次拿到别人的树形结构源码,都先跑一遍自检再谈功能,省得后面 debug 到怀疑人生。希望这份拆解帮到你,源码包里的东西自己动手跑一遍,比看十篇教程都实在。
本文还有配套的精品资源,点击获取