
简介基于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.cppxl/workbook.xml工作表集合、sheet顺序、工作表名称XLSheet.cppxl/worksheets/sheet*.xml单元格区域、行列操作入口XLRowData.cppsheet XML里的row一行内的单元格数据容器XLCellValue.cppc节点的值和类型数值、字符串、布尔、公式的存放XLRow.cpp对行的封装行高、行索引、合并单元格等XLRelationships.cppworkbook.xml.rels工作表与文件的映射关系XLProperties.cppdocProps/core.xml作者、标题、修改时间等属性这个层析和Qt的模型/视图分离思路是相反的OpenXLSX是直接对文档对象操作没有signal/slot也不维护界面状态。所以你的库存管理界面里表格控件如QTableView和OpenXLSX之间必须自己写一个数据搬运层把数据库读到的行映射到XLRowData再写入XLSheet。如果直接把QSqlQuery的循环结果塞给单元格也可以但一旦字段顺序变化维护成本会立刻上来。2.2 pugixmlOpenXLSX不使用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需要C17所以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}; SERVER127.0.0.1;PORT3306; DATABASEinventory;UIDroot;PWD123456; CHARSETutf8mb4;OPTION3; ); 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找到。连接字符串里的OPTION3是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(); }入库时deltan出库时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. 数据导出ExcelOpenXLSX实战与遇到过的坑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_PATHD:/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自带的windeployqtwindeployqt 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; zzipfile.ZipFile(库存导出.xlsx); print(z.read(xl/worksheets/sheet1.xml).decode(utf-8))能看到sheetData里的row和c节点说明xlsx包是完整的。再检查sharedStrings里有没有字符串python -c import zipfile; zzipfile.ZipFile(库存导出.xlsx); print(z.namelist())如果xl/sharedStrings.xml存在且内容里有商品名称就说明字符串写出去了。这个方法比用Excel打开快得多适合在自动化测试或CI里加一步校验。日常开发时我还会临时改一下导出代码把生成的xlsx路径打印到控制台再配合QFileInfo检查文件大小能比肉眼判断更早发现文件未生成或为空的问题。本文还有配套的精品资源点击获取