
简介这是基于C与Qt框架开发的一款个人通讯录管理系统完整源码主要面向正在学习Qt桌面应用开发的学生、毕业设计者以及需要快速搭建信息管理项目模板的开发者。系统围绕联系人数据的新增、编辑、删除、查找与分组管理展开界面采用Qt Designer设计的UI文件功能模块通过C类分别封装结构清晰。资源压缩包共86个文件整体大小约8.75MB其中包含25个cpp源文件和25个h头文件用于实现联系人、同学、朋友、队友、亲属等分类业务逻辑15个ui文件对应各对话框及主窗口界面布局另有txt文本文件用于存储通讯录数据记录dll动态库与exe可执行程序则便于直接运行和二次部署。代码工程文件Contacts.pro保留了完整Qt项目配置源码中可以看到登录验证、生日提醒、信息查找等实用功能模块适合参考界面设计思路和增删改查落地写法。目前已有389人学习浏览作为课设作业或Qt入门练手项目都有一定借鉴价值。 我一直觉得“基于C Qt的通讯录管理系统”这类题目是被很多人低估的练手方向。乍看它只是个最简单的桌面小应用可真要动手把它从“能添加联系人”做到“能用、不崩、还能打包发给别人”需要搞定的东西会横跨C对象设计、Qt信号槽、界面与业务解耦、数据持久化乃至发布部署一整套链路。这篇文章就以我手头这套个人通讯录管理系统源码为例把当初设计时的取舍、关键模块的实现方式、以及踩过的坑都摊开讲一遍。适合什么人看如果你是正在做C课程设计的在校生、刚入门Qt想找个综合项目练手的开发者或者想了解桌面端小型管理系统怎么设计才便于扩展这篇都应该对你有参考价值。我会把核心类的设计思路、信号槽的调用链、数据持久化的选型逻辑都讲清楚不是丢一份源码让你自己啃而是让你看完之后能自己动手再写一遍。1. 为什么我用C和Qt做了一个桌面通讯录1.1 一个“简单”课设背后藏着的完整技术链通讯录这个选题几乎是所有C课设题目里的“常青树”因为它功能边界清晰无非是联系人信息的增删改查、搜索、分组展示。但功能简单不代表实现简单。要做出一个架构合理、不靠硬堆代码来维持运转的小程序你至少要面对这些问题联系人数据用什么结构保存界面和数据怎么保持同步窗口关闭后数据存到哪里换一台机器怎么运行列表里几百上千条联系人时搜索和刷新凭什么不卡这些问题单独拎出来都不难但组合在一起就构成了一条完整的桌面应用开发链路。用纯C写控制台程序当然也能做增删改查但没人想用黑窗口管理通讯录。我选Qt是因为它同时解决了“C图形界面开发效率低”和“跨平台发布”两个核心痛点信号槽机制天然适合界面事件驱动QString、QList、QFile等封装的容器和工具类又比裸写STL舒服很多。换句话说Qt让C桌面开发从“造轮子”变成了“搭积木”。1.2 对比Tkinter、MFC、Electron之后的选择在动手之前我其实简单比较过几套方案这里直接列一个对比表技术方案界面效果开发效率发布体积适合场景Python Tkinter朴素控件少高需要解释器环境快速原型、工具型小软件MFC老派控件齐全低消息映射繁琐小维护老项目Electron美观灵活高100MB起步追求界面效果的跨端应用C Qt原生流畅控件完善中等偏高中等可用MiNGW压缩桌面原生应用、跨平台业务系统我当时的需求很明确课设/项目展示要专业代码要有C含量程序要能在Windows上独立运行最好以后还能迁到Linux。Electron虽然界面好看但本质是JavaScript项目和我练C的目标背道而驰MFC太老Python做课设容易被导师追问“C呢”。最终敲定C Qt现在回头看在工具链成熟度、学习资料完整性和后续职业面试价值上这笔投入都不亏。2. 工程目录和三个核心类写界面之前先把数据结构想明白2.1 源码目录结构设计很多人拿到一份 Qt 源码压缩包第一反应是直接打开.pro文件跑起来看效果这没错但我建议先看目录结构。我这份源码的工程组织不算复杂但也刻意做了分层方便以后加功能AddressBook/ ├── AddressBook.pro ├── src/ │ ├── main.cpp │ ├── contact.h / contact.cpp │ ├── contactmanager.h / contactmanager.cpp │ ├── mainwindow.h / mainwindow.cpp │ └── contactdialog.h / contactdialog.cpp ├── data/ │ └── contacts.json └── resources/ ├── app.ico └── style.qss把实体类、管理类、界面类分开是这套源码里最值得借鉴的一点。很多新手喜欢“万物都在MainWindow里实现”短期内运行没问题但一旦要加联系人分组、加头像字段、加导入导出MainWindow会迅速膨胀成一两千行的巨型类改一个功能牵扯全局非常痛苦。2.2 联系人实体类实体类负责描述“联系人”这个业务对象本来的样子不掺任何界面逻辑。我用一个Contact结构体搞定为了后续扩展灵活性字段给得比较全struct Contact { int id -1; QString name; // 姓名 QString phone; // 手机号 QString email; // 邮箱 QString group; // 联系人分组 QString remark; // 备注 QDateTime createTime; // 创建时间 QJsonObject toJson() const; static Contact fromJson(const QJsonObject obj); };toJson()和fromJson()是实体对象与存储格式之间的桥梁这个设计在后面做持久化时帮了大忙。我建议你在自己的项目里也给实体类补上这两个方法因为无论是存 JSON、发网络请求还是做数据库映射都离不开对象和序列化格式之间的转换。2.3 QObject派生的管理器类与业务接口管理类是这套源码里连接界面和数据的中枢它继承了QObject这意味着它能使用信号槽在未来如果界面需要实时感知数据变化比如新增联系人后列表自动刷新可以直接通过信号广播出去。class ContactManager : public QObject { Q_OBJECT public: explicit ContactManager(QObject* parent nullptr); bool loadFromFile(const QString filePath); bool saveToFile(const QString filePath) const; QListContact allContacts() const; QListContact searchContacts(const QString keyword) const; bool addContact(const Contact c); bool updateContact(const Contact c); bool removeContact(int id); Contact contactById(int id) const; private: QListContact m_contacts; QString m_filePath; };注意我把“查找联系人”和“增删改”这几个业务操作都定义在了这里而不是让 MainWindow 直接操作 QList。这样做的好处是如果哪天想从 JSON 存储换成 SQLite只需要改 ContactManager 内部实现窗口界面基本不用动。这套逻辑就是 MVC 思想里 Model 层该干的事规模再小也值得按这个思路分层。3. 用户界面落地QListView、搜索框和信号槽的通联3.1 主窗口布局与列表模型界面我采用了最常见的三段式结构顶部一个QLineEdit做关键字搜索中间左侧一个QListView展示联系人姓名列表右侧用QLabel/QLineEdit只读控件展示联系人详情底部一排按钮负责新增、编辑、删除。这里有一个关键选择QListView配QStringListModel。QListWidget也能实现列表但它是“控件 数据”耦合的写法数据一变就得手动清理条目而 View/Model 的做法把“列表怎么显示”和“列表显示什么”分开数据源更新后只需调用model-setStringList()就能整体刷新对通讯录这种经常增删的场景更省心。如果你的需求是每个列表项要显示头像和两行文字可以换成QListWidget配合自定义QListWidgetItem或者上QTableViewQSqlTableModel这条路是留给后续扩展的。3.2 搜索与选中刷新的实现细节搜索是最容易写出性能隐患的地方。我最初的版本是每敲一个字符就立刻遍历一次联系人列表然后把结果 set 回去在联系人只有几十条时毫无感觉但为了“看起来专业”我加了一批模拟测试数据到一千条编辑栏立刻出现了轻微的卡顿感。后来我做了两处优化第一把搜索逻辑放到ContactManager::searchContacts里统一处理界面只负责拿关键字和收结果第二利用Qt的QTimer做了一个 300ms 的简单防抖——用户在输入停止 300ms 后才真正发起搜索避免了每次按键都全表遍历。// mainwindow.cpp 中初始化搜索框 m_searchTimer new QTimer(this); m_searchTimer-setSingleShot(true); m_searchTimer-setInterval(300); connect(m_searchLineEdit, QLineEdit::textChanged, this, [](const QString text) { m_searchTimer-start(); // 每次文字变化只重启定时器 }); connect(m_searchTimer, QTimer::timeout, this, []() { QString keyword m_searchLineEdit-text().trimmed(); m_contacts m_manager-searchContacts(keyword); m_model.setStringList(contactNames(m_contacts)); });别看这个防抖只有几十行代码它体现的“高频触发 延迟执行”思路在搜索框、滚动加载、窗口拖动这些场景里都是通用的。3.3 增删改操作的信号槽链路通讯录的增删改走了一条非常典型的手动输入弹窗链路点击“新增/编辑”按钮 - 弹出ContactDialog模态对话框 - 用户填写表单 - 点击确定 - 校验数据 - 调用ContactManager对应接口 - 刷新列表。模态对话框的好处是天然阻塞主窗口操作用户必须在完成编辑和取消之间做选择不会出现同时开着好几个编辑框改同一个联系人的混乱局面。contactDialog-exec()返回QDialog::Accepted时再取数据这个模式看起来简单却是很多小项目稳定运行的基石。这里说一个细节编辑旧联系人和新增新联系人复用同一个对话框时我在打开前先调用contactDialog-setContact(contact)把数据回填点击保存后让对话框内部调用contactDialog-contact()返回填写结果。这样窗口类自身不关心当前是新增还是编辑模式全部委托给 Manager 层判断代码可读性提高了不少。4. 持久化选型JSON落盘SQLite留作扩容4.1 QSettings、JSON、SQLite的取舍数据到底放哪我纠结了一小段时间这里把我的思考过程放出来帮你省点试错时间QSettings写注册表或 INI 文件很容易但它天生是给“程序配置”用的存联系人这种结构化、需要遍历搜索的数据读写起来会很别扭。只适合保存“上次打开的窗口大小”这类程序设置。纯文本文件实现最简单但扩展性和健壮性太差。一旦联系人数到几百手工解析文本格式就是噩梦。JSONQt 内置QJsonDocument、QJsonObject、QJsonArray支持良好文件可读性强方便调试和手动修改非常适合中小规模结构化数据。SQLite数据量大、查询条件复杂时的正解但要引入 Qt SQL 模块需要额外处理数据库连接、建表 SQL、ORM 映射等对通讯录这种操作简单的项目来说属于“过设计”。最终第一版直接选了 JSON 文件存储理由很现实代码量小、平台兼容好联系人数据量级通常在几千条以内JSON 完全够用。SQLite 的接口我在 Manager 里预留了位置后续真要换也只动loadFromFile、saveToFile两个函数这是分层设计带给我的底气。4.2 JSON读写实现的要点JSON 读写这一块我封装了三个关键步骤大家在自己的项目里也可以照抄这个套路bool ContactManager::saveToFile(const QString filePath) const { QJsonArray array; for (const Contact c : m_contacts) { array.append(c.toJson()); } QJsonDocument doc(array); QFile file(filePath); if (!file.open(QIODevice::WriteOnly)) { return false; } file.write(doc.toJson(QJsonDocument::Indented)); // 缩进格式方便人工查错 file.close(); return true; } bool ContactManager::loadFromFile(const QString filePath) { QFile file(filePath); if (!file.exists()) return false; if (!file.open(QIODevice::ReadOnly)) return false; QJsonParseError err; QJsonDocument doc QJsonDocument::fromJson(file.readAll(), err); if (err.error ! QJsonParseError::NoError || !doc.isArray()) { return false; } m_contacts.clear(); const QJsonArray array doc.array(); for (const QJsonValue val : array) { m_contacts.append(Contact::fromJson(val.toObject())); } return true; }两点提醒一是写入时用QJsonDocument::Indented生成的 JSON 文件会带缩进文件体积略大但可读性极好排错时直接打开看字段一目了然没必要为了省那几 KB 用紧凑格式二是读取时一定检查QJsonParseError用户手动改坏了 JSON 文件至少不能让程序崩溃给个提示或者自动备份成.bak都是更稳妥的做法。我还在程序启动时加了一个“如果默认 data/contacts.json 不存在就创建空文件并写入示例联系人”的逻辑。别小看这一步很多评测者拿到源码第一反应就是跑起来看界面有两条示例数据能让他们立刻看到搜索、点击、编辑的效果比空列表的冲击力强得多。5. 打包发布的完整流程和著名的platform plugin报错5.1 windeployqt的正确打开方式项目做完还不算完要把源码工程编译出的程序发给别人用才是“能用”的最后一步。Qt 的桌面程序依赖 Qt 的 DLL 和若干插件直接裸拷 exe 到别的机器上跑十有八九起不来。我用的是 Qt 自带的windeployqt.exe部署工具具体步骤记录一下用 Release 模式编译项目生成AddressBook.exe打开 Qt 对应的命令提示符环境比如“Qt 5.15.2 (MinGW 8.1.0 64-bit)”进入 exe 所在目录执行windeployqt AddressBook.exe工具会自动分析依赖把需要的 Qt DLL、platforms/qwindows.dll、styles 插件等拷贝到 exe 同级目录把整个目录打成压缩包发给别人对方无需安装 Qt 也能运行。热词里那个经典的 “windows no qt platform plugin could be initialized? reinstalling the application”我在第一次打包时就撞上了后面单独花一节说。5.2 排查“no qt platform plugin”的完整思路这个报错几乎每个 Qt 新手都会遇到关键是很多人一看英文提示就慌了第一反应是“重新安装 Qt”或者“换版本”。其实它百分之九十的情况就是程序运行时找不到platforms目录下的qwindows.dll插件。我的排查链路是这样的你可以照着一路走先确认 exe 同级目录下有没有platforms文件夹里面是否有qwindows.dll。没有就手动新建platforms文件夹从 Qt 安装目录的plugins/platforms里拷过来。确认项目的构建目录和部署目录是否正确。用qmake/CMake在某个 build 目录编译后exe 在 debug/release 子目录里要在这个子目录执行windeployqt不要对项目源码根目录运行。开启 Qt 插件的调试日志命令行为set QT_DEBUG_PLUGINS1然后运行 exe终端会打出插件加载失败的具体原因是少了依赖 DLL 还是插件路径不对直接暴露。检查环境变量。如果之前手动设置过QT_QPA_PLATFORM_PLUGIN_PATH且指向了不存在的路径程序也会报同样的错直接删掉这个变量再试。最后才考虑 Qt 版本与编译器不匹配的问题比如用 MSVC 编译的插件搭配 MinGW 编译的程序这种组合也会加载失败。这次排查给我最大的体会是报错信息是程序的“求救信号”不要慌着重装顺着它的提示查路径、查依赖基本能在十分钟内解决八成问题。5.3 关于中文乱码和资源引入的两个补充中文乱码是 Qt 项目另一个高频问题。源码文件保存为 UTF-8 时如果编译器默认用本地代码页如 GBK解析字符串常量窗口上就会显示乱码。我现在的习惯是所有.cpp/.h文件统一 UTF-8 编码在 Pro 文件里加上QMAKE_CXXFLAGS -finput-charsetUTF-8 -fexec-charsetUTF-8MSVC 场景则用/utf-8同时源文件里中文字符串尽量用QStringLiteral(中文)包裹从机制上避开编码转换问题。另外如果想让程序图标好看一点在.pro文件里加一句RC_ICONS resources/app.icoWindows 下重新编译后 exe 就会带上自定义图标。这一步虽然是可选的但和人沟通“项目是否完整”时会是个加分项。如果你有精力继续扩展这套通讯录我个人建议的方向是联系人分组管理、CSV 导入导出以及给列表项加一个简单的头像显示。分组可以基于group字段用一个QComboBox过滤CSV 导入导出则是在ContactManager里新增两个方法不牵扯界面做完这些之后你会发现这套分层结构撑到上千行代码也依然能维持清晰。本文还有配套的精品资源点击获取