news 2026/10/20 5:29:28

JavaFX 家谱管理开发实战:TreeView 递归渲染与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaFX 家谱管理开发实战:TreeView 递归渲染与避坑指南

简介:这是一份基于Java与JavaFX实现的家谱管理系统完整工程源码,适合Java桌面应用开发者、课程设计学习者参考。项目核心包含Person、Family等数据模型及家庭成员关系维护逻辑,界面采用JavaFX TreeView展示家谱树,并集成了SQLite数据库实现家庭成员数据的持久化存储。资源包共239个文件,以Java源码、class编译文件、FXML界面文件为主,另含jar库、图片及说明文档,整包大小约3.45MB,结构完整可直接导入开发环境学习。已有1620人学习下载,可帮助读者快速掌握JavaFX界面构建、树形数据展示与数据库读写等实战技能,并在此基础上扩展搜索、导出等功能。

1. 开局:拿 JavaFX 做家谱管理,值不值得自己写一套

过年回老家翻出一本手写家谱,几十号人谁是谁的子女、谁和谁是夫妻,全靠长辈口头指认,年轻一辈完全理不清。我当时就决定用 Java 家谱管理系统把这堆关系落到程序里,技术栈直接选 JavaFX。这套桌面应用能让你在几分钟内完成人物录入、亲属关系绑定、世系树展开,还能按姓氏、出生年份、辈分过滤统计,数据全部存在本地文件里,不需要给亲戚们搭服务器。适合想用桌面端管理家族数据、又不想碰 Web 部署的开发者,也适合拿它练 Java 集合、递归遍历和 UI 事件处理。

2. 技术选型与工程结构:为什么单机桌面项目比 Web 更顺手

2.1 技术栈全景:Java 11 + JavaFX 17 + H2

看到“Java 家谱管理系统”这个需求时,我第一反应不是做成网页挂在服务器上,而是做一个双击就能开的桌面程序。家谱数据量撑死几百上千人,单机完全够用;桌面程序免去了部署、登录、浏览器兼容这些运维成本,数据文件往 U 盘一拷就是备份。JavaFX 相比老牌 Swing,自带 TreeView 控件和类似 CSS 的样式表,家谱树展开的交互写起来干净很多。

这套工程实际使用的技术栈如下。

层面选择说明
开发语言Java 11+享受 try-with-resources、var 等语法
UI 框架JavaFX 17.x需单独引入依赖,JDK 不自带
数据库H2 2.x 文件模式整个库就是一个 .mv.db 文件
数据访问JDBC + 手写 DAO业务简单,没必要上 ORM 黑匣子
构建工具Maven 3.6+配合 javafx-maven-plugin 一条命令起服务

有一个很多新手不知道的坑:JavaFX 从 Java 11 开始不再随 JDK 分发,直接双击打包出来的 jar 会报运行时组件缺失,这是家谱系统这类桌面项目最容易翻车的地方,第 5 章专门讲排查方案。

2.2 工程目录结构:三层分包,UI 不直接碰 SQL

源码包里的目录结构长这样:

Family-Tree ├── pom.xml └── src/main ├── java │ └── com/example/familytree │ ├── App.java │ ├── controller │ │ ├── MainController.java │ │ └── PersonEditController.java │ ├── model │ │ ├── Person.java │ │ └── Relation.java │ ├── service │ │ ├── FamilyTreeService.java │ │ └── TreeBuilder.java │ └── dao │ ├── DbHelper.java │ ├── PersonDao.java │ └── RelationDao.java └── resources ├── fxml │ ├── main.fxml │ └── person_edit.fxml └── css └── style.css

这个分包是按职责切的三层结构:controller 只管接收界面事件,service 装业务逻辑,dao 做最基础的增删改查。FXML 和 CSS 放在 resources 里,方便 JavaFX 直接用相对路径加载,不需要额外配置 resource 目录映射。

我当时刻意没引入 MyBatis 或 JPA。家谱系统说到底就是两张表的增删改查,手写 JDBC 能把 SQL 完全摊开,出了问题直接看日志里的语句就能定位,比让 ORM 帮你拼 SQL 更省心。等后期要切 MySQL,只改 dao 包里的实现,service 层根本不需要动。

