简介这是一份面向计算机相关专业低年级学生的C课程设计参考项目以股票管理系统为主题将C面向对象编程、Qt图形界面开发与MySQL数据库操作整合为一个可运行的桌面应用适合正在准备课设、毕设或想入门Qt数据库组合开发的学习者。压缩包共56个文件约36.51MB包含14个cpp源文件、13个头文件、12个ui界面文件以及png流程图、sql建库脚本、qrc资源文件、pro工程文件和课程设计报告pdf覆盖从界面布局到数据存储的完整结构。项目涉及信号与槽、SQL增删改查、数据库表设计、多线程与文件I/O等知识点并附有打包好的可执行程序双击即可运行体验。目前已有247人学习下载可作为课设选题模板、代码临摹范本与答辩材料参考帮助读者快速理解一个完整小系统的组织方式与实现思路。1. 从一份大一课设说起QtMySQL 股票管理系统到底在练什么很多人第一次看到「大一 C 课设作业股票管理系统QtMySQL」这个标题第一反应是「不就是个增删改查吗」。真动手做过的都知道这玩意儿是 C 初学者第一次把语言、GUI、数据库三条线拧成一股绳的实战。它要解决的核心问题是用 C 写一个带图形界面的桌面程序把股票代码、名称、持仓、买卖记录这些数据存进 MySQL再通过 Qt 的界面读出来、改回去。适合谁适合刚学完 C 语法、想找一个能写进简历、又能真正跑起来的练手项目的人。它不追求高并发但能让你把「结构体怎么映射成表」「Qt 怎么连数据库」「界面卡顿怎么优化」这些真问题踩一遍。下面我按自己带学弟做课设的顺序把这条路拆开讲清楚。2. 环境先跑通Qt、MySQL 和编译器的版本对齐2.1 为什么版本对不齐是第一个翻车点Qt 和 MySQL 的搭配最玄学的地方不在代码在版本。Qt 5.15.2 的 msvc2019_64 套件配 MySQL 8.0 的 C 连接库再配 Visual Studio 2019 的编译器这是目前课设里最稳的一组。很多人用 Qt 6 配 MySQL 5.7结果QMYSQL driver not loaded报错查一整天。原因很简单Qt 自带的 MySQL 驱动是用特定版本的 MySQL 客户端库编译的你本地装的 MySQL 版本和它不一致驱动就加载失败。常见做法是先确认 Qt 安装时勾选了 Sources 和 MySQL 驱动源码然后自己用qmake重新编译驱动。下面这段是编译 MySQL 驱动的典型命令在 Qt 的 MySQL 源码目录下执行。# 进入 Qt 自带的 MySQL 驱动源码目录 cd C:\Qt\5.15.2\Src\qtbase\src\plugins\sqldrivers\mysql # 用 Qt 的 qmake 生成 Makefile注意 MySQL 的 include 和 lib 路径要换成你本地的 qmake INCLUDEPATHC:/Program Files/MySQL/MySQL Server 8.0/include \ LIBSC:/Program Files/MySQL/MySQL Server 8.0/lib/libmysql.lib \ mysql.pro # 用 nmake 编译需要先打开 VS 的 x64 Native Tools 命令行 nmake nmake install这段命令的逻辑是告诉 qmake 去哪里找 MySQL 的头文件和库文件然后编译出qsqlmysql.dll最后安装到 Qt 的插件目录。参数里INCLUDEPATH指向 MySQL 安装目录下的 includeLIBS指向 libmysql.lib。编译成功后把生成的 dll 复制到Qt\5.15.2\msvc2019_64\plugins\sqldrivers下。注意MySQL 8.0 默认的认证插件是caching_sha2_password而老版本 Qt 驱动可能只认mysql_native_password如果连不上需要在 MySQL 里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;改一下认证方式。2.2 用 Qt Creator 建工程并连上数据库环境通了之后新建一个 Qt Widgets Application在.pro文件里加上QT sql。然后写一个最小的连接测试确认能读到 MySQL 的版本号。#include QCoreApplication #include QSqlDatabase #include QSqlQuery #include QSqlError #include QDebug int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); // 添加 MySQL 驱动 QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL); db.setHostName(127.0.0.1); // 本机数据库 db.setPort(3306); // MySQL 默认端口 db.setDatabaseName(stock_db); // 提前建好的库名 db.setUserName(root); db.setPassword(your_password); if (!db.open()) { qDebug() 连接失败: db.lastError().text(); return -1; } qDebug() 连接成功MySQL 版本: db.driver()-handle().value(serverVersion).toString(); QSqlQuery query; query.exec(SELECT VERSION()); if (query.next()) { qDebug() 查询到的版本: query.value(0).toString(); } return a.exec(); }这段代码的关键参数是setDatabaseName它对应 MySQL 里已经CREATE DATABASE stock_db;建好的库。setHostName用127.0.0.1比localhost更稳避免 socket 和 TCP 的歧义。如果db.open()返回 false先看lastError().text()常见的是密码错、库不存在、驱动没加载。驱动没加载的报错是QMYSQL driver not loaded回到 2.1 重新编译驱动即可。3. 表结构设计把股票、持仓、交易记录拆成三张表3.1 为什么不能一张表存所有东西初学者最容易犯的错是建一张大表字段从股票代码一直排到买入时间然后每次买卖都往里面插一行。这样做的后果是查当前持仓要写复杂的聚合改股票名称要更新几十行数据冗余到没法维护。正确的做法是按实体拆表股票基础信息一张表持仓一张表交易流水一张表。股票表存代码、名称、最新价持仓表存代码、数量、成本价交易表存每一笔买卖的时间、方向、价格、数量。三张表通过股票代码关联。下面是我课设里用的建表 SQL字段类型和索引都标清楚了。-- 股票基础信息表 CREATE TABLE stock_info ( stock_code VARCHAR(10) PRIMARY KEY, -- 股票代码主键 stock_name VARCHAR(50) NOT NULL, -- 股票名称 latest_price DECIMAL(10,2) DEFAULT 0, -- 最新价保留两位小数 update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 持仓表 CREATE TABLE position ( id INT AUTO_INCREMENT PRIMARY KEY, stock_code VARCHAR(10) NOT NULL, quantity INT NOT NULL DEFAULT 0, -- 持仓数量 cost_price DECIMAL(10,2) NOT NULL, -- 成本价 FOREIGN KEY (stock_code) REFERENCES stock_info(stock_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 交易流水表 CREATE TABLE trade_record ( id INT AUTO_INCREMENT PRIMARY KEY, stock_code VARCHAR(10) NOT NULL, trade_type TINYINT NOT NULL, -- 1 买入2 卖出 price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, trade_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_code (stock_code), INDEX idx_time (trade_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时注意DECIMAL(10,2)而不是FLOAT金额用浮点会丢精度这是血泪经验。utf8mb4是为了支持中文股票名。外键约束保证持仓里的代码一定在股票表里存在但课设里如果嫌麻烦可以去掉外键用程序逻辑保证。索引加在stock_code和trade_time上后面按代码查流水、按时间排序都用得到。3.2 用结构体映射表别在界面里裸写 SQLQt 里操作数据库常见做法是定义一个结构体对应一张表把查询结果塞进结构体界面只跟结构体打交道。这样界面代码和 SQL 解耦改表结构时只改一处。// Stock.h 定义结构体 struct StockInfo { QString code; QString name; double latestPrice; }; struct Position { QString code; int quantity; double costPrice; }; // 从数据库读取股票列表 QListStockInfo loadAllStocks(QSqlDatabase db) { QListStockInfo list; QSqlQuery query(db); query.exec(SELECT stock_code, stock_name, latest_price FROM stock_info); while (query.next()) { StockInfo s; s.code query.value(0).toString(); s.name query.value(1).toString(); s.latestPrice query.value(2).toDouble(); list.append(s); } return list; }这段代码的逻辑是查询结果逐行读出来按字段顺序赋值给结构体最后返回一个列表。参数上query.value(0)对应 SELECT 里的第一个字段顺序不能错。如果表字段多建议用query.record().indexOf(stock_code)按列名取避免改 SELECT 顺序时出错。界面层拿到QListStockInfo后直接往QTableWidget或QTableView里填不用再碰 SQL。4. 界面与数据联动从 QTableWidget 到 QTableView 的取舍4.1 小数据量用 QTableWidget 快速出活课设初期股票数量就几十条用QTableWidget最省事。它每个单元格就是一个QTableWidgetItem直接setItem就行。下面是把股票列表填进表格的典型写法。void MainWindow::refreshStockTable() { QListStockInfo stocks loadAllStocks(db); ui-tableWidget-setRowCount(stocks.size()); ui-tableWidget-setColumnCount(3); ui-tableWidget-setHorizontalHeaderLabels({代码, 名称, 最新价}); for (int i 0; i stocks.size(); i) { ui-tableWidget-setItem(i, 0, new QTableWidgetItem(stocks[i].code)); ui-tableWidget-setItem(i, 1, new QTableWidgetItem(stocks[i].name)); ui-tableWidget-setItem(i, 2, new QTableWidgetItem( QString::number(stocks[i].latestPrice, f, 2))); } }逻辑很直白先设行数和列数再逐行逐列塞数据。参数f, 2表示浮点数保留两位小数。这种写法在数据量小于几百行时完全够用但一旦股票数量上千或者你频繁刷新界面就会卡。原因在于QTableWidget为每个单元格都创建了一个QTableWidgetItem对象内存和刷新开销都大。4.2 数据量上来后换 QTableView 自定义 Model热搜里「qt 表格大数据卡顿优化 tablewiget 到 qtableview 自定义 model」说的就是这个坎。QTableView配合QAbstractTableModel只渲染可见区域数据量大时流畅得多。下面是一个最小可用的自定义模型。class StockTableModel : public QAbstractTableModel { Q_OBJECT public: explicit StockTableModel(QObject *parent nullptr) : QAbstractTableModel(parent) {} // 告诉视图有多少行 int rowCount(const QModelIndex parent QModelIndex()) const override { return m_stocks.size(); } // 固定三列 int columnCount(const QModelIndex parent QModelIndex()) const override { return 3; } // 决定每个单元格显示什么 QVariant data(const QModelIndex index, int role Qt::DisplayRole) const override { if (!index.isValid() || role ! Qt::DisplayRole) return QVariant(); const StockInfo s m_stocks[index.row()]; switch (index.column()) { case 0: return s.code; case 1: return s.name; case 2: return QString::number(s.latestPrice, f, 2); default: return QVariant(); } } // 表头 QVariant headerData(int section, Qt::Orientation orientation, int role) const override { if (role ! Qt::DisplayRole || orientation ! Qt::Horizontal) return QVariant(); switch (section) { case 0: return 代码; case 1: return 名称; case 2: return 最新价; default: return QVariant(); } } // 外部更新数据后调用通知视图刷新 void setStocks(const QListStockInfo stocks) { beginResetModel(); m_stocks stocks; endResetModel(); } private: QListStockInfo m_stocks; };这个模型的核心是rowCount、columnCount、data三个函数。data里根据index.row()和index.column()返回对应单元格的内容。setStocks里用beginResetModel和endResetModel包住数据替换视图会自动刷新。用的时候在窗口里new StockTableModel然后ui-tableView-setModel(model)。数据量上千行时这种写法的滚动和刷新明显比QTableWidget顺滑。注意data函数会被频繁调用里面不要做数据库查询或复杂计算只做内存读取。5. 避坑与排查课设里最容易卡住的五个地方5.1 现象编译报错dependent ..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwid... does not exist原因.pro文件里引用了不存在的路径或者 Qt 安装目录被移动过qmake 缓存了旧路径。解决删掉工程目录下的Makefile、.qmake.stash和debug/release文件夹重新执行qmake再构建。如果还不行检查.pro里有没有手写的INCLUDEPATH指向了已删除的目录。5.2 现象程序运行后界面能显示但一操作数据库就崩溃原因QSqlDatabase对象在多个线程间共享或者QSqlQuery在数据库关闭后还在使用。Qt 的数据库连接不能跨线程直接传递。解决每个线程用QSqlDatabase::addDatabase单独建连接连接名用线程 ID 区分。课设里如果没用多线程检查是不是在main函数里db是局部变量出了作用域就析构了而界面里还在用。把db设成全局或类的成员变量。5.3 现象MySQL 插入中文股票名变成乱码原因数据库、表、连接三处的字符集不一致。解决建库时用CREATE DATABASE stock_db DEFAULT CHARSET utf8mb4;建表时也指定utf8mb4Qt 连接后执行query.exec(SET NAMES utf8mb4);。三处都对齐后中文就不会乱。注意 MySQL 8.0 默认就是utf8mb4但 5.7 需要手动设。5.4 现象QTableWidget刷新时闪屏或者滚动条位置乱跳原因每次刷新都clearContents再重新setItem视图整个重建。解决数据量小的时候可以只更新变化的单元格数据量大就换QTableView Model用dataChanged信号局部刷新。另外刷新前用ui-tableWidget-setUpdatesEnabled(false);刷完再true能减少闪烁。5.5 现象程序打包到别人电脑上提示缺少libmysql.dll原因libmysql.dll没有随程序一起发布。解决把 MySQL 安装目录lib下的libmysql.dll复制到 exe 同级目录。如果还提示缺Qt5Sql.dll和qsqlmysql.dll用 Qt 的windeployqt工具自动拷贝依赖windeployqt your_app.exe。注意qsqlmysql.dll在plugins\sqldrivers下需要手动建sqldrivers文件夹放进去。6. 进阶技巧用事务保证买卖操作的原子性课设做到后面老师往往会问一句「如果买入时扣了钱但没加上持仓怎么办」。这就是事务要解决的问题。股票买卖涉及两张表交易流水插一条持仓表更新一条。这两步必须一起成功或一起失败。MySQL 的 InnoDB 引擎支持事务Qt 里用QSqlDatabase::transaction()和commit()包起来。bool buyStock(QSqlDatabase db, const QString code, double price, int quantity) { if (!db.transaction()) { // 开启事务 qDebug() 开启事务失败: db.lastError().text(); return false; } QSqlQuery query(db); // 第一步插入交易流水 query.prepare(INSERT INTO trade_record (stock_code, trade_type, price, quantity) VALUES (?, 1, ?, ?)); query.addBindValue(code); query.addBindValue(price); query.addBindValue(quantity); if (!query.exec()) { qDebug() 插入流水失败: query.lastError().text(); db.rollback(); // 失败回滚 return false; } // 第二步更新持仓如果已存在则累加数量和成本 query.prepare(INSERT INTO position (stock_code, quantity, cost_price) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quantity quantity VALUES(quantity), cost_price (cost_price * quantity VALUES(cost_price) * VALUES(quantity)) / (quantity VALUES(quantity))); query.addBindValue(code); query.addBindValue(quantity); query.addBindValue(price); if (!query.exec()) { qDebug() 更新持仓失败: query.lastError().text(); db.rollback(); return false; } if (!db.commit()) { // 提交事务 qDebug() 提交失败: db.lastError().text(); db.rollback(); return false; } return true; }这段代码的关键在ON DUPLICATE KEY UPDATE它利用stock_code的唯一索引如果持仓已存在就更新不存在就插入。成本价的计算公式是加权平均(原成本 * 原数量 新价格 * 新数量) / 总数量。事务保证了两步要么都成功要么都回滚不会出现「扣了钱没加仓」的脏数据。参数上addBindValue按?的顺序绑定顺序不能错。注意MySQL 的VALUES()函数在 8.0.20 之后被标记为废弃但课设环境里通常还能用如果报错就改用别名写法。验证事务是否生效可以故意在第二步之前抛一个异常看流水表里有没有多出记录。如果没有说明回滚成功。这个技巧在答辩时很加分因为它证明你考虑到了数据一致性而不是只会写增删改查。我自己的习惯是凡是涉及两张表以上的写操作一律用事务包起来宁可多写几行代码也不留后悔药。希望帮到你。本文还有配套的精品资源点击获取