资讯动态

用C++与Qt重构通讯录:从课程设计到工程实践的架构进阶

发布时间:2026/9/16 14:42:59 来源:尧图企业网站定制
简介基于C与Qt开发的通讯录管理系统课程设计源码面向计算机相关专业在校生、教师及初入Qt的开发者适合课程设计结题、期末大作业冲刺也可作为初学Qt的练手项目。压缩包共52个文件核心代码包括11个cpp与10个h源文件、4个ui界面布局文件、qrc资源文件和pro工程文件另附18个png图标素材、使用说明README、程序报告doc及界面设计细节PDF整体仅3.27MB目录结构清晰便于对照学习。目前已有199人学习下载。随包提供完整可运行的通讯录增删改查、分类管理同学/朋友/亲属及近期生日提醒等功能演示代码模块涵盖地址簿核心逻辑、主界面交互、信息表单与列表展示等报告文档详细说明大作业界面设计实现细节适合在现有代码上二次扩展如增加数据持久化或搜索增强是快速上手Qt项目实践的不错参考。代码在提交前已多次运行验证主要功能均能正常使用。1. 拿 C 和 QT 重写通讯录为什么不是在做课程设计而是在打基础通讯录管理系统大概是计算机专业里被布置次数最多的课程设计题目PB 时代用 VB 做Java 时代用 Swing 做到了现在 C 配上 QT表面上只是换了一套 GUI 框架实际上对能力的要求完全不同。这个项目最容易被低估的地方在于它同时踩中了 C 的两个核心难点——内存管理和对象生命周期又踩中了 QT 的信号槽机制、Model/View 架构、事件循环这些真正在生产环境里天天用的东西。换句话说这个“课程设计”的完成质量基本能反映一个人能不能上手真实 Qt 桌面项目。常见的做法是把数据放在 vector 或链表里用 QTableWidget 直接展示增删改查一把梭再把窗口拖一拖就算完事。这样做能跑但到了要扩展成真正的通讯录产品——比如数据量过万、需要模糊搜索、需要导出导入、需要给不同的人分配权限——再回过来改代价远比重写还大。所以这篇文章会换一个思路用 QStandardItemModel 做数据层与界面层的分离用文件做持久化把通讯录拆成“数据层、业务层、界面层”三层结构再谈怎么调搜索、排序和崩溃恢复。2. C 与 QT 的选型内存模型、对象树和模型/视图框架2.1 通讯录系统里真正涉及的内存问题C 做通讯录最直接的方案是写一个 Contact 类里面放姓名、电话、邮箱、备注这些字段然后用容器管理。问题在于很多初学者把重点放在了界面能不能弹出、按钮能不能点上而忽略了内存管理——因为 Qt 的对象树机制在一定程度上掩盖了这个问题。一个 QPushButton new 出来之后如果不指定父对象就得手动 delete如果指定了父对象父对象析构时子对象会被自动回收。这个机制在通讯录这种“用户反复打开对话框、反复增删联系人”的场景下最容易出现两种故障一种是 new 了对话框但忘了 delete每次打开都泄漏一点跑一个下午内存涨上去几百兆另一种是栈上创建了 QDialog 然后 exec() 进去对话框都退出了对象还没释放逻辑上容易写出悬垂引用。常见做法是 QDialog 用栈对象配合 exec()或者 new 出来之后设置 WA_DeleteOnClose 属性让窗口关闭时自动销毁。具体到通讯录项目里编辑联系人的对话框就适合用 WA_DeleteOnClose而主窗口适合用对象树挂载。这里的一条核心原则是谁负责创建谁就要管好释放拿不准的时候明确指定父对象让它进入 Qt 的对象树而不是依赖运气。2.2 通讯录项目为什么推荐 Model/View 而不是直接往 QTableWidget 里塞数据很多课设代码里常见的是 QTableWidget 配合 setItem() 往表格里填数据。这种写法在小数据量下完全没问题几十个联系人怎么操作都流畅。但 QTableWidget 是把数据存在表格的 item 里数据和界面是绑死的数据一变就得手动同步界面搜索、排序、筛选全得自己写逻辑反复遍历表格。QTableView 配合 QStandardItemModel 则是数据和界面分离QStandardItemModel 只负责存数据、维护行列关系QTableView 只负责把模型的内容画出来。数据变了只要发一个 dataChanged 信号视图自动刷新要做筛选用一个 QSortFilterProxyModel 插在模型和视图之间整套逻辑天然支持。通讯录这个体量的项目用 Model/View 看起来有点杀鸡用牛刀但搜索、分组、排序这些都是通讯录的基本需求用 QSortFilterProxyModel 做一套下来比在 QTableWidget 里折腾 item 要干净得多。而且这套思路直接对应着 Qt 里做数据密集型应用的通用架构学一次以后做表格、树、列表都能用。2.3 一个能跑通的最小骨架QStandardItemModel 展示联系人列表先给出一个最小可运行的代码结构把通讯录最核心的“展示联系人列表”这一件事跑通。下面的代码包含了主窗口的基本布局、模型初始化和数据填充。#include QApplication #include QMainWindow #include QTableView #include QStandardItemModel #include QHeaderView class MainWindow : public QMainWindow { public: MainWindow(QWidget* parent nullptr) : QMainWindow(parent) { QTableView* view new QTableView(this); // 创建模型4列姓名、电话、邮箱、备注 model new QStandardItemModel(0, 4, this); model-setHeaderData(0, Qt::Horizontal, 姓名); model-setHeaderData(1, Qt::Horizontal, 电话); model-setHeaderData(2, Qt::Horizontal, 邮箱); model-setHeaderData(3, Qt::Horizontal, 备注); // 添加一行测试数据 QListQStandardItem* rowItems; rowItems new QStandardItem(张三); rowItems new QStandardItem(13800138000); rowItems new QStandardItem(zhangsanexample.com); rowItems new QStandardItem(同事); model-appendRow(rowItems); // 设置表格属性 view-setModel(model); view-horizontalHeader()-setStretchLastSection(true); view-setSelectionBehavior(QAbstractItemView::SelectRows); view-setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed); setCentralWidget(view); resize(800, 500); } private: QStandardItemModel* model; }; int main(int argc, char* argv[]) { QApplication app(argc, argv); MainWindow w; w.show(); return app.exec(); }这段代码的核心逻辑分三块QStandardItemModel(0, 4, this) 创建了一个初始 0 行 4 列的模型第三个参数传 this 表示模型归主窗口管理析构时自动释放appendRow 把一组 QStandardItem 作为一整行追加到模型末尾每个 item 对应一个单元格。QTableView 本身不存储数据只是把模型里的行列内容渲染出来。setSelectionBehavior 设成 SelectRows 表示点击任意单元格都选中整行这在通讯录里比默认的选单元格要顺手得多。setEditTriggers 设成 DoubleClicked意思是双击单元格才能编辑避免误触改动数据。跑起来之后应该能看到一个带表头、有内容、能整行选中、能双击编辑的表格这就是整个通讯录系统的界面底座。3. 通讯录的核心数据结构与文件持久化方案3.1 Contact 类怎么设计才不会被后续需求推翻一个通讯录管理系统的数据字段到底要哪些很多课设用四个字段就打发了。但稍加推敲就会发现不够用通讯录里的联系人很可能有多个电话家庭电话、工作电话、手机可能有生日、公司、职务、地址可能需要分组——家人、朋友、同事、黑名单。如果一开始就做一个固定的四字段结构后面每加一个需求就要改类定义、改模型初始化、改文件读写、改界面表单改动成本成倍增长。合理做法是设计一个 Contact 结构体里面用标准容器承载变长数据:#include QString #include QStringList #include QMap struct Contact { int id -1; QString name; QStringList phones; QStringList emails; QString company; QString group; QString note; bool operator(const Contact other) const { return id other.id; // 以 id 为唯一标识 } };把电话和邮箱设成 QStringList 而不是单个 QString是给“一个人有多个联系方式”留的口子。用 int id 做唯一标识而不是用名字做键是因为名字可能重复而文件同步、编辑定位都需要一个稳定的标识。避免用 QMapQString, Contact 以姓名为键的做法——通讯录里重名的情况并不少见一旦重名后插入的数据会覆盖前面的。3.2 文件格式选 JSON 还是自定义文本读写与兼容的权衡通讯录数据要持久化最简单的方式是存文件。存文件的方式五花八门可以按行写“姓名,电话,邮箱”可以用 QSettings 存 ini也可以用 QJsonDocument 存 JSON。对通讯录这个项目来说JSON 是最合适的——原因有三个第一Qt 自带的 QJsonDocument 支持完整不引入第三方库第二JSON 的嵌套结构能表达“一个联系人有多个电话”这种一对多关系第三文件内容可读出了问题能用文本编辑器直接检查。自定义格式虽然写起来简单但解析时容易埋坑——比如电话字段里如果出现了分隔符读出来就是错乱的。JSON 天然规避了这类问题。#include QJsonArray #include QJsonDocument #include QJsonObject #include QFile bool saveContacts(const QString filePath, const QListContact contacts) { QJsonArray rootArray; for (const Contact c : contacts) { QJsonObject obj; obj[id] c.id; obj[name] c.name; obj[phones] QJsonArray::fromStringList(c.phones); obj[emails] QJsonArray::fromStringList(c.emails); obj[company] c.company; obj[group] c.group; obj[note] c.note; rootArray.append(obj); } QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return false; } file.write(QJsonDocument(rootArray).toJson(QJsonDocument::Indented)); file.close(); return true; }注意 QJsonArray::fromStringList 是专门把 QStringList 转成 QJsonArray 的快捷接口省去了手动逐条 append。写文件时用 Indented 模式生成的是带缩进、人类可读的 JSON方便调试时直接打开看内容。读取是反向操作整体读入 QJsonDocument把根节点转成数组遍历每个元素用 obj[name].toString() 之类的接口取值。3.3 文件读写里最容易翻车的三个细节文件读写看似简单实际开发中每个细节都可能变成线上问题。第一个坑是路径问题。如果直接写 relative.json程序的工作目录不同文件可能落在完全不同的地方。我一般会在程序启动时用 QCoreApplication::applicationDirPath() 拼接出可执行文件同目录下的 data/contacts.json——这样不管你从哪个目录启动程序文件位置都是确定的。第二是编码问题。Windows 下默认编码可能是 GBK但 QT 6 默认源码编码是 UTF-8直接调用 QString::fromLocal8Bit 在某些环境下会出现乱码。推荐的写法是一律以 UTF-8 作为文件编码读写时用 QFile 搭配 QTextStream 显式指定 setEncoding(QStringConverter::Utf8)。打开一个之前写好的通讯录文件如果姓名全是乱码十有八九是编码不匹配。第三是写文件的安全性问题。通讯录数据虽不值钱但用户输了一下午的联系人因为程序崩溃被清空体验是毁灭性的。稳妥的做法是先写临时文件成功后再用 QFile::remove 加 QFile::rename 覆盖旧文件。这样即使写入中途断电旧文件也还在。3.4 把模型和 Contact 列表同步起来增删改查的统一走法模型存的是界面要展示的 itemContact 列表存的是业务数据。增删改查时两者必须保持同步。常见做法是动作发生时同时改模型和列表但顺序不一样结果可能完全不同。推荐顺序是“先改数据层再刷新模型”——数据永远以 QList 为准模型只是投影。void addContact(Contact c) { c.id getNextId(); // 自动分配自增 id m_contacts.append(c); refreshModel(); } void updateContact(const Contact c) { for (int i 0; i m_contacts.size(); i) { if (m_contacts[i].id c.id) { m_contacts[i] c; break; } } refreshModel(); } void removeContact(int id) { for (int i 0; i m_contacts.size(); i) { if (m_contacts[i].id id) { m_contacts.removeAt(i); break; } } refreshModel(); } void refreshModel() { model-removeRows(0, model-rowCount()); for (const Contact c : m_contacts) { QListQStandardItem* items; items new QStandardItem(c.name); items new QStandardItem(c.phones.isEmpty() ? : c.phones.first()); items new QStandardItem(c.emails.isEmpty() ? : c.emails.first()); items new QStandardItem(c.notes); model-appendRow(items); } }refreshModel 的逻辑是“全量重建”先把模型里所有行删掉再按 m_contacts 列表重建所有行。数据量在 1 万以内时这个操作的耗时基本是毫秒级用户无感知。但如果你预期联系人会到十万级就不能这么写了——那需要只刷新变化的那一行用 model-item(row, col)-setText() 定点更新这属于后面的优化项不是课程设计阶段该优先考虑的。4. 用 QSortFilterProxyModel 做搜索、筛选和排序4.1 为什么不在 QStandardItemModel 里直接做查找通讯录系统的核心使用场景是“找人”不是“看列表”。一个用户加了 500 个联系人之后往下翻是不现实的搜索就是刚需。很多课设的做法是遍历 model 的行拿 item 里的文本做字符串匹配然后重建 model。这样能做到但有一个明显的坏处排序和筛选的逻辑和数据模型写在一起代码会越来越乱。更好的做法是利用 Qt 自带的 QSortFilterProxyModel在模型与视图之间插入一个有“过滤能力”的模型代理。#include QSortFilterProxyModel QSortFilterProxyModel* proxyModel new QSortFilterProxyModel(this); proxyModel-setSourceModel(model); proxyModel-setFilterCaseSensitivity(Qt::CaseInsensitive); proxyModel-setFilterKeyColumn(-1); // 搜索所有列 view-setModel(proxyModel); // 视图挂代理模型而不是原模型setFilterKeyColumn(-1) 表示过滤时搜索所有列的文本而不是只搜某一列——在通讯录场景里用户输入“张”可能匹配姓名里的“张”也可能匹配公司里的“张”全列搜索体验更好。setFilterCaseSensitivity 设成大小写不敏感避免用户输小写字母时搜不到大写开头的英文名。核心方法就一个调用 proxyModel-setFilterFixedString(text) 或 setFilterRegularExpression(regExp)界面自动过滤什么都不用手动刷新。4.2 动态搜索QLineEdit 的 textChanged 信号怎么和代理模型对接QLineEdit 每输入一个字符都会发 textChanged 信号把这个信号直接连到代理模型的过滤方法上搜索就是实时的。connect(searchEdit, QLineEdit::textChanged, this, [](const QString text) { if (text.isEmpty()) { proxyModel-setFilterRegularExpression(QRegularExpression()); } else { proxyModel-setFilterRegularExpression(QRegularExpression::escape(text)); } });这里有一个关键细节是 QRegularExpression::escape——用户搜索“C”时如果直接把文本传给 setFilterRegularExpression“”会被当成正则量词匹配结果完全不对。正确的做法是对用户输入做转义把用户输入当作字面量来匹配。这是通讯录搜索里最常见的隐性 bug也是面试时值得讲出来的细节。4.3 按字母分组和常用字段排序代理模型的两个进阶用法如果通讯录的联系人超过几百人按姓名首字母分组是一个非常实用的功能。QSortFilterProxyModel 本身不做分组但可以配合 QTableView 的 setRowHidden 来做或者换用 QTreeView 加 QStandardItemModel 做树形分组。对课设而言更简单可靠的是在代理模型上重写 lessThan 实现点击表头排序class ContactProxyModel : public QSortFilterProxyModel { protected: bool lessThan(const QModelIndex left, const QModelIndex right) const override { QString leftData sourceModel()-data(left).toString(); QString rightData sourceModel()-data(right).toString(); return QString::localeAwareCompare(leftData, rightData) 0; } };重写 lessThan 时用 localeAwareCompare 而不是按字节比较是为了让中文排序按拼音进行而不是按 Unicode 码点排序。QTableView 默认就能点击表头排序把 setSortingEnabled(true) 打开即可。这一套组合起来通讯录的“搜索—排序”体验就完整了。5. 界面交互细节从课设代码到可用软件的差距5.1 弹窗编辑联系人时的窗口生命周期管理编辑联系人的对话框是通讯录系统中最常打开的窗口。这里有一个常见的错误用法每次点“添加”都 new 一个 QDialog但从不 delete。虽然 Qt 的对象树可能在父窗口销毁时统一回收但如果 parent 传的是 nullptr这个窗口就会泄漏到程序退出。推荐做法有两种一是用 QDialog::exec() 配合栈对象二是 new 出来加 WA_DeleteOnClose。void MainWindow::onAddContact() { Contact c; ContactEditDialog dlg(this); dlg.setContact(c); if (dlg.exec() QDialog::Accepted) { addContact(dlg.contact()); } }把对话框创建在栈上exec() 会开启一个嵌套事件循环直到对话框关闭才返回。if 判断返回值是 Accepted 还是 Rejected决定是保存数据还是丢弃。整个过程不会泄漏。如果用户加了 100 个联系人打开关闭了 100 次编辑框内存不会有任何增长。5.2 空状态提示没有联系人时界面不能是一张白表一个在课设里很少做、但真实产品里一定会有的细节当通讯录为空时界面应该提示用户“没有联系人点击添加”而不是显示一个空白表格。实现方式是在 QTableView 上覆盖一层 QLabel当行数为 0 时让它可见有数据时隐藏代码很简单但能显著改善观感。同样值得做的细节还有删除联系人前弹确认框、编辑未保存关闭时提示等。这些不是算法难题但决定了用户愿不愿意真正使用这个系统。5.3 中文乱码和路径兼容QT 课程设计里最常被忽略的问题Qt 在 Windows 下写中文如果源码文件不是 UTF-8 编码或者 MSVC 编译时没指定 /utf-8 参数中文字符串字面量在运行时会变成乱码。Windows 用 MSVC 或 MinGWQt Creator 里一般默认已经是 UTF-8但如果拿 Visual Studio 配 Qt 插件需要手动加上 /utf-8 编译选项。判断方法很简单如果界面上的姓名、按钮文字全是乱码第一反应就查编译参数和源码编码。文件路径方面如果要把通讯录作为绿色软件拷贝到别的机器上直接写相对路径可能读不到数据文件。用 QStandardPaths 或者至少 QCoreApplication::applicationDirPath() 拼路径能让程序的健壮性上一个台阶。6. 发布与进一步扩展打包、崩溃恢复和后续优化方向6.1 用 windeployqt 打出一个免安装包课程设计要提交可运行的程序就不可能让老师在自己的电脑上装一套 Qt 环境。Qt 的发布方案是工具链自带的 windeployqtWindows和 macdeployqtmacOS它会自动把程序依赖的 Qt 动态库、插件、翻译文件全部拷贝到目标目录。在 Qt 命令行环境里执行cd build/Release windeployqt contacts.exe这条命令能扫描 exe 的导入表把需要的 Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll 以及 platform 插件如 qwindows.dll一并拷过来。常见的坑是只拷贝了 exe 和几个看着眼熟的 DLL结果在别的机器上双击没反应——大部分情况是缺了 platforms\qwindows.dll这个目录必须保持与 exe 的相对位置。打完包后最好在一台干净的机器上测试确认能跑、数据文件能读写、中文显示正常。6.2 崩溃恢复临时文件加主文件的两次写入策略桌面软件的崩溃恢复不是通讯录的必需功能但如果要做得比别人更完整最常见的做法是“双文件策略”数据写入时先写 contacts.json.tmp写完并校验后再覆盖 contacts.json。程序启动时检查临时文件是否比主文件新如果是说明上次写入可能不完整可以做一次恢复。实现成本不高但对数据安全的意义很大——课设答辩时的“程序在老师电脑上闪退但数据没丢”比任何口头描述都更能说明工程意识。6.3 这个系统还能往哪个方向扩展做完通讯录基本的增删改查、搜索、排序、文件持久化之后自然的扩展方向有三个第一个是导入导出功能支持 CSV 或 vCard 格式把联系人和安卓手机通讯录打通第二个是 Qt 国际化用 QTranslator 加载中文和英文的 .qm 文件做一个语言切换菜单这也是 Qt 框架里非常成熟的一块第三个是数据库迁移从 JSON 文件换成 SQLite用 Qt SQL 模块数据量上来之后的查询效率完全是另一档。这三个方向里随便挑一个深入下去都足够撑起更复杂项目的技术储备——这一点比课设本身的成绩重要得多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价