简介本资源是一份面向计算机专业本科生的数据结构课程设计实战项目基于Qt框架实现家谱管理系统适用于正在完成课程设计、期末大作业或寻求C与数据结构综合应用练习的学习者。项目完整包含可编译运行的源码工程与配套课程设计报告文档涵盖树形结构存储、家族关系查询、成员增删改查等核心功能体现链表、树、栈等典型数据结构的实际落地能力。压缩包共57个文件含14个cpp与12个h文件构成主体逻辑5个ui文件定义界面布局另有pro工程配置、debug/release构建目录及docx报告文档整体体积5.14MB结构规范便于学习者理解Qt信号槽机制与MVC分层设计思路。目前已有189人下载学习获取即得高分98分通过的完整方案含可执行exe、详细设计说明、模块化源码及清晰的目录组织助你高效复现、调试并拓展功能。1. 家谱管理系统为什么非得用 Qt——当树形结构遇上跨平台桌面 GUI 的真实需求你手头正赶着数据结构大作业老师要求“实现家谱管理”但没说清楚是写个控制台打印father-son-grandson就交差还是真要能增删查改、拖拽排序、导出 PDF、双击看族谱图很多同学卡在第一步——用什么框架把“树”和“人”真正连起来。答案很现实Qt 是当前高校数据结构课程中唯一能在 Windows/macOS/Linux 三端一键编译、自带成熟树视图QTreeView、支持自定义委托Delegate渲染关系线、且 C 原生对接链表/二叉树/图结构最顺手的 GUI 框架。它不靠 Python 脚本临时拼凑界面也不依赖 JavaFX 那种启动慢、打包重的方案它让“链表存节点”和“双击展开分支”之间只隔一层 Model/View 分离逻辑。这个压缩包里的源码报告不是炫技 Demo而是某高校数据结构课程连续三年迭代的真实作业模板从单链表存储直系血缘到用邻接表扩展旁系关联从纯文本导出到生成带缩略图的 HTML 族谱页。如果你正被“怎么把课本上的树结构变成可点可拖的窗口程序”折磨这篇就是为你写的落地笔记。2. 从零搭起家谱核心数据结构链表 树的混合建模与 Qt Model 封装家谱不是标准二叉树——一个人可以有多个子女但父母只有两个或一个还存在收养、过继等非血缘关系。硬套 AVL 或红黑树只会让代码越来越玄学。我们采用双向链表 多叉树的混合结构每个节点用结构体封装基础信息用std::vectorPerson* children存子节点指针用Person* father和Person* mother存双亲指针。这样既保留链表的灵活插入删除又支持树的递归遍历。2.1 定义 Person 结构体与家族关系约束// person.h struct Person { QString id; // 全局唯一ID如 P001 QString name; QDate birthDate; QDate deathDate; QString gender; // M/F/O QString relation; // biological, adopted, step Person* father{nullptr}; Person* mother{nullptr}; std::vectorPerson* children; // 构造函数确保父子引用双向建立 void setFather(Person* p) { if (father) father-removeChild(this); father p; if (p std::find(p-children.begin(), p-children.end(), this) p-children.end()) { p-children.push_back(this); } } void removeChild(Person* child) { auto it std::find(children.begin(), children.end(), child); if (it ! children.end()) children.erase(it); } };提示setFather()中的双向解绑逻辑是血泪经验——漏掉father-removeChild(this)会导致内存泄漏或重复添加。家谱中“解除收养关系”“更正亲子关系”等操作必须显式清理双向指针。2.2 实现 QAbstractItemModel把链表树映射成 Qt 可视化模型Qt 的QTreeView不直接操作你的Person*它只认QAbstractItemModel。我们必须重写index(),parent(),rowCount(),columnCount(),data()这五个核心函数。关键在于用QModelIndex的internalPointer字段存Person*用row字段存该人在父节点children向量中的索引。// familytreemodel.h class FamilyTreeModel : public QAbstractItemModel { Q_OBJECT public: explicit FamilyTreeModel(QObject *parent nullptr); ~FamilyTreeModel(); // 必须重写根据 row/col/parent 构造 QModelIndex QModelIndex index(int row, int column, const QModelIndex parent QModelIndex()) const override; // 必须重写根据 QModelIndex 找到其父节点 QModelIndex parent(const QModelIndex index) const override; // 必须重写返回某节点下有多少子项即 children.size() int rowCount(const QModelIndex parent QModelIndex()) const override; int columnCount(const QModelIndex parent QModelIndex()) const override; QVariant data(const QModelIndex index, int role Qt::DisplayRole) const override; private: Person* root{nullptr}; // 根节点通常为始祖 };// familytreemodel.cpp QModelIndex FamilyTreeModel::index(int row, int column, const QModelIndex parent) const { if (!hasIndex(row, column, parent)) return QModelIndex(); Person* parentNode parent.isValid() ? static_castPerson*(parent.internalPointer()) : root; if (!parentNode || row static_castint(parentNode-children.size())) return QModelIndex(); Person* child parentNode-children.at(row); return createIndex(row, column, child); // internalPointer child } QModelIndex FamilyTreeModel::parent(const QModelIndex index) const { if (!index.isValid()) return QModelIndex(); Person* child static_castPerson*(index.internalPointer()); if (!child || !child-father) // 默认以父系为树主干无父则无父节点 return QModelIndex(); Person* father child-father; // 找 father 在其父节点 children 中的 row 索引 if (!father-father) { // father 是根节点的子节点 for (int i 0; i static_castint(root-children.size()); i) { if (root-children.at(i) father) return createIndex(i, 0, father); } } return QModelIndex(); // 简化处理只支持一级根节点多级需递归查找 }参数说明createIndex(row, column, ptr)是 Qt Model 的灵魂。ptr会原样存入QModelIndex::internalPointer()后续所有data()、rowCount()调用都靠它取回原始Person*。别试图存int或QString——Qt 会把它当无效指针。2.3 初始化模型并绑定到 QTreeView在主窗口构造函数中// mainwindow.cpp MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui-setupUi(this); // 创建模型实例 model new FamilyTreeModel(this); model-setRoot(/* 传入你的始祖 Person* */); // 绑定到视图 ui-treeView-setModel(model); ui-treeView-setHeaderHidden(true); // 家谱通常不需要表头 ui-treeView-setIndentation(20); // 缩进值影响层级视觉感 }此时运行QTreeView已能显示始祖及其直系后代。但全是文字——下一节让它“活”起来。3. 让家谱“长出枝叶”自定义 Delegate 渲染关系线与状态图标默认QTreeView只显示name字符串无法体现“这是长子”“已故”“养子”等语义。Qt 提供QStyledItemDelegate允许我们接管每个单元格的绘制。我们重写paint()函数在文字左侧画小图标在右侧加关系标注。3.1 设计 Delegate 类并注册到视图// treedelegate.h class TreeDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit TreeDelegate(QObject *parent nullptr); protected: void paint(QPainter *painter, const QStyleOptionViewItem option, const QModelIndex index) const override; QSize sizeHint(const QStyleOptionViewItem option, const QModelIndex index) const override; };// treedelegate.cpp void TreeDelegate::paint(QPainter *painter, const QStyleOptionViewItem option, const QModelIndex index) const { if (!index.isValid()) return; // 获取原始 Person 数据 Person* person static_castPerson*(index.internalPointer()); if (!person) return; // 绘制背景保持默认风格 QStyleOptionViewItem opt option; initStyleOption(opt, index); QApplication::style()-drawControl(QStyle::CE_ItemViewItem, opt, painter); // 计算文字区域避开图标和状态区 QRect textRect opt.rect.adjusted(24, 2, -60, -2); // 左留24px给图标右留60px给状态 // 绘制主姓名加粗 QFont nameFont opt.font; nameFont.setBold(true); painter-setFont(nameFont); painter-drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, person-name); // 绘制状态图标已故、性别、关系类型 QIcon statusIcon; if (person-deathDate.isValid()) { statusIcon QIcon(:/icons/dead.png); // 资源文件需提前添加 } else if (person-gender M) { statusIcon QIcon(:/icons/male.png); } else if (person-gender F) { statusIcon QIcon(:/icons/female.png); } if (!statusIcon.isNull()) { QPixmap pixmap statusIcon.pixmap(16, 16); QRect iconRect(opt.rect.left() 4, opt.rect.center().y() - 8, 16, 16); painter-drawPixmap(iconRect, pixmap); } // 绘制关系标注右对齐 QString relationText; if (person-relation adopted) relationText 【养】; else if (person-relation step) relationText 【继】; if (!relationText.isEmpty()) { QFont relFont opt.font; relFont.setPointSize(relFont.pointSize() - 1); painter-setFont(relFont); painter-drawText(opt.rect.right() - 50, opt.rect.center().y() 4, relationText); } }注意initStyleOption()是关键——它把option初始化为当前项的默认样式含背景色、选中态等之后再调用drawControl()才能保持 Qt 原生风格一致。跳过这步会导致自绘区域覆盖整个背景失去选中高亮效果。3.2 注册 Delegate 并启用编辑功能// mainwindow.cpp MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { // ... 前面初始化代码 // 设置自定义 Delegate TreeDelegate* delegate new TreeDelegate(ui-treeView); ui-treeView-setItemDelegate(delegate); // 启用双击编辑姓名需重写 model 的 setData ui-treeView-setEditTriggers(QAbstractItemView::DoubleClicked); }此时双击任意姓名即可修改修改后自动触发model-setData()只要你在FamilyTreeModel::setData()中更新person-name并调用dataChanged()通知视图刷新就完成了闭环。4. 家谱操作的核心闭环增删改查与跨节点关系维护GUI 界面只是外壳家谱系统的灵魂在于“关系”的准确表达。用户点击“添加子女”不能只往childrenvector 里 push 一个空Person必须同步设置newChild-father selectedPerson并检查selectedPerson是否已有该子女防重复。本节给出四个高频操作的标准实现路径。4.1 添加子女强制校验父子双向引用// 在 FamilyTreeModel 中添加 bool FamilyTreeModel::addChild(Person* parent, const QString name) { if (!parent) return false; Person* child new Person; child-name name; child-id generateUniqueId(); // 如 P QString::number(counter) // 关键双向绑定 child-setFather(parent); // 此函数内部已处理 parent-children 插入 // 若需设置母亲同理调用 child-setMother(mother) // 通知视图parent 节点新增一行 emit layoutChanged(); // 简单粗暴适合小数据量 // 更优做法beginInsertRows(parentIndex, rowCount(parentIndex), rowCount(parentIndex)); // endInsertRows(); return true; }4.2 删除节点递归清除所有子树引用void FamilyTreeModel::removePerson(Person* person) { if (!person) return; // 1. 断开所有子节点的父引用 for (Person* child : person-children) { if (child-father person) child-father nullptr; if (child-mother person) child-mother nullptr; } // 2. 断开父节点的子引用 if (person-father) person-father-removeChild(person); if (person-mother) person-mother-removeChild(person); // 3. 递归删除子树深度优先 for (Person* child : person-children) { removePerson(child); // 先清孙子 } // 4. 最后 delete 自身 delete person; }避坑 / 常见问题 / 排查现象 1删除某人后其子女在树中消失但再次添加新子女时旧子女“复活”。原因removePerson()中只清除了childrenvector但未将child-father设为nullptr导致child仍认为自己属于被删节点QTreeView刷新时因parent()返回非法指针而崩溃或错位。解决必须在removeChild()后显式置空child-father/mother。现象 2添加子女后树视图不刷新或刷新后位置错乱。原因直接调用layoutChanged()会重绘整棵树大数据量卡顿而beginInsertRows()参数错误如parentIndex传错会导致 Qt 内部索引错乱。解决先用QModelIndex parentIndex indexFromPerson(parent)获取合法父索引再调用beginInsertRows(parentIndex, oldRowCount, oldRowCount)。现象 3双击编辑姓名后内容未保存关闭程序再打开仍是旧值。原因setData()中只改了person-name但未调用emit dataChanged(index, index)告知视图更新。Qt 不会自动监听成员变量变化。解决setData()最后必须加emit dataChanged(index, index)。现象 4搜索某人时返回多个同名结果但点击后定位到错误节点。原因indexFromPerson()实现中仅用比较Person*地址但搜索返回的是新Person*未加入树地址不同。解决搜索应返回QModelIndex而非Person*或在Person中增加id字段用id查找。4.3 查找与定位基于 ID 的 O(1) 索引加速为避免每次搜索都遍历整棵树我们维护一个QHashQString, Person* idMap// familytreemodel.h private: QHashQString, Person* idMap; // familytreemodel.cpp void FamilyTreeModel::buildIdMap(Person* node) { if (!node) return; idMap[node-id] node; for (Person* child : node-children) { buildIdMap(child); } } QModelIndex FamilyTreeModel::indexFromId(const QString id) const { Person* p idMap.value(id); if (!p) return QModelIndex(); // 找到 p 的父节点再计算 row if (!p-father) return createIndex(0, 0, p); // 根节点 Person* father p-father; for (int i 0; i static_castint(father-children.size()); i) { if (father-children.at(i) p) { QModelIndex parentIndex indexFromPerson(father); return createIndex(i, 0, p); } } return QModelIndex(); }调用indexFromId(P007)即可获得对应QModelIndex再调用ui-treeView-scrollTo(index)实现精准定位。5. 导出与持久化JSON 序列化 HTML 族谱页生成作业验收不仅要看运行效果还要交“可验证的输出”。我们提供两种导出轻量级 JSON供老师快速校验数据结构和可视化 HTML带缩略图、可浏览器打开。5.1 JSON 导出用 QJsonDocument 保证结构清晰QByteArray FamilyTreeModel::toJson() const { QJsonObject rootObj; rootObj[type] family_tree; rootObj[version] 1.0; if (root) { rootObj[root] personToJson(root); } QJsonDocument doc(rootObj); return doc.toJson(QJsonDocument::Indented); } QJsonObject FamilyTreeModel::personToJson(const Person* p) const { QJsonObject obj; obj[id] p-id; obj[name] p-name; obj[birth] p-birthDate.toString(yyyy-MM-dd); if (p-deathDate.isValid()) { obj[death] p-deathDate.toString(yyyy-MM-dd); } obj[gender] p-gender; obj[relation] p-relation; // 递归序列化子女 QJsonArray childrenArr; for (const Person* child : p-children) { childrenArr.append(personToJson(child)); } obj[children] childrenArr; // 只存 ID不存指针避免循环引用 if (p-father) obj[father_id] p-father-id; if (p-mother) obj[mother_id] p-mother-id; return obj; }保存时调用QFile file(family.json); if (file.open(QIODevice::WriteOnly)) { file.write(model-toJson()); file.close(); }5.2 HTML 族谱页用 QTextDocument 生成可打印网页QString FamilyTreeModel::toHtml() const { QTextDocument doc; QTextCursor cursor(doc); // 插入 CSS 样式 cursor.insertHtml(R( style body { font-family: Segoe UI, sans-serif; margin: 20px; } .person { display: inline-block; padding: 8px; margin: 4px; border: 1px solid #ccc; border-radius: 4px; } .deceased { background-color: #fdd; } .male { color: #006; } .female { color: #606; } /style )); // 递归生成 HTML generateHtmlForPerson(cursor, root, 0); return doc.toHtml(); } void FamilyTreeModel::generateHtmlForPerson(QTextCursor cursor, const Person* p, int depth) const { QString indent QString( ).repeated(depth); // 全角空格缩进 QString cls ; if (p-deathDate.isValid()) cls deceased; if (p-gender M) cls male; else if (p-gender F) cls female; cursor.insertHtml(QString(R(div classperson%1)).arg(cls)); cursor.insertText(indent p-name); if (p-birthDate.isValid()) { cursor.insertText( ( p-birthDate.toString(yyyy) )); } if (p-deathDate.isValid()) { cursor.insertText( — p-deathDate.toString(yyyy)); } cursor.insertHtml(/div); // 递归子女缩进1 for (const Person* child : p-children) { generateHtmlForPerson(cursor, child, depth 1); } }保存为 HTML 后用QDesktopServices::openUrl(QUrl::fromLocalFile(family.html))直接在默认浏览器打开效果接近专业族谱软件。6. 验证与调试用 QTest 模拟用户操作 内存泄漏检测技巧写完功能不等于稳定。家谱系统最怕“删着删着程序崩了”或“反复添加后内存暴涨”。我一般用两招守住底线一是用 Qt Test 写自动化用例二是用 AddressSanitizer 抓内存问题。6.1 写三个必跑的 QTest 用例// test_family.cpp #include QtTest #include familytreemodel.h class TestFamily : public QObject { Q_OBJECT private slots: void init() { model new FamilyTreeModel; root new Person{P001, 张大山}; model-setRoot(root); } void cleanup() { delete model; model nullptr; } void test_addChild_then_find() { QVERIFY(model-addChild(root, 张小山)); QModelIndex idx model-indexFromId(P002); QVERIFY(idx.isValid()); QCOMPARE(model-data(idx).toString(), QString(张小山)); } void test_removeChild_then_childrenEmpty() { Person* child new Person{P002, 张小山}; child-setFather(root); QVERIFY(root-children.size() 1); model-removePerson(child); QVERIFY(root-children.empty()); QVERIFY(child nullptr); // removePerson 内部已 delete } };运行./test_family -o report.xml,xunitxml即可生成标准测试报告集成进 CI 流程。6.2 AddressSanitizer 编译与典型泄漏模式识别在.pro文件中加入# 仅 Debug 模式启用 CONFIG(debug, debug|release) { QMAKE_CXXFLAGS -fsanitizeaddress -fno-omit-frame-pointer QMAKE_LFLAGS -fsanitizeaddress }运行程序若出现 12345ERROR: AddressSanitizer: heap-use-after-free on address 0x60200000a120说明你访问了已被delete的Person*。常见于removePerson()中先delete child再child-father-removeChild(child)setData()修改person-name后data()函数仍用旧person地址读取。我的固定动作每次delete person前加一句qDebug() DELETE person-id;配合日志确认释放顺序所有Person*成员变量声明后立即初始化为nullptrdata()中加if (!person) return QVariant();防空指针。最后说句实在话这个家谱系统我最早是帮某高校导师带课时写的教学 Demo后来发现学生交上来的作业90% 卡在“树怎么画出来”和“删了为啥还显示”。所以这份源码刻意没用高级特性如 QML、C20 概念全部用 Qt5.15 C17 最稳组合.pro里连CONFIG c17都写死了。你照着跑通再按报告文档里“扩展建议”加个“三代以内血缘图谱分析”作业分数基本就稳了。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?