简介:基于Qt与OpenXLSX实现的库存管理系统源码,适合计算机相关专业学生、Qt初学者,以及需要快速构建桌面数据管理工具的开发者。系统通过ODBC连接MySQL数据库,图形化界面支持商品信息的增删改查,并实现入库、出库时的库存数量自动更新,还能将库存数据导出为Excel文件,能够直接用于课程设计、毕业设计或小型企业库存管理场景的二次开发。资源包共109个文件,以hpp头文件、cpp源文件、ui界面文件为主,另含ico图标、qss样式、sql数据库脚本、cmake构建配置以及第三方库源码,压缩包整体仅472KB,目录划分清晰,便于按模块阅读和修改。目前已有163人学习下载,具备一定参考热度,可作为同类项目的起步模板。源码完整展示了OpenXLSX对Excel文件的读写流程、Qt界面与数据库的交互方法、界面样式定制,以及业务逻辑与数据持久化的分层组织方式,对理解桌面应用开发具有实际帮助。
1. 为什么库存管理系统要用Qt加OpenXLSX而不是用网页
这个项目最初是从一个课程设计需求演化来的:要在Windows桌面端管理商品入库、出库、查询,还要一键把库存表导出成Excel交给财务。用网页做要部署服务端,用MFC写界面又太旧,最后选了Qt + OpenXLSX。Qt负责界面和MySQL交互,OpenXLSX负责xlsx读写,两者一组合,整个系统不依赖Office安装也能导出Excel。源码里能看到OpenXLSXConfig.cmake、pugixml.cpp、XLDocument.cpp这些文件,说明是把OpenXLSX作为第三方库直接编进工程的。适合正在做课设、或者想把现有MFC/Winform库存系统迁到Qt的人参考;也适合想搞清楚OpenXLSX内部封装层次的读者。关键是这个组合把“数据库里结构化数据”与“Excel表格文件”之间的转换变得非常直接,代码量比想象中少。
2. 源码结构拆解:OpenXLSX在Qt项目里到底扮演什么角色
2.1 从文件名认清OpenXLSX的封装层次
拿到源码包先别急着编译,把OpenXLSX相关的几个cpp文件对应到xlsx文件结构上,后面排错会快很多。xlsx本质上是一个zip压缩包,里面有[Content_Types].xml、xl/workbook.xml、xl/worksheets/sheet1.xml、xl/sharedStrings.xml等文件。OpenXLSX在内存里把这些XML分别封装成对象,源码文件名几乎可以直接映射到xlsx部件。
| 源码文件 | 对应的xlsx部件 | 主要职责 |
|---|---|---|
| XLDocument.cpp | 整个xlsx包 | create/open/save,维护全局类型与关系 |
| XLWorkbook.cpp | xl/workbook.xml | 工作表集合、sheet顺序、工作表名称 |
| XLSheet.cpp | xl/worksheets/sheet*.xml | 单元格区域、行列操作入口 |
| XLRowData.cpp | sheet XML里的<row> | 一行内的单元格数据容器 |
| XLCellValue.cpp | <c>节点的值和类型 | 数值、字符串、布尔、公式的存放 |
| XLRow.cpp | 对行的封装 | 行高、行索引、合并单元格等 |
| XLRelationships.cpp | workbook.xml.rels | 工作表与文件的映射关系 |
| XLProperties.cpp | docProps/core.xml | 作者、标题、修改时间等属性 |
这个层析和Qt的模型/视图分离思路是相反的:OpenXLSX是直接对文档对象操作,没有signal/slot,也不维护界面状态。所以你的库存管理界面里,表格控件(如QTableView)和OpenXLSX之间必须自己写一个数据搬运层,把数据库读到的行映射到XLRowData,再写入XLSheet。如果直接把QSqlQuery的循环结果塞给单元格,也可以,但一旦字段顺序变化,维护成本会立刻上来。
2.2 pugixml:OpenXLSX不使用Qt XML的原因是轻量
OpenXLSX的内部XML解析用的是pugixml.cpp,这是一个单文件、无依赖的XML解析库。为什么不用QXmlStreamReader?主要原因是OpenXLSX设计上不要求必须有Qt,它可以独立用在纯C++项目里,所以内部不能绑定QDomDocument或QXmlStreamReader。pugixml是DOM模型,读一份几百KB的sheet XML完全没有压力,而且对命名空间处理比早期Qt XML模块更宽松。源码里pugixml.cpp会参与完整编译,如果你在链接时遇到pugixml::xml_node相关未解析符号,多半是漏了这个文件,或者混用了其他版本的pugixml。我一般会把OpenXLSX的include目录和pugixml.cpp一起放进一个静态库目标,这样Qt工程里只需要引用一个库,不需要关心内部依赖。
还要注意一点:pugixml默认把XML声明编码成UTF-8,这对xlsx来说是正确选择。Excel的sheet XML本身就是UTF-8带BOM或者不带BOM都允许,pugixml不写BOM也不影响Excel打开。如果你通过stringstream把单元格内容转来转去,反而会引入编码问题。所以和OpenXLSX打交道时,尽量用它的API,不要手动去解析同一个sheet XML,否则数据写回时会和内存里的DOM状态冲突。
2.3 CMake里接上OpenXLSXConfig.cmake
项目里出现OpenXLSXConfig.cmake,说明OpenXLSX是预先构建过的,并且导出了CMake配置。这样你在自己的CMakeLists.txt里写:
cmake_minimum_required(VERSION 3.16) project(inventory LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) find_package(Qt5 REQUIRED Widgets Sql) find_package(OpenXLSX REQUIRED) add_executable(inventory main.cpp ...) target_link_libraries(inventory PRIVATE Qt5::Widgets Qt5::Sql OpenXLSX::OpenXLSX )find_package(OpenXLSX REQUIRED)会去CMAKE_PREFIX_PATH指向的目录里找OpenXLSXConfig.cmake。如果构建时提示找不到OpenXLSX,先确认OpenXLSX的安装目录确实在CMAKE_PREFIX_PATH里,不要在CMakeLists里手写绝对路径。OpenXLSX需要C++17,所以CMAKE_CXX_STANDARD不能低于17;用Qt 5.15.2配合MSVC2019_64工具链时,OpenXLSX也建议用同一套编译器编译,否则STL版本不同容易在运行时崩溃。
OpenXLSX的头文件内部用到std::filesystem,老版本MSVC或GCC 8以下的库会缺符号。所以在Linux上用Qt 5.15加GCC 9要保证libstdc++版本至少是9;在Windows上,最简单的做法是直接用项目里已经编好的OpenXLSX静态库,别自己再编一遍。静态链接后发布exe时不需要带OpenXLSX的dll,省掉很多麻烦。
3. 数据库交互:ODBC连接与商品/库存CRUD实现
3.1 MySQL ODBC连接字符串与QODBC驱动检查
这个系统的数据库访问走的是ODBC,在Qt里就是QSqlDatabase::addDatabase("QODBC")。连接MySQL时有两个选择:一是先在Windows的odbcad32.exe里建好DSN,二是直接写连接字符串。我倾向于用连接字符串,因为部署时不用在每台机器上创建DSN,改一处配置就行:
QString connStr = QStringLiteral( "DRIVER={MySQL ODBC 8.0 Unicode Driver};" "SERVER=127.0.0.1;PORT=3306;" "DATABASE=inventory;UID=root;PWD=123456;" "CHARSET=utf8mb4;OPTION=3;" ); QSqlDatabase db = QSqlDatabase::addDatabase("QODBC", "inventory"); db.setDatabaseName(connStr); if (!db.open()) { qWarning() << "open db failed:" << db.lastError().driverText() << db.lastError().databaseText(); }QODBC驱动是Qt自带插件,位置在plugins/sqldrivers/qodbc.dll里,不是MySQL安装提供的。部署时如果只拷贝exe不拷贝sqldrivers目录,程序会报“QSqlDatabase: QODBC driver not loaded”。出现这个错误时,用QSqlDatabase::drivers()打印一下当前可用驱动名,能立刻判断插件有没有被Qt找到。连接字符串里的OPTION=3是MySQL ODBC驱动常见的连接选项,等于CLIENT_MULTI_STATEMENTS和CLIENT_FOUND_ROWS的叠加;去掉也没关系,但保留能兼容一些老版本MySQL服务器的行数返回处理。实际连接时如果中文乱码,优先检查CHARSET是不是utf8mb4,其次检查表字段的排序规则,两者必须匹配。
3.2 商品添加、修改、删除的SQL参数绑定
商品管理本质上就是对goods表做增删改查。关键不只是会写INSERT语句,而是要用prepare/bindValue绑定参数,避免用户输入的单引号破坏SQL。下面这段是添加商品的典型写法:
QSqlQuery query(db); query.prepare("INSERT INTO goods " "(gid, name, spec, unit, stock, safe_stock) " "VALUES (?, ?, ?, ?, 0, ?)"); query.addBindValue(gid); query.addBindValue(name); query.addBindValue(spec); query.addBindValue(unit); query.addBindValue(safeStock); if (!query.exec()) { qWarning() << "insert error:" << query.lastError().text() << query.lastQuery(); db.rollback(); return false; }addBindValue按顺序替换SQL里的?,避免字符串拼接。注意stock初始值直接写在SQL里为0,新商品入库默认是0,再由入库操作用UPDATE累加。修改商品时也是同样的套路,唯一容易踩坑的是WHERE条件要绑定商品ID,而且最好先查一下记录是否存在;如果ID不存在,执行UPDATE会影响0行,界面不能提示成功。我一般会加上query.numRowsAffected()判断,等于0时直接返回“未找到该商品”。删除操作我会在界面上做二次确认,数据层则在删除前先查库存,如果stock不为0就拒绝删除,避免账面库存凭空消失。
商品查询这里也顺手说一下,输入商品ID或名称模糊搜索是常见需求:
QSqlQuery query(db); query.prepare("SELECT gid, name, spec, stock FROM goods " "WHERE gid = ? OR name LIKE ?"); query.addBindValue(gid); query.addBindValue("%" + keyword + "%"); query.exec();LIKE的模糊匹配在使用绑定值时,需要用字符串拼接把百分号加到keyword前后,注意不要直接写LIKE ?然后bind进%?%,那样会被当成字面量。如果商品数量超过几万条,建议在gid和name上建索引,否则每次查询都是全表扫描,界面会卡顿。
3.3 入库出库时库存数量更新的并发考虑
入库和出库对应stock字段的增减,但不要用“先SELECT再UPDATE”的两步写法,因为两个窗口同时操作时会出现读到旧值的竞态。常见做法是把增减合并到一条UPDATE里:
bool updateStock(QSqlDatabase &db, const QString &gid, int delta) { QSqlQuery query(db); query.prepare("UPDATE goods SET stock = stock + ? WHERE gid = ?"); query.addBindValue(delta); query.addBindValue(gid); return query.exec(); }入库时delta=+n,出库时delta=-n,这样数据库在单条语句内完成加减,不会受之前SELECT结果影响。如果还想要出库时防止库存变成负数,就在WHERE里加条件:UPDATE goods SET stock = stock - ? WHERE gid = ? AND stock >= ?,执行后看numRowsAffected()是否为0,为0就是库存不足。整个入出库操作最好放在事务里,因为库存更新和流水记录必须同时成功:
db.transaction(); bool ok = updateStock(db, gid, delta); if (ok) { ok = insertFlow(db, gid, type, delta, remark); } if (ok) { db.commit(); } else { db.rollback(); }这里事务的好处是断电或程序异常时不会出现库存变了但流水没记的现象。Qt的QSqlDriver::hasFeature(QSqlDriver::Transactions)可以用来确认当前ODBC驱动是否支持事务,MySQL ODBC默认支持。如果你用的表是MyISAM引擎,事务会静默失效,所以建表时优先用InnoDB。界面这里还有个细节:入库和出库界面一般要填操作员和备注,这些字段属于单据信息,也应该在同一个事务里写入到flow表,只更新数量不带单据是很多库存系统后期对不上账的原因。
4. 数据导出Excel:OpenXLSX实战与遇到过的坑
4.1 用XLDocument创建和保存xlsx的最小代码
把库存数据导出成Excel是本项目的核心卖点。用OpenXLSX写一个最小文件只需要几行:
#include <OpenXLSX/OpenXLSX.hpp> using namespace OpenXLSX; XLDocument doc; doc.create("inventory_export.xlsx"); auto wks = doc.workbook().worksheet("Sheet1"); wks.cell("A1").value() = "商品ID"; wks.cell("B1").value() = "商品名称"; wks.cell("C1").value() = "库存数量"; wks.cell("D1").value() = "安全库存"; doc.save(); doc.close();doc.create()会创建一份默认含一个Sheet1的xlsx包,不是建空zip再手写XML。worksheet("Sheet1")返回XLWorksheet对象,cell("A1").value()返回的是XLCellValue代理,直接赋值成字符串或数字都行。这一步的关键是doc.save()必须在close()之前做,先close再save会重新打开文件,容易丢最后一次修改。另外,doc.create()传入路径时如果文件已存在,OpenXLSX的默认行为是抛异常,所以导出前先QFile::remove(targetPath)。
4.2 自定义保存路径与中文文件名
系统里用户一般通过QFileDialog::getSaveFileName选择保存路径,这里会碰到Windows中文路径问题。Qt5下QString转std::string直接.toStdString()得到UTF-8编码,而老版本OpenXLSX内部调用std::ofstream时在Windows上按ANSI码页解释路径,中文文件名很容易变成乱码或打不开。我的处理方式是先做一次路径转换:
QString target = QFileDialog::getSaveFileName( this, "导出库存", QDir::homePath() + "/库存导出.xlsx", "Excel 文件 (*.xlsx)"); #if defined(Q_OS_WIN) std::string path = target.toLocal8Bit().constData(); #else std::string path = target.toUtf8().constData(); #endif if (QFile::exists(target)) QFile::remove(target); doc.create(path);用toLocal8Bit()转换后,在中文版Windows上能正常创建带中文名的xlsx。如果以后升级到Qt6或新版OpenXLSX,可以直接用std::filesystem::path来处理,但目前这个写法最稳。QFileDialog返回的路径分隔符是/,不用再转成\,std::ofstream两种分隔符都能处理。另一个我踩过的坑是用户选择了带空格或“.”的目录,这时路径里的尾随空格可能被ofstream忽略,所以导出后最好立刻用QFileInfo::exists()验证一下文件是否真的生成。
4.3 从数据库到Excel的数据映射与单元格类型
查询结果写入Excel时,建议按查询字段顺序维护一个列偏移映射,而不是硬编码列号。这样数据库加字段时只改一个数组,导出代码不用大动:
QSqlQuery q(db); q.exec("SELECT gid, name, spec, stock, safe_stock FROM goods"); int row = 2; // 第一行是表头 while (q.next()) { wks.cell(row, 1).value() = q.value(0).toString().toStdString(); wks.cell(row, 2).value() = q.value(1).toString().toStdString(); wks.cell(row, 3).value() = q.value(2).toString().toStdString(); wks.cell(row, 4).value() = q.value(3).toInt(); wks.cell(row, 5).value() = q.value(4).toInt(); row++; }OpenXLSX写入时会根据XLCellValue的类型决定Excel单元格类型:int写进去是数值型,const char*写进去是字符型。如果拿toString()写数字,Excel里会出现绿色小三角提示“数字以文本形式存储”,后续公式统计会忽略它们。所以库存、安全库存这类数字字段,一定要用toInt()或toDouble()再赋值。我一般还会在最右侧加一列汇总,用公式字符串而不是预先算好的值:
wks.cell(row + 1, 4).value() = QString("=SUM(D2:D%1)").arg(row - 1).toStdString();不过要记住,OpenXLSX只是把公式文本写进XML,不会计算结果。用户用Excel打开时才会重算;如果用户用WPS或某些在线预览工具打开,公式结果可能不显示。这是导出类工具常见的“公式写进去但打开没值”的原因,不是代码写错。
4.4 常见异常:sharedStrings.xml和单元格引用的边界
如果导出的xlsx在Excel里提示“文件损坏,需要修复”,大概率是sharedStrings没有正确写入,或者单元格地址超出工作表允许范围。OpenXLSX会自己管理sharedStrings,不需要手动碰;但如果你同时用pugixml改过xl/sharedStrings.xml,就会和OpenXLSX的内存状态不一致,保存时互相覆盖。另一个容易踩的坑是单元格列号超过Z:用cell(1, 28)不会报错,但OpenXLSX不同版本的行列索引基数不一样,早期版本0基,后来改成1基。我的习惯是只写带列号的cell("B2")形式,避免搞混下标。
5. 构建打包与验证:Qt 5.15.2环境下让这个系统落地
5.1 CMake配置与Qt下载建议
项目里的Qt环境建议用5.15.2 msvc2019_64。下载Qt时如果离线安装包不好找,可以用国内镜像站,把在线安装器的--mirror指向镜像地址。安装时勾选MSVC 2019 64-bit组件,如果界面里用了图表再勾Qt Charts。CMake配置也要保持和Qt一致的架构:
cmake -DCMAKE_PREFIX_PATH="D:/Qt/5.15.2/msvc2019_64;D:/thirdparty/OpenXLSX" ..CMAKE_PREFIX_PATH里同时包含Qt和OpenXLSX,两者用分号隔开。如果之前配置过其他版本的Qt,最好先清空CMakeCache.txt,避免编译器或Qt模块路径错乱。源码包里如果自带了OpenXLSXConfig.cmake,说明OpenXLSX已经构建完成,不需要再单独编译pugixml.cpp。
5.2 windeployqt打包与ODBC运行时
发布时用Qt自带的windeployqt:
windeployqt inventory.exe --release --no-translations这个命令会把Qt5Widgets.dll、Qt5Sql.dll和sqldrivers/qodbc.dll拷到exe旁边。但MySQL的ODBC驱动本体是系统级的,目标机器上需要装MySQL Connector/ODBC。如果不想装,也可以把libmysql.dll拷到exe目录,但更稳妥的做法是在部署说明里写清要求。运行时如果提示qt_qpa_platform_plugin_path相关错误,说明platforms/qwindows.dll没有和exe放在一起,检查exe目录下是否有完整的platforms文件夹。我见过很多次“自己电脑能跑,拷到别人电脑报错”的问题,先查sqldrivers里有没有qodbc.dll,再查ODBC数据源管理器里能不能看到MySQL驱动,这两个点能排除绝大多数环境问题。
5.3 快速验证导出的xlsx是否正常
拿到导出的文件,别急着双击打开。先用命令行验证XML结构:
python -c "import zipfile; z=zipfile.ZipFile('库存导出.xlsx'); print(z.read('xl/worksheets/sheet1.xml').decode('utf-8'))"能看到<sheetData>里的<row>和<c>节点,说明xlsx包是完整的。再检查sharedStrings里有没有字符串:
python -c "import zipfile; z=zipfile.ZipFile('库存导出.xlsx'); print(z.namelist())"如果xl/sharedStrings.xml存在且内容里有商品名称,就说明字符串写出去了。这个方法比用Excel打开快得多,适合在自动化测试或CI里加一步校验。日常开发时我还会临时改一下导出代码,把生成的xlsx路径打印到控制台,再配合QFileInfo检查文件大小,能比肉眼判断更早发现文件未生成或为空的问题。
本文还有配套的精品资源,点击获取