C++与Qt实战:构建CS架构家谱管理系统,实现亲属关系可视化

📅 2026/7/24 4:30:06
C++与Qt实战:构建CS架构家谱管理系统,实现亲属关系可视化
1. 项目概述与核心价值最近在整理家族老照片和资料发现用纸质记录或者简单的Excel表格来管理家谱信息不仅查找麻烦而且很难直观地展示复杂的亲属关系。正好手头有个C的课程设计需求就想着不如自己动手做一个带图形界面的家谱管理系统。这个项目听起来有点“复古”毕竟现在都是Web和移动App的天下但对于我们这些从C入门、对桌面应用开发有执念的程序员来说用C配合一个经典的CS客户端-服务器架构来实现既是对基本功的一次全面检验也能做出一个真正实用、能离线运行、数据自己掌控的工具。这个“基于C实现CS界面的家谱管理系统”核心目标就是解决家谱信息的数字化、可视化和结构化查询问题。它不是一个简单的通讯录而是要能处理父子、夫妻、兄弟姐妹这种复杂的网状关系并且能以树形图等直观的方式呈现出来。选择C一方面是追求执行效率在处理成百上千的家族成员数据时响应要快另一方面也是想脱离那些“一键生成”的框架从底层理解一个图形界面应用的数据流动和事件处理机制。CS架构则意味着我们可以把数据管理服务器端和界面交互客户端分开为未来可能的网络化查询比如家族成员远程维护同一份家谱留出扩展空间尽管初始版本可能将两者合一放在一个进程里。对于学习者或开发者而言这个项目麻雀虽小五脏俱全。它会涉及C面向对象设计如何建模一个家族成员、数据结构如何高效存储和遍历亲属关系网、文件I/O如何持久化保存数据、以及图形界面编程如何使用如Qt、MFC或wxWidgets等库来绘制界面和响应用户操作。完成它你不仅能收获一个对自己有意义的作品更能深刻理解一个桌面应用从设计到实现的完整闭环。下面我就把自己从零构建这个系统的思路、关键技术的选型考量、实现中的坑以及爬坑经验毫无保留地分享出来。2. 整体架构设计与技术选型在动手写第一行代码之前花时间在设计上是绝对值得的。一个清晰合理的架构能避免后期大量的重构和调试痛苦。2.1 为什么选择CS架构而非纯单机虽然最终交付物可能是一个独立的桌面程序但我依然坚持按CS客户端-服务器的思想来设计。这里的“服务器”并非一定要是远程的物理服务器它可以是一个本地后台数据管理引擎。这种分离带来了几个好处职责清晰界面层Client只负责展示和接收用户指令业务逻辑与数据操作层Server负责处理核心算法和数据持久化。这符合单一职责原则。易于维护和扩展日后如果想增加一个Web查询前端或者让多个客户端连接同一个家族数据库我们只需要替换或增强“客户端”部分核心的数据模型和业务逻辑服务器端可以复用。便于测试我们可以对数据管理层进行独立的单元测试而不需要启动整个图形界面。在初始实现中我们可以用一个FamilyTreeServer类来扮演服务器的角色它运行在同一个进程内通过内部接口与GUI客户端通信。这为未来的解耦打下了基础。2.2 核心数据模型设计如何表示一个家族这是项目的基石。一个直观但错误的想法是直接用树Tree结构。家族关系不是严格的树因为一个人有双亲两个父节点并且夫妻关系构成了横向连接。更准确的模型是一个有向图特别是多叉树与图的结合。我设计的核心类如下// Member.h - 家族成员基类 class FamilyMember { private: int m_id; // 唯一标识符 std::string m_name; std::string m_gender; // “男”、“女” std::string m_birthDate; std::string m_deathDate; // 可为空 std::string m_biography; // 生平简介 // 关系指针 FamilyMember* m_father; FamilyMember* m_mother; std::vectorFamilyMember* m_children; std::vectorFamilyMember* m_spouses; // 配偶关系考虑多次婚姻 public: // 构造函数、getter/setter省略... void addChild(FamilyMember* child); void addSpouse(FamilyMember* spouse); // 其他关系操作方法... };关键设计考量使用指针而非ID在内存中直接使用对象指针来关联关系查询效率极高O(1)。但在序列化保存到文件时需要将指针关系转换为ID的映射关系在加载时再重建。m_spouses用向量考虑到可能存在多次婚姻的情况使用std::vector来存储所有配偶指针。在查找当前配偶时可以约定向量中最后一个为当前配偶或者增加一个isCurrent的状态字段。分离数据与逻辑FamilyMember类是一个纯数据模型。所有复杂的业务逻辑如“查找所有后代”、“判断两人关系亲疏度”应放在另一个管理类中如FamilyTreeManager避免污染数据类。2.3 图形界面库选型Qt vs MFC vs wxWidgets这是C桌面开发的一个关键抉择。我最终选择了Qt原因如下跨平台Qt支持Windows、macOS、Linux。一次编写多处编译这对于想在不同系统上使用的项目很有吸引力。信号与槽机制这是Qt的核心特性提供了对象间通信的强大、安全且松耦合的方式。相比于MFC的消息映射或原始回调函数信号与槽更直观、更易维护。丰富的控件和良好的文档Qt提供了几乎所有的现代UI控件并且文档极其详尽社区活跃遇到问题容易找到解决方案。一体化解决方案Qt不仅仅有GUI模块Qt Widgets还有网络、数据库、XML、图表Qt Charts等模块。我们后续要做的树状图展示利用Qt Graphics View框架会相对轻松。当然MFC更贴近Windows原生性能可能略有优势但学习曲线陡峭且跨平台能力弱。wxWidgets也是跨平台的但在我看来其设计和工具链的成熟度略逊于Qt。对于这样一个综合性的项目Qt能显著降低开发难度。注意如果你使用的是Visual Studio并且项目严格要求必须使用MFC那么选择MFC也完全可以。只是本文的后续示例代码将基于Qt展开但核心的数据结构和算法思想是完全通用的。2.4 项目结构规划一个清晰的项目目录结构是良好开发习惯的开始。FamilyTreeSystem/ ├── CMakeLists.txt # 使用CMake构建推荐 ├── src/ │ ├── core/ # 核心数据与逻辑 │ │ ├── FamilyMember.h/cpp │ │ ├── FamilyTreeManager.h/cpp # 关系计算、查询、文件IO │ │ └── FamilyTreeServer.h/cpp # 数据服务层 │ ├── gui/ # 图形界面 │ │ ├── MainWindow.h/cpp │ │ ├── MemberDialog.h/cpp # 添加/编辑成员的对话框 │ │ ├── TreeViewWidget.h/cpp # 显示家谱树的自定义控件 │ │ └── resources.qrc # Qt资源文件图标等 │ └── main.cpp # 程序入口 ├── data/ # 存放示例数据或生成的数据文件 ├── tests/ # 单元测试 └── docs/ # 设计文档使用CMake管理项目可以方便地集成Qt并且跨平台编译体验一致。在CMakeLists.txt中你需要使用find_package(Qt6 COMPONENTS Widgets REQUIRED)来定位Qt库并将你的源代码与Qt模块链接。3. 核心模块实现详解有了设计蓝图我们就可以开始动手搭建了。我们从最核心的数据管理层开始再到图形界面的各个功能点。3.1 家族成员关系管理与算法FamilyTreeManager类是系统的大脑。它持有一个所有成员的映射表std::unordered_mapint, FamilyMember* m_members并提供各种操作方法。3.1.1 成员的添加与关系建立添加成员时必须处理关系绑定。这里有一个易错点双向绑定的一致性。bool FamilyTreeManager::addMember(const FamilyMember member) { if (m_members.find(member.id()) ! m_members.end()) { return false; // ID重复 } FamilyMember* newMember new FamilyMember(member); m_members[member.id()] newMember; // 如果指定了父亲建立父子关系 if (member.fatherId() ! -1) { FamilyMember* father findMember(member.fatherId()); if (father) { father-addChild(newMember); newMember-setFather(father); } } // 处理母亲、配偶同理... return true; }实操心得在建立关系时一定要同时在两个对象上进行操作如父亲添加孩子孩子设置父亲。并且在保存到文件时我选择保存ID引用而不是指针。加载数据后需要一个专门的rebuildRelationships()函数遍历所有成员根据存储的ID查找对应的对象指针重新建立内存中的关系网。这个过程要确保所有数据加载完毕后再执行。3.1.2 复杂关系查询算法这是家谱系统的精华。例如“查找两个人的最近共同祖先”或“计算亲属关系称谓”。查找最近共同祖先这可以转化为二叉树中两个节点的最近公共祖先LCA问题但由于一个人有双亲家谱是一个图。一个实用的方法是使用向上标记法。从A开始向上遍历其所有祖先父、祖父...将他们加入一个集合ancestorsA并记录到该祖先的代数。从B开始向上遍历遇到的第一个存在于ancestorsA集合中的祖先就是共同祖先。通过比较代数可以确定哪个是“最近”的。由于存在过继、入赘等情况算法可能需要考虑主要血缘路径这是一个可以简化的点。计算关系称谓这是一个基于规则的知识库。可以根据共同祖先的代数差和性别映射到“堂兄”、“表舅”、“外甥女”等称谓。我实现了一个RelationshipCalculator类内部有一个大的switch-case或查找表来实现这个映射逻辑。std::string RelationshipCalculator::getRelationship(const FamilyMember* from, const FamilyMember* to) { // 1. 判断是否直系血亲父子、祖孙 // 2. 判断是否配偶 // 3. 通过最近共同祖先计算旁系血亲关系和代数 // 4. 根据性别、长幼顺序等组合出称谓 // ... }避坑指南中文亲属称谓非常复杂南北方还有差异。在项目初期可以只实现最核心的几种关系如父母、子女、配偶、兄弟姐妹、祖父母保证正确性后续再扩展。不要试图一次性实现所有可能的关系这会让逻辑变得极其复杂且容易出错。3.2 图形用户界面构建使用Qt Designer可以快速拖拽出界面原型但我更喜欢纯代码构建这样对布局的理解更深也便于版本控制。3.2.1 主窗口布局主窗口采用经典的QMainWindow包含菜单栏、工具栏、状态栏和一个中心部件。中心部件使用QSplitter左侧是成员列表QTreeWidget或QListView右侧是家谱树状图展示区域一个自定义的QGraphicsView子类。// MainWindow 构造函数片段 MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // 创建菜单和动作 createActions(); createMenus(); createToolBars(); // 核心分割布局 QSplitter *splitter new QSplitter(Qt::Horizontal, this); m_memberListWidget new QListWidget; // 成员列表 m_treeView new TreeViewWidget; // 自定义的家谱树视图 splitter-addWidget(m_memberListWidget); splitter-addWidget(m_treeView); splitter-setStretchFactor(1, 3); // 右侧树视图占3份宽度 setCentralWidget(splitter); // 连接信号与槽 connect(m_memberListWidget, QListWidget::itemDoubleClicked, this, MainWindow::onMemberDoubleClicked); // ... 更多连接 }3.2.2 家谱树状图绘制这是界面部分最具挑战性也最有成就感的一环。我们需要在QGraphicsView上动态地绘制节点成员和连接线关系。自定义图元创建一个MemberNodeItem类继承自QGraphicsRectItem或QGraphicsEllipseItem。它内部持有一个FamilyMember*指针并重写paint()方法绘制姓名、生卒年月等信息。布局算法如何自动排列这些节点简单的分层布局算法比较实用选定一个根节点如家族中最年长的祖先。采用广度优先搜索BFS遍历家谱。每一代辈分的节点放在同一水平线上。同一辈分的节点根据其子树的宽度在水平方向上均匀或紧凑排列。计算每个节点的位置setPos。绘制连接线使用QGraphicsLineItem或QGraphicsPathItem在父子节点之间绘制连线。夫妻关系可以用双横线或心形符号在节点侧面连接。// 在TreeViewWidget的布局函数中 void TreeViewWidget::layoutTree(FamilyMember* root) { m_scene-clear(); // 清空场景 if (!root) return; // 计算层级和位置 std::mapint, std::vectorFamilyMember* nodesByLevel; // 按辈分分组 // ... BFS遍历填充nodesByLevel // 计算每个节点的坐标 std::mapFamilyMember*, QPointF nodePositions; float levelHeight 100.0f; float startX 0; for (auto [level, members] : nodesByLevel) { float totalWidth members.size() * 80.0f; // 假设每个节点宽80 startX -totalWidth / 2.0f; for (size_t i 0; i members.size(); i) { QPointF pos(startX i * 80.0f, level * levelHeight); nodePositions[members[i]] pos; // 创建并放置节点图元 MemberNodeItem* item new MemberNodeItem(members[i]); item-setPos(pos); m_scene-addItem(item); } } // 绘制连线 for (auto [member, pos] : nodePositions) { for (auto child : member-children()) { QPointF childPos nodePositions[child]; QGraphicsLineItem* line new QGraphicsLineItem(pos.x(), pos.y(), childPos.x(), childPos.y()); line-setPen(QPen(Qt::black, 1, Qt::SolidLine)); m_scene-addItem(line); } } }注意事项当家族成员很多时一次性绘制所有节点会导致界面卡顿。可以考虑实现懒加载或局部渲染只绘制当前视图范围内的节点或者提供一个“缩略图”模式只显示关键人物。3.3 数据持久化文件的保存与加载数据不能只存在于内存中。我们需要定义一种文件格式来保存整个家族树。JSON和XML都是可读性好的选择我推荐使用JSON因为Qt对JSON的支持非常好QJsonDocument,QJsonObject,QJsonArray。3.3.1 序列化设计定义一个序列化辅助类将FamilyTreeManager中的成员和关系转换为JSON对象。QJsonObject Serializer::toJson(const FamilyTreeManager manager) { QJsonObject rootObj; QJsonArray membersArray; for (const auto [id, member] : manager.getAllMembers()) { QJsonObject memberObj; memberObj[id] member-id(); memberObj[name] QString::fromStdString(member-name()); memberObj[gender] QString::fromStdString(member-gender()); // ... 序列化其他基本属性 // 序列化关系ID if (member-father()) memberObj[fatherId] member-father()-id(); if (member-mother()) memberObj[motherId] member-mother()-id(); QJsonArray childrenIds; for (auto child : member-children()) childrenIds.append(child-id()); memberObj[childrenIds] childrenIds; // ... 序列化配偶ID membersArray.append(memberObj); } rootObj[members] membersArray; rootObj[version] 1.0; return rootObj; }3.3.2 反序列化与关系重建加载时先创建所有FamilyMember对象此时关系指针为空放入Manager。然后再进行第二遍遍历根据ID查找对象重建父子、夫妻等关系。bool Serializer::fromJson(const QJsonObject json, FamilyTreeManager manager) { manager.clear(); // 清空现有数据 QMapint, FamilyMember* idToMemberMap; // 临时ID到对象的映射 // 第一遍创建对象填充基本信息 QJsonArray membersArray json[members].toArray(); for (const auto memberVal : membersArray) { QJsonObject memberObj memberVal.toObject(); int id memberObj[id].toInt(); // ... 读取其他属性 FamilyMember* member new FamilyMember(id, name, gender, ...); manager.addMemberRaw(member); // 一个不建立关系的方法 idToMemberMap[id] member; } // 第二遍重建关系 for (const auto memberVal : membersArray) { QJsonObject memberObj memberVal.toObject(); int id memberObj[id].toInt(); FamilyMember* member idToMemberMap.value(id, nullptr); if (!member) continue; int fatherId memberObj[fatherId].toInt(-1); if (fatherId ! -1) { member-setFather(idToMemberMap.value(fatherId, nullptr)); } // ... 重建母亲、子女、配偶关系 QJsonArray childrenIds memberObj[childrenIds].toArray(); for (const auto childIdVal : childrenIds) { int childId childIdVal.toInt(); FamilyMember* child idToMemberMap.value(childId, nullptr); if (child) member-addChild(child); } } return true; }重要提示一定要进行数据完整性校验。在加载过程中检查ID是否唯一关系ID是否指向有效对象避免出现“悬空引用”。可以在加载完成后让FamilyTreeManager运行一个validate()函数检查所有关系的双向一致性。4. 功能实现与界面交互有了核心引擎和界面框架接下来就是将它们连接起来实现具体的功能。4.1 成员信息的增删改查这是最基本的功能。通过双击列表项或点击工具栏按钮弹出一个QDialog对话框MemberDialog来编辑成员信息。添加成员对话框中的“父亲”、“母亲”字段可以使用QComboBox下拉列表动态填充当前系统中已有的成员。在提交时调用FamilyTreeManager::addMember。编辑成员加载现有数据到对话框。注意修改成员的父母可能会影响整个树的结构需要谨慎处理。一种策略是禁止直接修改父母而是通过专门的“建立关系”功能来实现。删除成员删除一个成员是高风险操作因为它会影响其配偶、子女、父母的关系。需要提供选项是仅删除此人将其子女与配偶的关系转移到其他成员这很复杂还是连带删除其所有后代强烈建议在删除前进行二次确认并实现撤销Undo功能。对于课程设计可以简化为不允许删除有关联的成员必须先解除其所有关系。4.2 家谱树的浏览与交互在自定义的TreeViewWidget中我们需要实现一些交互鼠标点击选择节点在MemberNodeItem中重写mousePressEvent发出一个自定义信号如nodeClicked(FamilyMember*)主窗口可以连接这个信号来更新右侧的详细信息面板。鼠标拖拽平移视图QGraphicsView默认支持中键拖拽平移。我们可以设置为左键拖拽setDragMode(QGraphicsView::ScrollHandDrag)。鼠标滚轮缩放重写wheelEvent修改视图的变换矩阵scale。右键菜单在视图上点击右键弹出菜单提供“以此人为根视图”、“查看详细信息”、“添加配偶/子女”等操作。4.3 高级查询与统计功能除了直观的树状图系统还应提供基于条件的查询。按姓名/生日模糊搜索在FamilyTreeManager中实现相应的搜索函数返回匹配的成员列表。在GUI中提供一个搜索框实时过滤左侧的成员列表。统计功能在FamilyTreeManager中实现int getTotalMemberCount() const; int getMaleCount() const; int getFemaleCount() const; int getLivingCount() const; // 根据死亡日期判断 std::pairFamilyMember*, int findOldestLivingMember() const; // 最年长者这些统计结果可以显示在主窗口的状态栏或一个专门的统计对话框中。4.4 数据的导入与导出为了与其他软件或格式交换数据可以增加导入/导出功能。导出为图片将当前的家谱树视图渲染成一张高分辨率的图片PNG或SVG。Qt的QGraphicsScene可以直接渲染到QImage。QImage image(scene-sceneRect().size().toSize(), QImage::Format_ARGB32); image.fill(Qt::white); QPainter painter(image); scene-render(painter); image.save(family_tree.png);导出为文本报告生成一个格式化的文本文件按辈分列出所有成员及其基本信息。导入GEDCOM格式GEDCOM是家谱学领域的通用数据交换格式。虽然解析它有点复杂但实现一个简单的GEDCOM导入器只解析INDI和FAM记录会极大地提升项目的专业性和实用性。这可以作为一项高级扩展功能。5. 开发中的常见问题与调试技巧在开发这样一个涉及界面、数据、算法的项目中踩坑是必然的。这里记录几个典型问题及其解决方法。5.1 内存管理问题C中手动管理new和delete极易出错。问题表现程序运行一段时间后崩溃或出现不可预知的行为。解决方案使用智能指针将FamilyMember*替换为std::shared_ptrFamilyMember。这样当没有任何地方引用一个成员时其内存会自动释放。FamilyTreeManager中的映射表可以改为std::unordered_mapint, std::shared_ptrFamilyMember。注意循环引用如夫妻互相持有shared_ptr会导致内存无法释放此时可以考虑使用std::weak_ptr来持有配偶指针。如果坚持使用原始指针确保所有FamilyMember对象都由FamilyTreeManager集中创建和销毁。在FamilyTreeManager的析构函数中遍历m_members并delete所有指针。确保关系指针只是观察者不拥有所有权。5.2 界面刷新与性能当家谱成员很多时每次操作都重绘整个树会导致界面卡顿。问题表现添加、删除成员或滚动、缩放时界面响应缓慢。解决方案局部更新在TreeViewWidget中如果只是修改了某个节点的颜色或文本可以只调用该节点图元的update()而不是整个场景的update。延迟布局当连续进行多次修改如批量导入时不要每次修改都触发布局计算。可以设置一个标志位在修改结束时例如在一个定时器或空闲时统一进行一次布局更新。简化绘制在MemberNodeItem::paint()中避免进行复杂的计算或绘制大量细节。可以考虑提供一个“简洁模式”只绘制姓名。5.3 数据一致性与崩溃恢复程序意外崩溃可能导致数据文件损坏或未保存。问题表现崩溃后重新打开数据丢失或文件无法读取。解决方案定时自动保存实现一个后台定时器每隔一段时间如5分钟自动保存数据到一个临时文件如family_tree.autosave.json。操作日志实现一个简单的命令模式记录每一步修改操作。不仅可以用于实现撤销/重做也可以在崩溃后通过重放日志恢复到最近的一致状态对于课程设计这个可能有点重但思想很重要。文件完整性检查在加载文件时除了校验JSON格式还要检查必填字段是否存在关系ID是否有效。对于损坏的文件可以提供“尝试修复”或“加载备份”的选项。5.4 Qt信号与槽连接错误这是Qt开发中最常见的错误之一。问题表现点击按钮没反应或者程序运行时抛出关于连接的错误。排查技巧使用QObject::connect的新语法函数指针它在编译时就能检查类型是否匹配更安全。connect(m_addAction, QAction::triggered, this, MainWindow::onAddMember);确保发送信号和接收槽的对象在连接时都已经被正确创建不是nullptr。如果使用字符串形式的旧语法SIGNAL和SLOT要确保参数类型完全一致。任何不匹配都会导致连接失败且运行时不会报错只会输出一条警告到控制台。务必在调试时留意Qt的输出信息。6. 项目构建、测试与发布6.1 使用CMake构建项目在项目根目录创建CMakeLists.txt。cmake_minimum_required(VERSION 3.16) project(FamilyTreeSystem VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 查找Qt库 find_package(Qt6 COMPONENTS Core Widgets REQUIRED) # 设置自动处理Qt的元对象系统moc set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) # 添加可执行文件 add_executable(FamilyTreeSystem src/main.cpp src/core/FamilyMember.cpp src/core/FamilyTreeManager.cpp src/core/Serializer.cpp src/gui/MainWindow.cpp src/gui/MemberDialog.cpp src/gui/TreeViewWidget.cpp ) # 包含头文件目录 target_include_directories(FamilyTreeSystem PRIVATE src/core src/gui) # 链接Qt库 target_link_libraries(FamilyTreeSystem PRIVATE Qt6::Core Qt6::Widgets) # 在Windows下设置子系统为窗口程序避免弹出控制台 if(WIN32) set_target_properties(FamilyTreeSystem PROPERTIES WIN32_EXECUTABLE TRUE) endif()然后在终端中执行mkdir build cd build cmake .. -DCMAKE_PREFIX_PATH/path/to/your/qt/installation cmake --build .6.2 编写单元测试对核心的数据逻辑进行测试至关重要。可以使用Qt自带的QTest框架或者Google Test。 为FamilyTreeManager编写测试用例例如// test_familytree.cpp #include QTest #include FamilyTreeManager.h class TestFamilyTree : public QObject { Q_OBJECT private slots: void testAddMember() { FamilyTreeManager mgr; FamilyMember member(1, 张三, 男, 1900-01-01); QVERIFY(mgr.addMember(member)); QVERIFY(mgr.findMember(1) ! nullptr); QCOMPARE(mgr.findMember(1)-name(), 张三); } void testFindRelationship() { // 构建一个简单的家庭测试关系查找 FamilyTreeManager mgr; // ... 添加父亲、母亲、孩子 // auto rel mgr.getRelationship(child, father); // QCOMPARE(rel, 父亲); } };将测试项目单独编译和运行确保每次修改核心代码后基本功能依然正常。6.3 打包与发布项目完成后你可能想分享给家人使用他们可能没有编程环境。动态链接在Windows上程序运行需要Qt的DLL文件。使用windeployqt工具可以自动收集所有依赖。# 在构建目录下假设生成的可执行文件是FamilyTreeSystem.exe windeployqt FamilyTreeSystem.exe这会在可执行文件旁创建一个包含所有必要DLL和资源的文件夹。创建安装包使用如Inno Setup或NSIS等工具将整个文件夹打包成一个安装程序.exe方便分发。静态编译更高级的做法是静态编译Qt库这样生成的是一个独立的、无依赖的可执行文件但文件体积会大很多且需要遵循Qt的静态库许可协议。开发这样一个家谱管理系统从设计到实现是一个不断遇到问题、解决问题的过程。它强迫你去思考如何用代码精确地建模现实世界中复杂的人际关系如何设计高效的数据结构和算法以及如何构建一个用户友好的图形界面。当看到自己家族的脉络第一次清晰地呈现在屏幕上时那种成就感是无可比拟的。这个项目不仅巩固了C和Qt的技能更让你拥有了一个可以持续维护、为自己和家人服务的实用工具。如果在实现过程中对某些细节有疑问比如如何优化大规模数据的布局算法或者想为节点添加照片那都是值得进一步探索的有趣方向。