
简介面向学生信息管理场景的C毕业设计项目采用Qt框架构建图形界面使用MySQL数据库存储数据适合计算机类专业学生用于课程设计、毕业设计也适合正在学习Qt与数据库开发的初学者。系统围绕学生、班级、课程、成绩等核心信息提供录入、修改、删除、查询功能并加入成绩统计、表格导出、密码重置等模块从源码结构可看出涵盖成绩管理、选课管理、学生信息管理、班级信息管理、课程管理等多个子模块。压缩包共一百零五个文件包含二十六个C源文件、二十六个头文件、二十三个Qt界面文件、二十三张界面截图以及项目配置、数据库建表脚本、Word与Markdown文档和PPT介绍等容量约十四点七五兆目录组织清晰便于按模块查阅和二次开发。当前已有约四百九十四人学习下载可作为同类系统开发的直接参考。内含可运行源码、数据库初始化脚本、界面设计素材与配套文档既能对照界面理解代码也能根据需要扩展成绩统计、导出等功能有助于提升C工程实践和数据库应用能力。1. 为什么这次“学生信息管理系统”选择QtMySQL组合一次把界面、数据、交付全打通“C程序设计实践项目——学生信息管理系统基于QtMySQL.zip”这个标题拆开看是三个问题C 的面向对象设计、Qt 的界面框架、MySQL 的关系型数据访问。很多人前两关都顺利最后卡在最容易被小看的“Qt 怎么连 MySQL”上。最常见的局面是界面画好了按钮也响应了一打开数据库连接就报QMYSQL driver not loaded或者中文写进去变成问号。这篇文章按“先连上、再建表、后做界面、最后交付”的顺序把流程讲完整适合正在做 C 课程实践、毕业设计或者想接手类似老项目的读者。2. Qt连接MySQL的完整配置驱动、连接名与三个自检点2.1 第一步先确认QMYSQL驱动不是Qt装上就能用Qt 本身不内置 MySQL 客户端它通过插件机制访问数据库。你在QSqlDatabase::addDatabase(QMYSQL)里填的那个驱动名对应的是安装目录plugins/sqldrivers下的一个动态库。如果这个动态库不存在程序运行时会报QMYSQL driver not loaded而不是“用户名密码错误”——这个区别经常让新手误判成账号问题。拿到项目后先不要急着写业务代码用一段最小程序把所有可用驱动打印出来#include QCoreApplication #include QSqlDatabase #include QDebug int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); // drivers() 返回当前 Qt 环境能识别的全部 SQL 驱动名 qDebug() available SQL drivers: QSqlDatabase::drivers(); return 0; }运行后如果输出里没有QMYSQL说明插件缺失或没被正确加载。Qt6 之后 MySQL 驱动往往需要单独安装或自己编译Qt5 时代则是携带插件但依赖系统里的 MySQL 客户端库。两种情况的处理路径完全不同所以这一步排查优先级最高。另一个容易踩的点是 Qt 位数和 MySQL 客户端库不一致。我用的是 64 位 Qt但系统里装的是 32 位 MySQL 组件插件加载时一样会失败报错却仍然是 driver not loaded。检查plugins/sqldrivers目录里是否有qsqlmysql相关文件同时确认同目录或 PATH 里有对应位数的 MySQL 动态库这是最稳妥的做法。2.2 连接参数与字符集连接名写错会报重复连接确认驱动存在之后第二步就是写连接。下面这段初始化代码我一般会单独放在一个db.cpp文件里供整个项目复用。注意addDatabase的第二个参数连接名这是很多人会忽略的坑。#include QSqlDatabase #include QSqlQuery #include QSqlError #include QDebug QSqlDatabase initDb() { // 第二个参数 student_main 是连接名全局唯一 QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL, student_main); db.setHostName(127.0.0.1); db.setPort(3306); db.setDatabaseName(student_db); db.setUserName(root); db.setPassword(change_me); // 实际项目不要硬编码密码 if (!db.open()) { qDebug() open failed: db.lastError().text(); return QSqlDatabase(); } // 连接后立刻设置字符集与建库编码保持一致 QSqlQuery query(db); query.exec(SET NAMES utf8mb4); return db; }这里setHostName和setPort分开写是为了排查时能一眼看到目标地址。如果 MySQL 跑在同一台机器127.0.0.1通常没问题如果数据库在另一台机器3306端口要在防火墙里放行。setDatabaseName填的是库名不是表名写成表名会报Unknown database。连接名的作用是区分同一程序里的多条数据库连接。如果你在登录窗口和主窗口各调一次addDatabase(QMYSQL, student_main)第二次不会新建连接而是拿到同一个实例这本身是好事。但如果用了不同的连接名又没在退出时close()可能积累大量空闲连接。一个小建议全项目只用同一个连接名统一封装。参数速查表参数常见值说明hostName127.0.0.1 或 localhost本机数据库不必用公网地址port3306MySQL默认端口非常规端口时这里容易漏改databaseNamestudent_db必须是已存在的库名userNameroot 或专用账号课设阶段常用root但交付时建议建独立账号password自定义演示代码里别写真实口令2.3 连接失败自检分清驱动、服务与权限三个层面遇到连接失败我一般按三个层面排查驱动、服务、权限。驱动层就是 2.1 节说的QMYSQL是否存在服务层是 MySQL 进程有没有在监听权限层是账号能不能从那台主机连上这个库。服务层的排查命令# 看 3306 端口是否有进程在监听 netstat -an | grep 3306 # 检查 MySQL 服务状态 systemctl status mysql权限层的坑更隐蔽。MySQL 账号是区分 host 的rootlocalhost和root%不是同一个账号。你本地命令行能登录不代表 Qt 程序也能连接。可以在数据库里直接查看SELECT user, host FROM mysql.user;如果程序连接时报Access denied for user rootlocalhost说明账号存在但密码不符或者 host 段不匹配。常见做法是给程序单独建一个账号而不是用 root这样后期也不会被权限问题拖累。3. 数据表设计与增删改查学号主键、角色权限和事务删除3.1 先用SQL把两张表建出来学生信息管理系统最少需要两张表学生表和用户表。学生表负责业务数据用户表负责登录。建表脚本如下CREATE DATABASE student_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE student_db; CREATE TABLE student ( id VARCHAR(20) NOT NULL COMMENT 学号保留前导零, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT NOT NULL DEFAULT 0 COMMENT 0未知 1男 2女, major VARCHAR(80) NULL COMMENT 专业, class_no VARCHAR(30) NULL COMMENT 班级, phone VARCHAR(20) NULL COMMENT 联系电话, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE user ( username VARCHAR(50) NOT NULL, password_hash CHAR(64) NOT NULL COMMENT SHA-256哈希值, role VARCHAR(10) NOT NULL DEFAULT student, PRIMARY KEY (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计上学号主键我用VARCHAR(20)而不是自增INT。原因有三学号本身就是业务上的唯一标识不需要额外生成学号可能有前导零用INT会丢删除后自增主键补不补空位是个麻烦事业务主键没这个问题。gender用TINYINT而不是ENUM是因为后期如果加一种性别标识改表结构成本太高。字段清单字段类型说明idVARCHAR(20)学号业务主键nameVARCHAR(50)姓名必填genderTINYINT0未知 1男 2女majorVARCHAR(80)专业可空class_noVARCHAR(30)班级可空phoneVARCHAR(20)联系方式可空created_atDATETIME默认当前时间方便排序和回溯user表的password_hash固定CHAR(64)对应 SHA-256 哈希后的十六进制字符串。这里刻意不用明文密码课程设计答辩时提到这一点是加分项。3.2 登录验证prepare bindValue 挡 SQL 注入登录是大多数系统第一章业务逻辑也是检验你有没有基本安全意识的地方。直接拼 SQL 字符串是最常见的反面写法一旦用户名里出现单引号整个查询语句要么报错要么被绕过。我的做法是用prepare加bindValue#include QCryptographicHash QString loginUser(QSqlDatabase db, const QString username, const QString password) { // 先把明文口令转成 SHA-256 哈希 QByteArray hash QCryptographicHash::hash( password.toUtf8(), QCryptographicHash::Sha256).toHex(); QSqlQuery query(db); // 问号占位符配合 bindValue避免拼接 SQL query.prepare(SELECT role FROM user WHERE username ? AND password_hash ?); query.addBindValue(username); query.addBindValue(QString::fromLatin1(hash)); if (!query.exec()) { qDebug() login query failed: query.lastError().text(); return QString(); } // 找到用户返回角色字符串找不到返回空串 return query.next() ? query.value(0).toString() : QString(); }query.value(0)取的是SELECT的第一列也就是role。如果用户名不存在或密码错误query.next()返回false函数返回空字符串。调用方只需要判断返回值是否非空就能决定跳转到管理员界面还是提示“用户名或密码错误”。参数说明addBindValue绑定的是QVariant字符串类型传进去会自动处理单引号转义。这样既防 SQL 注入也避免中文特殊字符破坏语句结构。这是 QSqlQuery 最值得养成习惯的写法。3.3 增删改查的四类接口与批量删除事务数据访问层建议统一封装成函数界面层不直接写 SQL。一个学生数据访问类的典型接口如下struct StudentInfo { QString id; QString name; int gender; QString major; QString classNo; QString phone; }; bool insertStudent(QSqlDatabase db, const StudentInfo stu) { QSqlQuery query(db); query.prepare(INSERT INTO student (id, name, gender, major, class_no, phone) VALUES (?, ?, ?, ?, ?, ?)); query.addBindValue(stu.id); query.addBindValue(stu.name); query.addBindValue(stu.gender); query.addBindValue(stu.major); query.addBindValue(stu.classNo); query.addBindValue(stu.phone); if (!query.exec()) { qDebug() insert failed: query.lastError().text(); return false; } return true; }修改操作用UPDATE逻辑和插入完全对称只是bindValue的顺序不同。查询操作在下一章结合QSqlTableModel讲因为列表页用模型接管更省事。这里单独强调批量删除因为它是事务最常见的应用场景。bool deleteStudents(QSqlDatabase db, const QStringList ids) { // 批量删除必须放在事务里避免删一半失败留下脏数据 if (!db.transaction()) { return false; } QSqlQuery query(db); query.prepare(DELETE FROM student WHERE id ?); for (const QString id : ids) { query.addBindValue(id); if (!query.exec()) { // 任意一条失败就回滚全部 db.rollback(); return false; } } return db.commit(); }额外提一句如果学生表将来要关联选课表删除学生还要先处理外键约束否则会报外键冲突。事务在这里的作用就是保证“要么全部删掉要么什么都不删”避免界面显示已删除数据库里还躺着半截数据。4. QSqlTableModel接管列表界面不碰SQL的三种常见用法4.1 列表页使用模型而不是逐条QSqlQuery很多初学者的做法是界面里放一个QTableWidget用循环手动setItem填行。这个写法在小数据量时能跑但代码非常啰嗦查询结果要用query.value()逐列取值再创建QTableWidgetItem。换一个查询条件界面代码要重写一遍。更符合 Qt 架构的做法是用QSqlTableModel绑定数据表然后setModel给QTableView// MainWindow 构造函数里初始化模型 QSqlTableModel *model new QSqlTableModel(this, db); model-setTable(student); model-setEditStrategy(QSqlTableModel::OnManualSubmit); model-select(); // 真正执行查询 ui-tableView-setModel(model); // 设置表头显示文本界面显示中文表结构保持英文 model-setHeaderData(0, Qt::Horizontal, 学号); model-setHeaderData(1, Qt::Horizontal, 姓名); model-setHeaderData(2, Qt::Horizontal, 性别); model-setHeaderData(3, Qt::Horizontal, 专业);这段代码的关键点是setTable本身不访问数据库真正执行查询的是select()。表头可以在model上设置不用手动建QTableWidgetItem界面列宽、排序、编辑都交给 view 处理。后续如果换成别的表只需改表名和表头。4.2 搜索过滤LIKE 里两个百分号的来历QSqlTableModel提供了setFilter接口传进去的条件不需要WHERE关键字。这里的坑出现在写模糊搜索时void MainWindow::doSearch(const QString keyword) { QString kw keyword.trimmed(); if (kw.isEmpty()) { model-setFilter(QString()); } else { // 注意两个百分号一个留给 Qt 占位一个转义成 SQL 里的 % QString pattern QString(%%1%).arg(kw); model-setFilter(QString(name LIKE %1 OR id LIKE %1).arg(pattern)); } model-select(); // filter 设置后必须重新 select 才生效 }setFilter不是即时生效的改完必须调用select()。两个百分号的问题几乎所有第一次用的人都会问%%1里第一个%转义第二个%和结尾的%组合成 SQL 的模糊匹配符号。$$或者反斜杠转义在不同数据下行为不同所以这里我选了记录一个实例变量的方式把过滤条件留到保存时统一处理。4.3 编辑策略OnManualSubmit 还是 OnFieldChangeQSqlTableModel有三个编辑策略OnFieldChange、OnRowChange、OnManualSubmit。它们的区别在于数据提交到数据库的触发时机。OnFieldChange是用户改完一个单元格就立刻提交适合快速输入的场景但缺点是没有“保存”按钮改错一个字符就写进数据库想反悔只能靠revert。课程设计里我给的建议是OnManualSubmit所有编辑都停留在缓存中用户点“保存”才真正提交点“取消”就丢弃。提交保存的代码void MainWindow::onSaveClick() { if (!model-submitAll()) { QMessageBox::warning(this, 保存失败, model-lastError().text()); // 提交失败回滚当前所有未保存修改 model-revertAll(); return; } }这里有个初学者容易犯的手误在submitAll()成功后又去手动调用model-database().commit()。OnManualSubmit模式下submitAll()内部已经自己开了事务如果你再手动 commit数据库会报No transaction is active。我之前在这个问题上被误导过看报错才反应过来Qt 文档里写得很清楚特点是不说人话翻车一次就记住了。4.4 手动添加一行和删除选中行列表页另外两个常见操作是“添加”和“删除选中”。添加用insertRow在模型末尾追加一行记录void MainWindow::onAddClick() { int row model-rowCount(); if (!model-insertRow(row)) { return; } // 新行默认所有字段为空学号需要用户手动填写 model-setData(model-index(row, 1), 请填写姓名); }删除选中行要先收集选中行号再从模型和数据库里一起删。注意模型删除和数据库删除不能混单纯调用removeRow只是从模型里标记删除真正落到数据库还是要submitAll。void MainWindow::onDeleteClick() { QModelIndexList sel ui-tableView-selectionModel()-selectedRows(); // 倒序删除避免行号偏移 for (int i sel.size() - 1; i 0; --i) { model-removeRow(sel.at(i).row()); } if (!model-submitAll()) { QMessageBox::warning(this, 删除失败, model-lastError().text()); model-revertAll(); } }倒序删除是因为正序删掉第 0 行后原来第 1 行变成了第 0 行索引全部错位。这个细节在演示时如果选了三行删除会发现只删了其中一部分原因就是没倒序。模型把这层复杂性包装在了接口里但行索引偏移的逻辑还是要自己处理。5. 避坑清单QtMySQL 最常见的 5 个翻车现场5.1 报错 QMYSQL driver not loaded现象程序启动或执行addDatabase(QMYSQL)时报QSqlDatabase: QMYSQL driver not loaded。原因Qt 的插件目录里没有与当前 Qt 版本匹配的 MySQL 驱动插件或驱动依赖的 MySQL 客户端库不在这台机器上。解决先用QSqlDatabase::drivers()打印可用驱动列表确认。Qt5 常见做法是检查plugins/sqldrivers/qsqlmysql.dll是否存在Qt6 则需要单独安装 MySQL 驱动插件。若插件存在但加载失败用依赖检查工具看它缺少哪个动态库把对应 MySQL 客户端库拷贝到程序目录或系统 PATH 中。这是这套项目里翻车率最高的一条没有之一。5.2 中文写入后全是问号现象界面显示中文正常但写入 MySQL 后查看数据是??或者反过来数据库里正常界面显示乱码。原因建库字符集不是utf8mb4连接建立后没有执行SET NAMES utf8mb4Qt 源文件本身编码和系统编码不一致。解决建库脚本里显式指定DEFAULT CHARACTER SET utf8mb4在initDb()里每次连接成功后执行SET NAMES utf8mb4源码文件保存为 UTF-8 编码。三条同时做到中文乱码基本绝迹。5.3 点一次搜索就卡几秒现象数据表几千行每次搜索、刷新都明显卡顿窗口失去响应。原因每次搜索都执行SELECT * FROM student全表查询然后在内存里过滤而不是把过滤条件交给数据库。解决搜索前先setFilter再select()。比如按班级搜索过滤器写成class_no 软件2301让数据库返回最小数据集。如果业务数据涨到几十万行再用分页或者后台线程课程设计阶段把条件写进 SQL 已经足够。5.4 MySQL 重启后程序再也不连不回去现象项目跑到一半MySQL 服务重启之后界面操作全部报错重启程序也连不上。原因QSqlDatabase连接在底层断开后不会自动重连open()只认初始状态。解决封装一个ensureOpen()每次操作数据库之前都检查一次断了就重新open()并设置SET NAMES utf8mb4。bool ensureOpen(QSqlDatabase db) { if (!db.isOpen()) { return db.open(); } // 发一个轻量查询验证连接是否真的可用 QSqlQuery q(db); if (q.exec(SELECT 1)) { return true; } qDebug() connection lost, reconnecting: q.lastError().text(); if (db.open()) { q.exec(SET NAMES utf8mb4); return true; } return false; }调用方只需要把原来的QSqlQuery query(db);改成if (ensureOpen(db)) { QSqlQuery query(db); }程序的健壮性能上一个台阶。演示现场 MySQL 挂了又重启的尴尬靠这一招能挽救一半。5.5 打包到别的机器启动后驱动丢了现象开发机运行正常把整个发布目录拷贝到另一台电脑启动即报QMYSQL driver not loaded。原因Qt 的发布工具只收集 Qt 自身模块MySQL 驱动插件和客户端库需要手动拷贝很多新手只拷贝了 exe。解决发布时先把 Qt 运行时依赖工具跑一遍再检查程序目录。确认sqldrivers目录存在并且包含 MySQL 驱动文件同时把 MySQL 客户端动态库复制到程序目录或对应系统路径。整理一份清单每次打包后逐项检查不要凭记忆。6. 交付前再扫一遍导出、打包与演示数据的准备6.1 一个带 BOM 的 CSV 导出函数课设答辩时经常要做数据展示直接从界面截屏不如导出一份 CSV 给评委。CSV 导出最常见的坑是乱码Excel 打开 UTF-8 编码的 CSV 默认按 ANSI 解析中文全乱。解决办法是给文件加 BOM 头。bool exportStudentsToCsv(QSqlDatabase db, const QString filePath) { QSqlQuery query(db); if (!query.exec(SELECT id, name, gender, major, class_no FROM student ORDER BY id)) { return false; } QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return false; } QTextStream out(file); // Excel 需要 BOM 才能正确识别 UTF-8Qt 新版本用 setEncoding老版本用 setCodec out.setGenerateByteOrderMark(true); out 学号,姓名,性别,专业,班级\n; while (query.next()) { out query.value(0).toString() , query.value(1).toString() , query.value(2).toInt() , query.value(3).toString() , query.value(4).toString() \n; } return true; }性别字段存的是TINYINT导出时想显示成中文可以用一个简单映射0显示“未知”1显示“男”2显示“女”。这个映射逻辑放在界面层还是数据层看项目结构但我建议放在最靠近界面的地方数据层永远只负责存取原始值。6.2 打包与数据备份的最小检查列表交付前我一般照着这几条过一遍驱动插件是否在发布目录、动态库是否匹配位数、数据库连接密码是否改成演示环境的值、演示数据量是否足够覆盖增删改查。备份这个问题也值得多说一句把建表 SQL 脚本和一份演示数据 SQL 文件放到项目根目录这样换一台机器也能在几分钟内把数据库环境恢复起来不需要依赖原来那台电脑。我吃过一次亏现场演示时数据库连不上花了十分钟查才发现是机器重启后 MySQL 服务没起来台下一群人等着场面很难看。从那以后每次演示前我都会先跑一遍SELECT 1确认连接正常再开始这个习惯一直在用。希望本篇文章能帮你躲开同样的坑顺利跑通这个 QtMySQL 的学生信息管理系统。本文还有配套的精品资源点击获取