2.3 数据模型设计:一个人配一张关系表

家谱最核心的不是人,是关系。如果把“父亲”“母亲”直接做成 Person 表上的 father_id、mother_id 字段,遇到多配偶、收养、过继就会非常被动;后来我把关系抽成独立的 Relation 表,人物只存自己的属性,谁是谁的父母、谁和谁是配偶,全部放关系表里维护。

Person 实体代码:

public class Person { private Integer id; // 数据库自增主键 private String name; // 姓名 private String gender; // 男 / 女 private String birthDate; // 出生日期,格式 yyyy-MM-dd private String deathDate; // 逝世日期,可为空 private Integer generation; // 辈分,从 1 开始递增 private String remark; // 备注:职业、事迹、住址等 // 省略 getter / setter,构造器建议用全参 + 无参两个版本 }

generation 字段是这套系统的关键设计。它不存死数据,而是在添加成员时由 service 层计算:新人的辈分 = 其父亲或母亲的辈分 + 1。这样渲染世系树时可以直接按 generation 分组,统计辈分覆盖范围也只需要查这个字段。

Relation 表结构:

public class Relation { private Integer id; private Integer personIdA; // 关系中的甲方 private Integer personIdB; // 关系中的乙方 private String relationType; // PARENT / SPOUSE }

关系表只保留两种类型:PARENT 表示 A 是 B 的父母,SPOUSE 表示 A 与 B 是配偶。子女关系没有单独存,而是反向查询:想知道某人的所有子女,就查所有 personIdB 是他、relationType 是 PARENT 的记录。这样设计的好处是血缘关系永远有方向,渲染树时不会出现两个人互为父母这种脏数据。

3. 核心模块拆解:从添加成员到家谱树渲染的完整链路

一个家谱操作完整的链路是这样:用户在主界面点“新增人员”,弹窗收集姓名、性别、出生日期、辈分,保存进 Person 表;再选中某个人,点“绑定子女/配偶”,往 Relation 表写关系;每次保存完成后,系统从根节点出发重新构建整棵内存树,刷新左侧 TreeView。下面按这条链路拆核心代码。

3.1 人物 CRUD:保存时校验辈分和必填项

PersonDao 的保存方法是整套系统的地基,代码里做了两件事:参数校验和防误删根节点。

public int save(Person p) { if (p.getName() == null || p.getName().trim().isEmpty()) { throw new IllegalArgumentException("姓名不能为空"); } if (p.getGeneration() == null || p.getGeneration() < 1) { throw new IllegalArgumentException("辈分必须从 1 开始"); } String sql = "INSERT INTO person(name, gender, birth_date, death_date, generation, remark) " + "VALUES(?, ?, ?, ?, ?, ?)"; try (Connection conn = DbHelper.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, p.getName()); ps.setString(2, p.getGender()); ps.setString(3, p.getBirthDate()); ps.setString(4, p.getDeathDate()); ps.setInt(5, p.getGeneration()); ps.setString(6, p.getRemark()); ps.executeUpdate(); ResultSet rs = ps.getGeneratedKeys(); if (rs.next()) { return rs.getInt(1); } return -1; } catch (SQLException e) { throw new RuntimeException("保存人物失败", e); } }

这里用 PreparedStatement 拼参数,避免字符串拼接造成的 SQL 注入;RETURN_GENERATED_KEYS 让新增后能立刻拿到自增 id,因为接下来绑定关系时马上要用。IDE 里直接运行 JDBC 时,连接串里的数据库路径如果写的是相对路径“family”,实际文件会落在用户当前工作目录,容易造成“数据跑丢”的错觉,所以封装 DbHelper 时我建议把路径写成绝对路径,固定在用户主目录下。

删除人物的逻辑比新增多一道保险:根节点不能删。所谓根节点就是 family 表里 generation 为 1、且没有 PARENT 关系指向它的人。万一误删,整棵树的起点就断了,树渲染会从多个散点开始。我在 dao 层加了一个检查方法,删除前先 count 一下关系中是否还存在合法替代根。

3.2 关系绑定:配偶和子女怎么存才不乱

绑定子女的代码如下:

public void addChild(Integer parentId, Integer childId) { if (parentId.equals(childId)) { throw new IllegalArgumentException("不能把自己绑定为自己的父母"); } if (relationExists(parentId, childId, "PARENT")) { throw new IllegalArgumentException("该亲子关系已存在"); } String sql = "INSERT INTO relation(person_id_a, person_id_b, relation_type) VALUES(?, ?, 'PARENT')"; try (Connection conn = DbHelper.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, parentId); ps.setInt(2, childId); ps.executeUpdate(); // 同时修正新成员的辈分 Person child = personDao.findById(childId); Person parent = personDao.findById(parentId); int newGen = parent.getGeneration() + 1; if (child.getGeneration() == null || child.getGeneration() < newGen) { personDao.updateGeneration(childId, newGen); } } catch (SQLException e) { throw new RuntimeException("绑定亲子关系失败", e); } }

后缀上辈分那段逻辑,是防止用户先录入了孩子、后绑定父母时辈分对不上。这里用小于判断而不是直接覆盖,是怕孩子已经被绑定过更高辈分(比如领养场景),一刀切会打乱原有结构。

查询子女时就是一条反向 SQL:SELECT p.* FROM person p JOIN relation r ON p.id = r.person_id_b WHERE r.person_id_a = ? AND r.relation_type = 'PARENT' ORDER BY p.birth_date。按出生日期排序是为了让家谱树展开时兄弟姐妹从左到右按长幼排,观感更接近传统家谱的排行。配偶绑定更简单,relation_type 传 SPOUSE,唯一要防的是一个人同时绑两个活人配偶,我在 dao 里做了一个 if 判断:先检查此人是否已有 SPOUSE 关系,有就抛异常,避免二婚关系被默认覆盖。

3.3 世系树渲染:递归构建 TreeItem

JavaFX 展示家谱的核心控件是 TreeView,它吃的是 TreeItem 嵌套结构。构建树的 TreeBuilder 是整个可视化模块的心脏:

public class TreeBuilder { private final RelationDao relationDao = new RelationDao(); public TreeItem<Person> buildTree(Person root) { TreeItem<Person> node = new TreeItem<>(root); node.setExpanded(true); List<Person> children = relationDao.findChildren(root.getId()); for (Person child : children) { node.getChildren().add(buildTree(child)); } return node; } }

这是一段标准递归:每到一个节点,查它的直接子女,然后对每个子女再次调用 buildTree,直到没有子女的叶子节点返回。递归终止条件藏在 findChildren 返回空列表上,所以不会死循环,前提是关系表里没有成环的 PARENT 数据——这也是我在 addChild 里做 parentId.equals(childId) 校验的原因,成环数据会让递归栈溢出,JFram 直接崩溃。

MainController 里把树塞进界面只需要三行:

TreeItem<Person> rootItem = treeBuilder.buildTree(rootPerson); familyTree.setRoot(rootItem); familyTree.setCellFactory(new PersonCellFactory());

setCellFactory 用来定制每个节点显示的内容。默认的 TreeItem 显示的是 Person 对象的 toString,我重写了 TreeCell 让节点显示“姓名 + 生卒年”,并给不同性别配了不同文字颜色:

public class PersonCellFactory implements Callback<TreeView<Person>, TreeCell<Person>> { @Override public TreeCell<Person> call(TreeView<Person> param) { return new TreeCell<>() { @Override protected void updateItem(Person person, boolean empty) { super.updateItem(person, empty); if (empty || person == null) { setText(""); return; } String text = person.getName() + "(" + (person.getBirthDate() == null ? "?" : person.getBirthDate()) + ")"; setText(text); if ("男".equals(person.getGender())) { setStyle("-fx-text-fill: #2c3e50; -fx-font-weight: bold;"); } else { setStyle("-fx-text-fill: #c0392b;"); } } }; } }

updateItem 是 JavaFX 列表/树控件复用 cell 时的回调,它会被调用很多次,所以里面不能做耗时操作,更不能 new 数据库连接。这里只做字符串拼接和样式设置,性能没压力。样式用 -fx-text-fill 是 JavaFX 内联 CSS 的写法,和给节点起 id 再去 style.css 里统一设置是两种思路,小工程里内联更快。

3.4 搜索与统计:按姓氏和辈分过滤

主界面顶部的搜索框干两件事:按姓名模糊查询、按辈分精确过滤。实现没走 SQL 回表,而是直接在内存里的 List 上做过滤:

public List<Person> search(String keyword, Integer generation) { return allPersons.stream() .filter(p -> keyword == null || p.getName().contains(keyword)) .filter(p -> generation == null || p.getGeneration().equals(generation)) .collect(Collectors.toList()); }

搜索结果的渲染不改变原树结构,只是在 FilterableTreeItem 里动态隐藏不匹配的节点。这种方法的好处是切换关键字时不需要反复查数据库,几百条数据流式过滤的耗时几乎为零。如果家谱数据将来涨到几万条,再考虑把过滤下沉到 SQL,用 LIKE 拼接和 generation 等值查询。

统计面板的代码更简单:按性别分组计数、按辈分分组展示分布。这段逻辑直接复用 search 方法返回的 List,不需要额外查询,数据一致性有保障。

4. 本地运行:从空机器到跑起来的两条路

4.1 环境准备:JDK 与 JavaFX 的版本匹配

上手前先确认机器上装了 JDK 11 或更高版本。终端里敲java -version和mvn -v,版本号对不上后面所有命令都会失败。JavaFX 的依赖通过 Maven 从中央仓库拉取,第一次构建会下载不少 jar,网络不稳时容易超时,建议把 Maven 镜像换成国内仓库源。

JDK 版本这里有个容易踩的坑:如果用 JDK 17 跑 JavaFX 11 的旧代码,某些模块会报 IllegalAccessError,因为 JDK 强封装了内部 API。这套工程我在 pom 里锁定的 JavaFX 版本是 17.x,对应 Java 11 到 17 都能跑。直接用项目自带的 pom 配置最安全,不要手动改动依赖版本。

4.2 两条启动路径:Maven 命令与 IDE 配置

最省事的启动方式是 Maven 插件,pom 里已经配好了:

<plugin> <groupId>org.openjfx</groupId> <artifactId>javafx-maven-plugin</artifactId> <version>0.0.8</version> <configuration> <mainClass>com.example.familytree.App</mainClass> </configuration> </plugin>

命令行启动:

mvn clean javafx:run

这条命令会先编译源码,再启动 JavaFX 应用。插件默认会带上 javafx.controls 和 javafx.fxml 模块,不需要手动指定 --module-path。

如果你是 IDE 党,直接运行 App.java 会遇到 JavaFX 运行时组件缺失的报错,这是 JDK 11 之后不带 JavaFX 导致的。需要在运行配置里手动补 VM options:

--module-path /path/to/javafx-sdk-17/lib --add-modules javafx.controls,javafx.fxml

其中 --module-path 要指到你本机 JavaFX SDK 解压路径下的 lib 目录。没有单独下 SDK 的话,用 Maven 依赖里的 JavaFX 也可以,只是路径写在本地 Maven 仓库里比较长,我一般直接去仓库找 jar 所在目录拼出来。

4.3 首次使用:创建家族、添加成员、导出数据

程序第一次启动会自动在当前用户主目录下创建 familytree 文件夹,并在里面生成 H2 数据库文件 family.mv.db。建表语句写在 DbHelper 里,用CREATE TABLE IF NOT EXISTS保证重复启动不会报错。

界面操作的完整路径是:主界面点“新建家族”,填写家族名称后进入主面板;左侧是世系树,右侧是人物详情面板。添加根人物时辈分默认填 1,保存后它成为树根;再点“添加子女”,先选父亲/母亲再录入孩子信息,孩子辈分自动加 1。全部关系绑完后,点“导出 JSON”会把 Person 和 Relation 两张表的数据按嵌套结构写成一个文件:

public String exportJson() { JSONObject root = new JSONObject(); root.put("name", familyName); root.put("people", allPersons); root.put("relations", allRelations); return root.toString(); }

导出 JSON 方便备份,也更方便二次开发时被其他程序消费。如果要换机器迁移数据,直接拷贝整个 familytree 目录即可,不需要导出数据库。

5. 避坑记录:JavaFX 桌面项目最容易翻车的四个点

5.1 JavaFX runtime components are missing,窗口起不来

现象:在 IDE 里直接右键运行 App.java,控制台抛出 RuntimeException,提示缺少 JavaFX 运行时组件,程序瞬间退出。

原因:Java 11 以后 JDK 不再内置 JavaFX,Main 方法启动时加载不到 javafx.controls 等模块,自然起不来窗口。这是 JavaFX 项目最经典的启动失败。

解决:优先用mvn javafx:run启动,或者按 4.2 的 VM options 手动加载模块路径。还有一种弯路是给 pom 加一堆 classifier 的 jar 依赖,但主类入口不继承 Application 导致永远找不到窗口,所以我强烈建议直接用插件启动,别自己拼 module-path。

5.2 界面中文全部变成方块

现象:姓名、按钮文字在窗口里全是“口口口”,英文正常。

原因:运行环境的默认字体不支持中文,或者项目源码编译的时候用了系统默认编码(Windows 上是 GBK),导致字符串进数据库前就已经乱码。

解决:pom.xml 里强制声明 UTF-8 编码,同时在启动 VM options 里加-Dfile.encoding=UTF-8。再在 style.css 里给根节点设置中文字体,Windows 下用“Microsoft YaHei”,Linux 下用“Noto Sans CJK SC”。这样双保险,数据库里的中文和界面渲染的字体都不会再出问题。

5.3 新增成员后,左侧树不刷新

现象:保存新人物后数据库里有记录,但 TreeView 界面还是旧数据,重启应用才显示。

原因:TreeView 的根节点是普通对象,没有走 JavaFX 的 observable 机制。这里有个容易误解的点:我构建的 TreeItem 内部持有一个 ObservableList,但根引用本身没替换,新增的子节点虽然加到了 ObservableList 里,界面却不会自动感知“根变了”。

解决:每次增删改之后,不要只在原 stanza 上做局部修改,而是重新调用familyTree.setRoot(treeBuilder.buildTree(realRoot)),让整棵树强制重建。我一般会在 FamilyTreeService 里包装一个 refreshTree() 方法,所有写操作结束后统一调它,避免到处都是散落的刷新逻辑。

5.4 H2 数据库文件被锁,第二个窗口报“Database may be already in use”

现象:开了两个系统实例,后启动的那个弹窗报数据库被占用,或者上次程序闪退后重启报错。

原因:H2 文件模式默认会给 .mv.db 文件加文件锁,同一时间只允许一个进程独占。程序闪退时锁没释放,还可能残留 .lock.db 文件。

解决:家谱本来就是单机工具,正常情况一个实例就够了。排查时先检查任务管理器里有没有残留的 Java 进程,杀掉后再删掉目录下 .lock.db 文件即可。如果确实需要多窗口联调,把 DbHelper 的连接串改成AUTO_SERVER=TRUE,让 H2 切换成自动服务器模式,它能支持同机多客户端连接同一个小文件库。

6. 二次开发:把家谱系统改成“家族纪念册”的思路

拿到这套源码后,最常见的改造方向有两个:一是给成员详情加照片和生平时间线,二是把数据库从 H2 换到 MySQL,让多个亲戚同时在线维护。前者不需要动表结构,只要在 Person 里加一个 photoPath 字段,主界面右侧详情图片用 ImageView 加载本地文件即可;后者改动集中在 dao 包,把 DbHelper 的 JDBC 驱动换成 mysql-connector-j,建表语句去掉 H2 的IF NOT EXISTS写法,再加一个连接池,service 层和 controller 层基本不用动。

我建议的验证顺序是:先跑一遍原工程,确认主界面能开树,再改代码。改完增量后,用mvn clean test跑里面几个关键用例——我留了三个测试类,分别覆盖辈分计算、关系防重、树构建深度,专门防重构时把核心逻辑写坏。最后手工操作一遍:新增根节点、绑定配偶、绑定两个子女、导出 JSON,比对导出文件里嵌套关系是否正确。

具体改造时有一个我很推荐的技巧:把 Person 的死亡日期空值语义改成“若为空则视为健在”,然后在统计面板里多算一个数据——在世人数和平均年龄。这个功能只需要复用现成的 List ,加上 Java 8 的 ChronoUnit 算默认生日到今天的年份差,就能让静态家谱变成有生命力的家族档案。从那以后,我每接手一个桌面项目,都会先做一次“全链路操作手工过一遍”的 checklist,而不是急着写新功能;这个习惯帮我挡掉了不少深埋在自己代码里的低级错误。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 5:29:15

SSA-VMD自适应分解:麻雀算法自动优化K与alpha的完整方案

做信号处理的朋友对VMD应该都不陌生&#xff0c;但提到“自适应变分模态分解”和“麻雀优化算法”的组合&#xff0c;很多人第一反应是&#xff1a;这又是论文里的花架子吧&#xff1f;我在一套机械设备振动信号分析系统里把SSA-VMD完整跑通后&#xff0c;可以负责任地说&#…

作者头像 李华
网站建设 2026/10/20 5:29:09

Subversive连接器6.0.4:Eclipse离线安装与SVNKit/JavaHL选型

简介&#xff1a;Subversive 连接器包&#xff08;版本 6.0.4&#xff0c;构建标识 I20161211-1700&#xff09;是 Eclipse 下 SVN 插件 Subversive 的一套跨平台连接组件&#xff0c;面向依赖 Subversion 做代码管理的中高级 Java 开发者与团队&#xff0c;用于在 IDE 内完成提…

作者头像 李华
网站建设 2026/10/20 5:28:33

Android跌倒检测Demo:传感器状态机识别与TFLite优化实战

简介&#xff1a;Android端实时跌倒检测与识别Demo&#xff0c;面向移动端AI开发者或安全监护类应用开发者&#xff0c;可用于养老看护、独居老人安全监控、公共场所异常行为预警等场景&#xff0c;解决将YOLOv5人体检测能力落地到手机端的工程参考问题。资源包总计2个文件&…

作者头像 李华
网站建设 2026/10/20 5:27:46

HBase数据模型与RowKey设计:从逻辑结构到物理存储的实践指南

1. HBase数据模型&#xff1a;从整体框架谈起在继续HBase系列之前&#xff0c;先明确一个最容易让新手混淆的问题&#xff1a;HBase的数据模型更像一张“稀疏的、多维的映射表”&#xff0c;而不是传统关系型数据库中那种行列规整的二维表。它在设计上刻意牺牲了关系模型的一些…

作者头像 李华
网站建设 2026/10/20 5:27:23

MySQL内置函数实战:从字符串处理到避免索引失效

做 MySQL 这行时间久了会发现&#xff0c;最容易被忽略却又天天躲不开的&#xff0c;就是“内置函数”这四个字。新同事问我问题&#xff0c;十次里有五次不是索引方案出了问题&#xff0c;而是某个字符串拼接、日期转换或者条件判断的函数没用好&#xff0c;结果要么结果不对&…

作者头像 李华
网站建设 2026/10/20 5:26:56

本地AI工作流搭建:构建可审计、可定制的私有化大模型基础设施

1. 项目概述&#xff1a;这不是“平替”&#xff0c;而是一次本地AI工作流的主权重建你有没有算过一笔账&#xff1f;一个普通开发者&#xff0c;日常用OpenAI API做代码补全、文档摘要、技术方案初稿&#xff0c;每月稳定消耗$80–$120——这还没算上团队协作、批量处理、历史…

作者头像 李华