尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C++ Qt通讯录管理系统:从类设计到部署的完整实践

C++  Qt通讯录管理系统:从类设计到部署的完整实践 简介基于C与Qt开发的通讯录管理系统是一个完整可运行的课程设计作业主要面向计算机相关专业如计科、人工智能、通信工程、自动化、电子信息的在校学生及老师也适合作为毕业设计、课程设计或入门进阶的练手项目可直接用于代码学习与功能改造。解压后共52个文件涵盖11个C源文件、10个头文件、4个UI界面文件及Qt工程配置文件等另附运行截图、操作文档与课程设计报告压缩包整体仅3.27MB便于下载学习。目前已有199人浏览学习验证了其参考价值。代码中融入了通讯录管理、好友分组、近期生日提醒等模块界面基于Qt Designer设计结构清晰、注释明确既能帮助理解核心逻辑也能围绕界面美化或功能扩展进行二次开发是一份实用的教学与交流项目。1. 从课设到可用C Qt通讯录管理系统的设计边界把通讯录做成C课程设计控制台版本通常只需一个链表换到Qt上复杂度马上翻倍QListWidget的条目、QDataStream的持久化、QLineEdit的搜索联动、QDate的生日计算任何一环没考虑好程序就只能在截图里正常。这份基于C Qt的通讯录管理系统把mainwindow、list_item_form、recent_birth_form、info_form拆成了互相独立的文件联系人分同学、同事、朋友、亲属四类并提供头像、分类过滤和生日展示。与其说它是一个“大作业成品”不如说是一套能跑通全部生命周期的课设骨架。读者可以从README开始先编译运行再看代码适合高级语言程序设计课设、Qt入门、以及想了解C基础工程组织方式的人。2. 实体建模与扩展Person基类与同学/同事/朋友/亲属的分类实现通讯录的表单和列表再花哨数据模型没撑住后面每一步都是改bug。这个项目里person.h定义的是联系人实体的公共部分其余四个头文件各自扩展。选择继承而不是一个大结构体不是炫技是为了让“新增一种联系人”这件事只影响新增的文件不影响界面调度逻辑。2.1 Person基类先定公共字段再谈虚函数先看最小实现该有的字段。课设代码容易把字段写得特别随意phone用int分组用bool结果后期到处改。以下代码不是项目原样是简化后的可运行骨架命名和消息映射保持一致// person.h #pragma once #include QDate #include QString enum PersonType { TYPE_FRIEND 0, TYPE_CLASSMATE, TYPE_COLLEAGUE, TYPE_RELATIVE }; class Person { public: int id 0; QString name; QString phone; QString email; QString avatarPath; QDate birthday; PersonType type TYPE_FRIEND; Person() default; virtual ~Person() default; virtual QString categoryName() const { return 朋友; } };这段代码把每个联系人的公共属性收敛到一起id用于唯一标识birthday用QDate而不是三个int是为了后续生日计算可以直接调用日期运算。categoryName作为虚函数子类覆盖后可以给界面显示分类名。C 的隐藏规则在这里很容易踩坑如果子类定义了一个同名的categoryName函数但参数不同父类虚函数会被隐藏Qt的槽函数照常调用父类版本结果界面上分类永远是“朋友”。所以子类里要么用override明确覆盖要么不用同名函数。这也是热词里“c 覆盖 隐藏”在课设里最常见的出现位置。2.2 四个子类字段差异用表格说话项目里的classmate.h、colleague.h、friend.h、relative.h分别处理四类联系人。公共信息留在Person里差异信息归纳如下子类典型扩展字段分类标识默认头像ClassmatestudentId, classNameTYPE_CLASSMATEicon_classmate.pngColleaguedepartment, positionTYPE_COLLEAGUEicon_colleague.pngFriendnickname, wechatTYPE_FRIENDicon_friend.pngRelativerelation, addressTYPE_RELATIVEicon_relative.png实现时通过Person::type读取分类在info_form里根据type决定显示哪些输入框而不是根据对象类型反复dynamic_cast。常见做法是建一个createPerson函数用switch(type)返回不同的子类对象Person* createPerson(int type) { switch (type) { case TYPE_CLASSMATE: return new Classmate(); case TYPE_COLLEAGUE: return new Colleague(); case TYPE_FRIEND: return new Friend(); case TYPE_RELATIVE: return new Relative(); default: return new Friend(); } }createPerson让mainwindow的添加逻辑只有三行收集表单、创建对象、加入列表。后面要加“同事的老板”或“同学的导师”只需要改info_form和这个工厂函数。注意new出来的对象由调用方持有所有权归属要明确否则后面列表刷新时容易漏delete造成内存泄漏。2.3 序列化设计保存时只存差异字段文件读写逻辑在adressbook.cpp里。需要用QDataStream写二进制时先把Person的基础字段写入再写type最后按类型写子类字段。读取时先读type再构造具体对象避免new出基类导致数据丢失。注意二进制格式和文本格式的差别用QDataStream写入的int字段跨版本时如果有增删映射会错位建议在文件头写一个版本号。这个项目报告里有“界面设计实现细节”但没细说二进制兼容做课设答辩时能说出这条老师会认为你有生产意识。3. 界面与信号槽主窗口、列表项与搜索联动的实现Qt界面设计的核心是事件循环 信号槽。mainwindow.cpp负责整体界面list_item_form.ui定义每个联系人条目的布局info_form.ui是新增/编辑弹窗。三者通过信号连接起来。3.1 列表项自绘list_item_form如何装进QListWidgetQListWidget最常被误用的地方是直接用字符串做item然后把所有信息塞到item的text里。这个项目用list_item_form做成一个QWidget再通过ui-listWidget-setItemWidget()塞进QListWidgetItem。这样可以放头像QLabel、姓名QLabel、分类QLabel还能放多个按钮。模板如下// list_item_form.h class list_item_form : public QWidget { Q_OBJECT public: explicit list_item_form(QWidget *parent nullptr); void setPerson(Person *p); bool contains(const QString keyword) const; private: QLabel *avatarLabel; QLabel *nameLabel; QLabel *typeLabel; };setPerson里把Person的字段填到控件上。contains用来配合搜索过滤判断姓名或电话是否包含关键字。注意setItemWidget会在列表滚动时频繁调用不要在setPerson里做磁盘IO否则滚动页面会卡顿。头像路径最好在setPerson时一次加载成QPixmap缓存避免重复读文件。3.2 添加与编辑QDialog::exec()阻塞式交互新增联系人时主窗口的做法是弹出一个info_form等待用户点击保存。信号槽写法如下void MainWindow::onAddButtonClicked() { info_form dlg(this); dlg.setCurrentType(m_currentFilterType); if (dlg.exec() QDialog::Accepted) { Person *p dlg.takePerson(); addNewItem(p); saveToFile(); } }exec()进入一个新的事件循环对话框关闭后代码继续执行。这里一个容易犯的错误是在对话框内部delete了p结果takePerson回来的是野指针。我的习惯是对话框只负责创建并转移所有权谁调用了谁负责释放。关于“qt 槽函数 返回值”这个问题槽函数返回值会被connect忽略信号发射是不管槽返回值的所以不要指望通过槽函数的返回值往发射方传数据。数据传递应该通过对话框的成员变量或Q_PROPERTY完成这也是info_form里takePerson存在的意义。3.3 搜索过滤textChanged信号与QListWidgetItem隐藏给搜索框接一个textChanged信号既不用刻意处理Enter键也不需要额外的刷新请求。每一行是list_item_form里重载的contains方法主窗口遍历所有itemconnect(ui-searchEdit, QLineEdit::textChanged, this, MainWindow::filterList); void MainWindow::filterList(const QString text) { for (int i 0; i ui-listWidget-count(); i) { QListWidgetItem *item ui-listWidget-item(i); list_item_form *widget qobject_castlist_item_form *(ui-listWidget-itemWidget(item)); bool show text.isEmpty() || (widget widget-contains(text)); item-setHidden(!show); } }搜索的复杂度是O(n)联系人规模在几千以内完全够。不用引入排序模型否则为了一个搜索框去改QSortFilterProxyModel课设进度会拖累很多。需要注意setHidden会把整行隐藏而不是删除滚动条高度会自动更新如果直接delete item再点清空搜索框数据就真的没了。3.4 分类过滤与信号刷新界面左侧大概率有一个分类按钮组点击后把m_currentFilterType设置成对应值然后调用filterByType。搜索和分类两个条件叠加时可以用一个updateFilter()同时判断type和keyword而不是两个过滤器互相覆盖。一个常见方案是void MainWindow::updateFilter() { for (int i 0; i ui-listWidget-count(); i) { QListWidgetItem *item ui-listWidget-item(i); list_item_form *widget qobject_castlist_item_form *( ui-listWidget-itemWidget(item)); bool ok (m_currentFilterType TYPE_ALL || widget-person()-type m_currentFilterType); ok ok (m_keyword.isEmpty() || widget-contains(m_keyword)); item-setHidden(!ok); } }这里的widget-person()暴露了Person指针会造成外部能修改内部数据但课设范围内可接受。严谨做法是让list_item_form对外只提供只读的Person引用或者提供一个int personId()接口供外部判断分类。如果你后续要做批量操作建议改成只读接口否则过滤函数里的类型判断会越写越乱。4. 日期敏感逻辑最近生日提醒的排序与定时刷新实现通讯录管理系统的加分项在“最近生日”模块。recent_birth_form.ui负责显示recent_birth_form.cpp负责计算和刷新。生日提醒不是启动时算一次就行程序可能长期挂着所以要用QTimer周期刷新。4.1 生日距离计算跨年与2月29日的边界计算某人距离下次生日还有多少天不能直接拿当前日期减生日。正确逻辑是取今年的生日日期如果已经过了就加一年到下一年。代码如下int daysToBirthday(const QDate today, const QDate birthday) { if (!birthday.isValid()) return INT_MAX; QDate next(today.year(), birthday.month(), birthday.day()); if (next today) // 今年已过算明年 next next.addYears(1); else if (next today) return 0; return today.daysTo(next); }注意today是QDatetoday.year()返回当年birthday.month()和birthday.day()保持原样。比较时next today是QDate重载的小于运算符语义是日期早于今天。注意QDate(today.year(), 2, 29)在平年是非法日期构造时不会自动回退必须先判断再回退到 2 月 28 日。这个边界不处理程序在非闰年运行到生日逻辑时就会构造出非法QDate后续daysTo返回无意义值甚至触发“qt崩溃”里的断言失败。处理方式是在月份为2、日为29时用QDate(today.year(), 2, 28)替代。4.2 排序用std::sort而不是手写冒泡有了每个人对应的天数就可以用std::sort升序排列取前N条。手写冒泡在十几条数据上无所谓但体现不了C算法库的用法。用lambda比较两个Person的距离QVectorPerson* persons; // 从主窗口拿到的联系人 std::sort(persons.begin(), persons.end(), [today](const Person *a, const Person *b) { int da daysToBirthday(today, a-birthday); int db daysToBirthday(today, b-birthday); return da db; });排序结果直接feed给recent_birth_form的刷新接口。注意排序不能改变主列表的排序所以传入的是副本或索引数组。如果你在Qt容器里要保留原顺序用QVector 索引排序而不要直接std::sort联系人数组。4.3 QTimer定时刷新与启动时刷新QTimer的周期建议是60秒或更长因为通讯录窗口不需要秒级准点报时。连接方式m_birthdayTimer new QTimer(this); connect(m_birthdayTimer, QTimer::timeout, this, MainWindow::refreshBirthdayList); m_birthdayTimer-start(60 * 1000); refreshBirthdayList(); // 启动时先刷一次start(60 * 1000)里的单位是毫秒60秒刷新一次。不要把周期设成1秒那样每次都要遍历所有联系人做日期计算纯属浪费。真正需要秒级更新的是“离明天生日”这种倒计时通讯录场景完全没必要。不要在refreshBirthdayList里创建全套QWidget并重新插入布局那样会导致界面闪烁和内存泄漏。更好做法是只更新已有的label文字。如果列表项数量变化再动态增减。这样比每次重建整个recent_birth_form更接近Qt性能习惯。4.4 日期相关测试用例可以写一个单元测试或main函数里的自测覆盖“今天生日”“今年已过”“2月29日生人”三类情况。测试参考表测试输入预期输出today2025-05-01, birthday05-2019天today2025-05-01, birthday04-20355天下次2026年4月20日today2025-01-01, birthday2000-02-2958天按2月28日处理这类边界放在报告附录里比贴十张运行截图更值钱。老师看到你会自己设计测试用例通常会在答辩记录里写“测试意识良好”。5. 构建发布与常见踩坑Pro配置、Qt环境与部署检查最后把项目打包成能拷走演示的exe。先看pro文件里必须有的模块cpp_project_2_1.pro示例QT core gui widgets TARGET AddressBook TEMPLATE app SOURCES main.cpp mainwindow.cpp adressbook.cpp person.cpp friend.cpp classmate.cpp colleague.cpp relative.cpp info_form.cpp list_item_form.cpp recent_birth_form.cpp HEADERS mainwindow.h person.h friend.h classmate.h colleague.h relative.h info_form.h list_item_form.h recent_birth_form.h FORMS mainwindow.ui info_form.ui list_item_form.ui recent_birth_form.ui RESOURCES icon.qrc如果漏了QT widgets在Qt5上会报“找不到QApplication”错误。很多人把Qt6与Qt5混装pro文件里的变量也会踩坑比如Qt6里QtWidgets不写进core gui就会链接失败。发布时用Release构建不能用Debug版本Debug会带上巨大的调试dll。命令行操作cd build-AddressBook-Desktop_Qt_5_15_2_MinGW_32bit-Release windeployqt AddressBook.exewindeployqt会自动把Qt模块dll与平台插件platforms目录复制到exe同目录。如果目标机器缺c运行库把vcredist或对应的msvc运行库一并带上。课设演示时最好在你未安装Qt的电脑上先试一遍。Qt环境选择方面国内镜像站可以加速qt下载在线安装器选择MinGW分支更省事MSVC分支则要求本机装有Visual Studio。用VSCode配置Qt时注意“cppProject环境变量”要指向Qt安装目录并且clangd或C/C插件要能正确读取Q_OBJECT宏否则槽函数会显示语法错误。遇到unknown type name Q_OBJECT时在includePath里加上QtCore、QtWidgets目录即可。最后提供一个验证脚本的思路在同目录放一个check_bin.batecho off if not exist platforms\qwindows.dll echo platforms/qwindows.dll missing exit /b 1 echo deploy ok这个检查能快速确认程序是否会出现“qt_qpa_platform_plugin_path”那类运行时报错。如果仍然提示该路径错误优先确认 platforms\qwindows.dll 是否与 exe 同级而不是去改系统环境变量。本文还有配套的精品资源点击获取
返回列表