Qt+MySQL股票管理系统:驱动配置、数据库设计与避坑指南
发布时间:2026/10/2 16:48:56来源:尧图网络
简介大一C课设作业中的股票管理系统完整整合了C、Qt和MySQL是一份适合计算机专业初学者借鉴的课程设计项目。资源包含56个文件覆盖14个cpp源文件、13个h头文件、12个ui界面文件以及数据库SQL脚本、课程设计报告PDF、流程图PNG、Qt工程文件pro和可直接运行的打包程序压缩包共36.51MB目录结构清晰。目前已有247人学习下载。项目从需求分析、ER图到数据库表创建均有体现源码中涉及Qt信号与槽、控件布局、QSS样式、C文件I/O、多线程等常见技术点可帮助读者快速掌握桌面应用与数据库联调的基本流程。报告和打包程序可辅助验证实现效果适合用于复习C语法、熟悉Qt Designer操作并在此基础上扩展出完整的毕业设计或课设方案。1. 大一 C 课设用 QtMySQL 写股票管理系统这套资源能帮你把“界面能跑”和“数据落库”两端接起来数据库课设答辩现场最尴尬的问题往往不是“界面好不好看”而是老师追问一句“你这些数据到底存在哪”。用 Qt MySQL 写的股票管理系统在这方面天然占便宜它有 Qt Designer 排出来的界面、有 QTableView 列表、有 MySQL 里真实存储的股票和成交数据整套系统一般覆盖登录、股票信息维护、价格波动模拟、成交记录查询这几块恰好把 QSqlDatabase 连接、驱动加载、结果集刷新这一整条链路都走了一遍。这个项目的代码量不算大但环境配置的坑比业务逻辑多。如果你正在赶课设的大一学生或者想快速捡回 C 与数据库联调经验的从业者照着这套资源把环境跑通比从头瞎试能省下整整一个周末。2. Qt 5.15.2 与 MySQL 8.0 的环境匹配驱动加载是第一个分水岭拿到任何 Qt 数据库源码我先不急着打开工程而是先花十分钟验证驱动环境。暗坑太多了盲目编译大概率栽在同一个报错上QMYSQL driver not loaded。2.1 先打驱动清单QSqlDatabase::drivers() 能列出的才是可用的在 Qt 里连 MySQL第一步不是写连接函数是确认当前 Qt 编译环境里到底有没有 MySQL 驱动。QSqlDatabase::drivers() 返回的是插件库里所有能加载的数据库驱动名它跟本机是否安装 MySQL 没有直接关系。用下面这段代码可以快速自检#include QDebug #include QSqlDatabase void printAvailableDrivers() { QStringList list QSqlDatabase::drivers(); foreach (const QString driver, list) { qDebug() driver: driver; } }这段代码的逻辑很简单把 Qt 插件目录下已编译好的数据库驱动逐行打印出来。重点看输出里有没有 QMYSQL有说明 Qt 自带的 sql 插件包含 MySQL 驱动没有说明安装 Qt 时没带这个插件后面调用 addDatabase(QMYSQL) 一定会报 driver not loaded。为什么驱动要分两层理解第一层是 Qt 编译出来的 qsqlmysql.dll它负责把 Qt 的 API 转成 MySQL 的 C API第二层是 MySQL 客户端库 libmysql.dll负责底层网络通信。两层中任何一层缺失或者位数不一致表现都是同一个报错。环境问题最头疼的地方也在这里你根本不知道是缺了哪一层。如果驱动缺失最快的路径不是去源码编译而是确认你安装的是 MSVC 版 Qt然后到 Qt 安装目录的 plugins/sqldrivers 下找 qsqlmysql.dll把它和 MySQL 的 libmysql.dll 一起复制到 exe 目录。2.2 .pro 文件与插件 dll项目编译和运行各自缺什么另一种情况是代码一编译就报“QSqlDatabase: No such file or directory”这跟驱动无关是 .pro 文件少了 sql 模块。Qt 的 SQL 模块不会默认链接进来必须在工程文件里显式追加。一个标准的课设工程 .pro 文件长这样QT core gui sql greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET StockManager TEMPLATE app SOURCES \ main.cpp \ mainwindow.cpp HEADERS \ mainwindow.h FORMS \ mainwindow.ui参数逐条说明QT sql是关键它让 qmake 在链接阶段引入 Qt5Sql.lib在 Qt 6 里模块改叫 Qt6Sql但写法不变。greaterThan(QT_MAJOR_VERSION, 4): QT widgets这行处理 Qt4/Qt5 兼容Qt 5.15.2 工程保留它不会出错。FORMS mainwindow.ui让构建系统自动调用 uic 工具把界面描述转成 C 代码不用手动生成。编译只解决符号链接。程序运行时Qt 会去 exe 目录和 Qt 安装目录找插件找不到就会“编译通过但一打开数据库就报 driver not loaded”。这里有一个容易被忽略的细节是编译器类型混用MinGW 版 Qt 与 MSVC 版 Qt 的插件二进制不能互换你把 MSVC 的 qsqlmysql.dll 塞进 MinGW 工程的 exe 目录结果要么报加载失败要么在 open 时直接崩。常见环境组合按可坑程度排一下环境组合表现建议Qt 5.15.2 MSVC2019_64 MySQL 8.0 x64稳定首选复制 libmysql.dll 后几乎不出问题Qt 5.15.2 MinGW64 MySQL 8.0 x64驱动插件难找课设不推荐除非你有编译驱动的经验Qt 5.15.2 MSVC2019_64 MariaDB 10.x稳定MySQL 装不上时可退一步2.3 认证插件选择caching_sha2_password 与 mysql_native_password连接时报 Access denied不一定是密码错也可能是认证插件不兼容。MySQL 8.0 默认新用户走 caching_sha2_password而老版本 Qt 的 QMYSQL 驱动对这个插件支持不完善两边握不上手。最稳的解决方案是建一个指定老认证插件的专用账号CREATE USER stock_userlocalhost IDENTIFIED WITH mysql_native_password BY stock_pass; GRANT ALL PRIVILEGES ON stock_db.* TO stock_userlocalhost; FLUSH PRIVILEGES;这段脚本做三件事一、创建应用专用账号不跟 root 共用密码答辩时出问题直接重置这个账号也不影响全局二、指定 mysql_native_password让 QMYSQL 驱动能顺利通过认证三、只授予 stock_db 这一个库的权限避免误操作其他业务库。MySQL 8.0 后续版本对 mysql_native_password 的支持也在收紧但在课堂设计和课程作业场景下这仍然是最成熟、被验证次数最多的方案。环境配置到这里就算把第一层打通了。配置完先别急着写业务代码在程序启动函数里执行一条SELECT 1确认认证确实通了再往下走。3. 数据库表设计把股票信息和成交记录拆开字段类型直接决定后续代码量如果只求“能跑”股票信息和成交记录塞一张表也能跑但后续的关联查询、价格模拟、成交列表全部会变得别扭。从课设角度我建议至少分成 stock_info 和 trade_record 两张表。3.1 两张核心表的字段定义DECIMAL 与 BIGINT 的使用理由下面是完整建表脚本在 MySQL 命令行或 Navicat 里执行都行CREATE DATABASE IF NOT EXISTS stock_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE stock_db; DROP TABLE IF EXISTS trade_record; DROP TABLE IF EXISTS stock_info; CREATE TABLE stock_info ( stock_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 内部主键, stock_code VARCHAR(10) NOT NULL UNIQUE COMMENT 股票代码如 600519, stock_name VARCHAR(32) NOT NULL COMMENT 股票名称, current_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 当前股价, total_shares BIGINT NOT NULL DEFAULT 0 COMMENT 总股本单位:股, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE trade_record ( trade_id INT PRIMARY KEY AUTO_INCREMENT, stock_id INT NOT NULL, trade_price DECIMAL(10,2) NOT NULL, trade_volume INT NOT NULL, trade_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_stock_id FOREIGN KEY (stock_id) REFERENCES stock_info(stock_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个字段选择背后的理由是这份资源真正值得读的地方current_price 用 DECIMAL(10,2) 而不用 FLOAT。FLOAT 是近似存储1.1 这种金额在计算尾数时会变成 1.100000023做总资产统计时对不上账DECIMAL 是精确小数两位正好覆盖股价显示需求。total_shares 用 BIGINT 不用 INT。贵州茅台总股本约 12.56 亿股平安银行接近 194 亿股INT 上限约 21.47 亿直接不够用。stock_code 设置 UNIQUE保证同一股票代码不能重复插入这是股票场景最基本的业务约束。trade_record 用外键关联 stock_id。课设规模小看不出性能差异但它能让你在答辩时讲清楚“每一笔成交如何定位到对应股票”。注意TIMESTAMP 类型有 2038 年溢出问题。课设演示用 TIMESTAMP 图的是ON UPDATE CURRENT_TIMESTAMP自动更新时间如果想把这个工程留作毕业设计基础更稳的方案是把 updated_at 和 trade_time 都改成DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP功能几乎一样但不会在 2038 年出边界问题。3.2 从建库语句到 Qt 连接函数把数据库初始化写成一个独立入口Qt 连接 MySQL 的标准做法是 QSqlDatabase::addDatabase然后逐项填连接参数。下面这个 init 函数在课设场景里基本是通用模板bool initDatabase() { QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL); db.setHostName(127.0.0.1); db.setPort(3306); db.setDatabaseName(stock_db); db.setUserName(stock_user); db.setPassword(stock_pass); if (!db.open()) { qDebug() DB open failed: db.lastError().text(); return false; } return true; }参数说明addDatabase(QMYSQL)的字符串必须与第 2 节 drivers() 打印的结果完全一致写成 mysql 或 MySQL 都找不到驱动。setHostName(127.0.0.1)明确走 TCP。如果填 localhost部分 Linux/macOS 环境会优先尝试 Unix socket后面报错形式完全不同。setPort(3306)对应 MySQL 默认端口。如果你装的是 MariaDB 或改过端口这里要同步。setConnectOptions(MYSQL_OPT_RECONNECT1)可以加在 open 之前让 MySQL 服务重启后程序自动重连但该选项在较新的 libmysql 版本里支持程度不稳定不建议把它当救命稻草。还有一个隐藏问题如果在一个程序里多次调用 addDatabase 而不给第二个参数会触发 duplicate connection name。具体绕法在第 5.5 节展开。3.3 初始化基础数据QSqlQuery 执行插入的两种姿势表建好后第一次运行需要几条演示数据。最直接的方式是在程序启动流程里执行 INSERTvoid seedData(QSqlDatabase db) { QSqlQuery q(db); q.exec(INSERT INTO stock_info(stock_code, stock_name, current_price, total_shares) VALUES(600519, 贵州茅台, 1710.50, 1256000000)); q.exec(INSERT INTO stock_info(stock_code, stock_name, current_price, total_shares) VALUES(000001, 平安银行, 12.36, 19400000000)); qDebug() seed done; }这段代码做了两件事把两条基础行情写进 stock_info再打印日志。注意19400000000这个数字超过 int 范围但数据库字段是 BIGINT插入没问题反过来Qt 读取时也要用 qlonglong 接不能声明成 int。整型字段的匹配是双向的建库时用 BIGINT读取时也得配套。如果担心重复运行导致数据翻倍seed 前先清空表。清空用 TRUNCATE stock_info 会重置自增主键DELETE FROM stock_info 只清行但自增不归零。演示场景我倾向 TRUNCATE因为股票代码是基础数据而不是业务流水自增从 1 重新开始更干净。4. 核心功能实现QSqlTableModel 绑定表格到批量更新股价这一章是整套系统的主干。界面与数据库的交互我用的方案是 QSqlTableModel 负责单表展示与编辑QSqlQuery 负责批量更新和复杂查询。这两条路线分开代码结构会清晰很多。4.1 显示股票列表QSqlTableModel 与 QTableView 的绑定QSqlTableModel 是 Qt 提供的一种不需要手写查询就能映射到整张表的模型。设置好表名和编辑策略后模型会维护一份数据缓存表格刷新由模型驱动。绑定最小代码QSqlTableModel *model new QSqlTableModel(this); model-setTable(stock_info); model-setEditStrategy(QSqlTableModel::OnManualSubmit); model-setHeaderData(1, Qt::Horizontal, 股票代码); model-setHeaderData(2, Qt::Horizontal, 股票名称); model-setHeaderData(3, Qt::Horizontal, 当前价格); model-select(); ui-tableView-setModel(model); ui-tableView-setSelectionBehavior(QAbstractItemView::SelectRows);逻辑说明setTable(stock_info)只是配置表名真正把数据拉进来的是末尾的select()setHeaderData改的是列标题显示名字段索引 1 对应表里的 stock_code索引 0 是自增主键 stock_id所以列表第一列通常不显示主键。给表格设置SelectRows后点任意单元格都会选中整行后面做删除操作定位更可靠。这里最值得强调的是setEditStrategy的三个取值。它决定用户编辑一个单元格后什么时候把修改写回 MySQL策略写回时机课设建议QSqlTableModel::OnFieldChange当前字段编辑完立即写库误触直接改库不推荐QSqlTableModel::OnRowChange光标离开当前行时写库相对保守但仍可能误改QSqlTableModel::OnManualSubmit只有主动 submitAll() 才写库推荐保存按钮自己控制课设答辩时老师经常会在表格里乱点几下如果用 OnFieldChange展示用的数据库可能已经悄悄被改乱了。OnManualSubmit 就是“后悔药”式策略本地随便怎么改不点保存就不写库。4.2 新增与删除记录的完整流程QSqlTableModel 缓存模型下新增记录通常这样写void MainWindow::onAddStock() { int row model-rowCount(); model-insertRow(row); model-setData(model-index(row, 1), 000000); model-setData(model-index(row, 2), 新股票); model-setData(model-index(row, 3), 0.00); } void MainWindow::onSave() { QSqlDatabase db model-database(); if (!db.transaction()) { qDebug() transaction error: db.lastError().text(); return; } if (model-submitAll()) { db.commit(); refreshStockTable(); } else { db.rollback(); model-revertAll(); qDebug() submit error: model-lastError().text(); } }逻辑说明insertRow(row)在缓存末尾追加一行此时数据库没有变化setData只改本地缓存。onSave 里先把缓存放进事务提交成功后立刻重新读表让视图与数据库对准失败则 rollback 并调用revertAll()回滚缓存。“提交失败必须 revert”这个动作很多人漏写漏写的后果是下次操作时旧数据又被当作新编辑带出来。删除记录则是定位当前行后 removeRowvoid MainWindow::onDeleteStock() { int row ui-tableView-currentIndex().row(); if (row 0) { return; } model-removeRow(row); onSave(); }这里的判断是row 0直接返回因为没选中行时 currentIndex() 返回无效索引row() 是 -1直接 removeRow(-1) 可能引发崩溃。这是删除按钮最常见的故障来源。4.3 事务与批量更新模拟股价波动的代码本身课设里常要求做一个“模拟股价随机波动”的按钮。最容易想到的实现是循环修改 QSqlTableModel 的每行再逐行 submitAll。这样有两个问题逐行提交慢中途失败无法回滚。我建议直接用 QSqlQuery 的 prepare 批量更新把所有更新包进一个事务void MainWindow::simulatePriceChange(int rangePercent) { QSqlDatabase db QSqlDatabase::database(); if (!db.transaction()) { qDebug() start transaction failed: db.lastError().text(); return; } QSqlQuery q(db); q.prepare(UPDATE stock_info SET current_price ROUND(current_price * ? ?, 2) WHERE stock_id ?); for (int i 1; i model-rowCount(); i) { double factor 1.0 (rand() % (2 * rangePercent * 100) - rangePercent * 100) / 10000.0; double bias (rand() % 101 - 50) / 100.0; q.addBindValue(factor); q.addBindValue(bias); q.addBindValue(i); q.exec(); } if (!db.commit()) { db.rollback(); qDebug() commit failed; return; } refreshStockTable(); }这段代码有三个关键点要说明。第一prepare addBindValue 的价值在于不让浮点数走字符串拼接。浮点数转字符串可能出现 0.129999999 这种尾巴拼进 SQL 会让最终结果差一分钱甚至引号处理不当直接语法错误绑定变量把值交给 MySQL 处理这类问题直接消失。第二rand() 生成的范围是 [-rangePercent%, rangePercent%]rangePercent * 100把百分比换算成 1/100 单位再除以 10000 得到 0.01% 粒度整体就是平移到正负区间的均匀分布。第三事务保证要么全部行更新要么全部不更新。循环 200 次时第 150 次失败前 149 次已经写库演示现场就是“一半涨了一半没动”非常尴尬用事务后任意一次 exec 失败rollback 会把整个表恢复原样。刷新函数 refreshStockTable 需要特别注意直接 model-select() 虽然能重新读库但表头会恢复成数据库字段名所以我每次刷新时都重新设置三个列标题。这个“表头变回 stock_id”的现象我第一次演示时也碰到过下面避坑清单里再展开。5. 避坑清单五个真实踩过的坑从驱动装不上到表格不刷新这一章是我的血泪记录。每条按现象、原因、解决三步走遇到相同报错可以直接对号入座。5.1 现象QMYSQL driver not loaded现象程序运行到 db.open() 时控制台输出 QSqlDatabase: QMYSQL driver not loadedopen() 返回 false。原因Qt 插件目录里没有 qsqlmysql.dll或者 qsqlmysql.dll 依赖的 libmysql.dll 找不到。这个坑最会伪装成“代码写错”实际上纯粹是运行环境问题。还有一种比较玄学的情况是昨天还好好的、今天突然报错多半是 PATH 里的 MySQL bin 目录被系统更新或环境变量调整给弄丢了。解决按顺序排查。先用 2.1 的打印函数确认 drivers() 里有 QMYSQL有就检查 exe 同目录下的 libmysql.dll没有就去 Qt 安装目录 plugins/sqldrivers 找 qsqlmysql.dll找到后连同 libmysql.dll 一起复制到 exe 目录。libmysql.dll 一般在 MySQL 8.0 安装目录的 bin 文件夹下。提示这两个 dll 必须配对出现只复制其中一个问题不会消失。5.2 现象中文显示成“????”或乱码现象MySQL 命令行工具中文正常Qt 表格里却显示“????”或乱码。原因三个环节的字符集不一致建库字符集、Qt 客户端连接字符集、源码文件保存字符集。任一个环节用了 GBK、另一个用了 utf8mb4就会出现乱码。解决建库统一成 utf8mb4连接成功后执行SET NAMES utf8mb4QSqlQuery q(db); q.exec(SET NAMES utf8mb4);另外要注意源码文件的保存编码。Windows 下 Qt Creator 新建的 .cpp 默认是 UTF-8但如果你用 VS Code 打开后另存成 GBK字符串字面量里的中文在 MSVC 编译后字节就不对。这种问题不好定位因为它不报错只是显示乱码。如果源码编码已经被动过把中文字符串统一用QString::fromUtf8(股票)包一层是短期见效的做法。5.3 现象submitAll() 成功但表格显示不刷新现象onSave() 走完数据库里确实有数据QTableView 显示却不变重启程序才看到新数据。原因OnManualSubmit 策略下submitAll() 之后模型缓存不会自动重新查询。更隐蔽的是执行 select() 之后如果没有重新 setHeaderData表头会从“股票名称”变回原始的 stock_name。解决把“设置表名 设置列标题 select”拆成一个 refreshStockTable() 独立函数在初始化、保存、股价波动三处统一调用函数末尾重新设置一次列标题。这样就永远不会出现保存后列表不动、表头变回字段名的问题。void MainWindow::refreshStockTable() { model-select(); model-setHeaderData(1, Qt::Horizontal, 股票代码); model-setHeaderData(2, Qt::Horizontal, 股票名称); model-setHeaderData(3, Qt::Horizontal, 当前价格); }5.4 现象Access denied 或 socket 错误现象连接失败报 Access denied for user rootlocalhost或是 Cant connect to local MySQL server through socket /tmp/mysql.sock。原因两条独立路径。Access denied 基本是认证插件不匹配或密码错误socket 报错则是因为主机名填了 localhost在 Linux/macOS 上 Qt 走了 Unix socket 而不是 TCP而 MySQL 服务端配置的 socket 路径不一致。解决主机名统一改为 127.0.0.1用户用专用账号 authentication plugin 选 mysql_native_password如果密码确实不对重新走 MySQL 的 reset password 流程。对课设来说专门建一个 stock_user 不只解决认证问题权限也更好控制删除重建不影响其他库。5.5 现象重复 addDatabase 导致崩溃或 duplicate connection name现象第二次调用 initDatabase() 时程序崩溃或控制台输出 QSqlDatabasePrivate::addDatabase: duplicate connection name qt_sql_default_connection。原因addDatabase 在不给连接名时会使用默认名 qt_sql_default_connection多次调用等于重复注册同一个连接名。Qt 这一层对同名连接直接报错。解决single-window 课设其实只需要调用一次 initDatabase()把它放在启动阶段全局保持唯一。如果确实需要动态重连必须遵循“remove 在连接对象作用域之外”这个规则bool reconnectDb() { { QSqlDatabase db QSqlDatabase::database(); if (db.isOpen()) { db.close(); } } QSqlDatabase::removeDatabase(qt_sql_default_connection); QSqlDatabase db2 QSqlDatabase::addDatabase(QMYSQL); // ... 重新设置 host、port、user、password return db2.open(); }注意removeDatabase 要求所有关联的 QSqlDatabase 对象先被释放所以先放进一对大括号让 db 析构再执行 remove。写成 db.close(); QSqlDatabase::removeDatabase(...) 仍然会出问题这是 Qt 数据库模块里一个极易踩的内存管理陷阱。6. 最后再加一个小功能股票名称模糊搜索与验证演示收尾时最容易加分的往往是一个搜索框。在界面上放一个 QLineEdit输入“平安”按回车股票列表立刻只剩平安银行清空则返回全表。用 QSqlQueryModel 做代码量小而且不会破坏原有的编辑模型。void MainWindow::onSearch() { QString keyword ui-searchEdit-text().trimmed(); if (keyword.isEmpty()) { refreshStockTable(); return; } QSqlQueryModel *searchModel new QSqlQueryModel(this); QSqlQuery q(QSqlDatabase::database()); q.prepare(SELECT stock_code, stock_name, current_price, updated_at FROM stock_info WHERE stock_name LIKE ? OR stock_code LIKE ? ORDER BY stock_code LIMIT 50); QString pattern QString(%%1%).arg(keyword); q.addBindValue(pattern); q.addBindValue(pattern); if (q.exec()) { searchModel-setQuery(q); ui-tableView-setModel(searchModel); } }这段代码细看有三个细节通配符 % 是在代码侧与关键字拼好再用 addBindValue 传给 SQL而不是拼进 SQL 字符串防止用户输入单引号等特殊字符时破坏语句结构。LIMIT 50 是一个性能护栏就算表里以后有几万条记录搜索也只取前 50 条页面不会卡。用 QSqlQueryModel 而不是继续用 QSqlTableModel是因为搜索结果只读能防止用户在查询结果里误触发编辑。验证这个功能是否真正可用我一般固定三步第一步输入“平安”看返回结果是否只剩平安银行第二步输入“600”确认能按股票代码命中第三步输入一个不存在的代码确认返回空列表而不是弹错误框。三步全绿搜索功能在答辩演示时就有说服力了。从那以后每次课设交付我都强制把这套验证顺序走一遍先打印驱动清单再执行一次 select最后跑一次 LIKE 搜索。三层验证全绿才敢把程序交出去。这次拆这份 QtMySQL 股票管理系统的源码时也顺手把验证跑完希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网