Qt+OpenXLSX实现Excel库存管理系统的完整实践
2026/9/17 3:23:05 网站建设 项目流程

简介:这套源码实现了一个基于Qt和OpenXLSX的库存管理系统,适合正在学习Qt桌面开发、MySQL数据库集成以及Excel数据导出的开发者,也可作为课程设计或毕业设计的参考。系统以图形界面完成商品信息的增删改查、入库出库流程,并可通过ODBC连接MySQL持久化数据,同时调用OpenXLSX库将库存表导出为Excel文件,覆盖了典型中小型管理系统的核心功能。压缩包共含109个文件,大小仅472KB,核心代码以49个hpp头文件和29个cpp源文件为主,辅以5个ui界面文件、6个h文件以及1个sql数据库脚本,便于直接理解界面与逻辑的对应关系。另含OpenXLSX库的相关源码与配置,如XLDocument、XLWorkbook等底层实现,可辅助研究Excel读写机制的细节。目前已有163人学习该资源,适合用来参考项目分层、ODBC连接配置和Qt与第三方库的集成方式,快速搭建自己的库存管理原型,整体结构清晰,实用性强。

1. 为什么库存管理系统要选Qt和OpenXLSX这套组合

不少仓库的库存主数据并不在数据库里,而是躺在一份不断被多人另存的Excel表中。与其强行把数据迁到MySQL再做一套B/S管理后台,不如让桌面程序直接读写这份xlsx:Qt负责窗体、表格和交互,OpenXLSX负责把Excel文件读进内存再写回去。这样业务人员不改变日常留档习惯,程序侧又能统一控制写入逻辑,保留逐行审计的可能。这套组合的实用边界很清晰:SKU在几千以内、单机或局域网共享目录、日操作量百次左右的中小库存场景。超过这个量级,Excel的文件锁和整表重写会拖垮效率,那是数据库该解决的问题。下面从数据模型开始,把一条能直接跑的Qt + OpenXLSX路径拆开讲。

2. 库存表的数据模型与OpenXLSX读写接口

2.1 库存Sheet怎么设计:从Excel列到C++结构体

规划库存表时,第一行必须固定为表头,且不要使用合并单元格,不要在同一列里混填文本和数字。程序读Excel时会把“行”映成一个InventoryItem对象,列顺序一旦变化,读取逻辑就要跟着改,因此表头列名建议作为校验锚点。

以最常见的库存表为例,建议至少包含下面这几列,顺序可以按业务调整:

Excel列表头名C++字段类型典型值
A编号skustd::stringSP-0001
B名称namestd::string不锈钢螺丝 M3x10
C分类categorystd::string标准件
D库存数量quantityint1200
E安全库存safeStockint500
F单价unitPricedouble0.05
G供应商supplierstd::string华东五金
H最后更新lastUpdatestd::string2025-01-12 10:30

数量、安全库存和单价不要用字符串保存,否则排序、求和和低库存判断都会变成字典序,出错很难查。日期列先以yyyy-MM-dd HH:mm格式存字符串,等业务需要跨天计算时再改成OpenXLSX真正的日期时间类型,起步阶段不必过度设计。

对应的C++结构体可以这样写:

#include <string> #include <vector> struct InventoryItem { std::string sku; std::string name; std::string category; int quantity = 0; int safeStock = 0; double unitPrice = 0.0; std::string supplier; std::string lastUpdate; bool isLowStock() const { return quantity <= safeStock; } };

这里把isLowStock()放在领域对象里,而不是在Qt界面层用if判断。原因是低库存规则可能从quantity < safeStock变成quantity <= safeStock * 0.8,集中在一处修改,UI和告警窗口都不用动,后续写单测也方便。

2.2 封装一个ExcelStore类:读取和写入都要经过它

OpenXLSX最核心的对象只有三个:XLDocument表示整个xlsx文件,XLWorkbook管理工作表集合,XLWorksheet操作具体某个Sheet。日常读写Excel的代码要尽量收拢到一个ExcelStore类里,不要让MainWindow直接拼Cell字符串,否则界面和文件格式耦合在一起,换存储介质时等于重写界面逻辑。

下面是一个典型的读取实现,按行把“库存”工作表映射成InventoryItem向量:

#include <OpenXLSX/OpenXLSX.hpp> #include <vector> using namespace OpenXLSX; class ExcelStore { public: std::vector<InventoryItem> readAll(const std::string& path); }; std::vector<InventoryItem> ExcelStore::readAll(const std::string& path) { XLDocument doc; doc.open(path); auto wks = doc.workbook().worksheet("库存"); std::vector<InventoryItem> items; // 从第2行开始,第1行是表头 for (int row = 2; row <= static_cast<int>(wks.rowCount()); ++row) { auto skuCell = wks.cell("A" + std::to_string(row)); if (skuCell.value().type() == XLValueType::Empty) { continue; } InventoryItem item; item.sku = skuCell.value().as<std::string>(); item.name = wks.cell("B" + std::to_string(row)).value().as<std::string>(); item.category = wks.cell("C" + std::to_string(row)).value().as<std::string>(); item.quantity = wks.cell("D" + std::to_string(row)).value().as<int>(); item.safeStock = wks.cell("E" + std::to_string(row)).value().as<int>(); item.unitPrice = wks.cell("F" + std::to_string(row)).value().as<double>(); item.supplier = wks.cell("G" + std::to_string(row)).value().as<std::string>(); item.lastUpdate= wks.cell("H" + std::to_string(row)).value().as<std::string>(); items.push_back(std::move(item)); } doc.close(); return items; }

这段代码有几个值得注意的地方。wks.cell("A" + std::to_string(row))用的是Excel坐标字符串,不是XLCellReference(row, col)。因为不同OpenXLSX小版本对行列下标的起点定义有差异,直接用坐标字符串最不容易踩0/1索引坑。读到空行就continue,避免Excel表格里残留的格式让rowCount()虚高。doc.close()放在函数末尾,防止文件句柄占住,导致后面保存时无法覆盖写。

写入逻辑同样封装在ExcelStore里,核心思路是先打开或新建文档,再逐格调用value()覆盖旧值,最后doc.save()。不要每写一个单元格就保存一次文件,xlsx本质是zip包,频繁保存会产生大量临时文件和IO开销。

3. 用Qt和OpenXLSX把增删改查接到Excel文件上

3.1 QTableWidget与内存模型的信号槽绑定

界面层推荐用QTableWidget承载库存条目,因为它可以直接把InventoryItem的字段按行填进表格,不需要额外维护QAbstractTableModel。业务量变大后要换回Model/View架构时,再单独抽象InventoryTableModel也不迟。

主窗口结构一般是这样:

#include <QMainWindow> #include <QTableWidget> #include <QPushButton> #include <vector> class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); private slots: void onAdd(); void onEdit(); void onDelete(); void onSave(); private: void refreshTable(); ExcelStore store_; std::vector<InventoryItem> items_; std::string currentFilePath_; QTableWidget *table_ = nullptr; };

refreshTable()把内存里的items_整体刷进QTableWidget。注意数量、单价这两列要设置成右对齐,或者让单元格存储数值而不是纯文本,否则按表头排序时会出现“1000排在200前面”的问题。

void MainWindow::refreshTable() { table_->setRowCount(static_cast<int>(items_.size())); table_->setHorizontalHeaderLabels({"编号", "名称", "分类", "数量", "安全库存", "单价", "供应商", "更新时间"}); for (int r = 0; r < static_cast<int>(items_.size()); ++r) { const auto& item = items_[r]; table_->setItem(r, 0, new QTableWidgetItem(QString::fromStdString(item.sku))); table_->setItem(r, 1, new QTableWidgetItem(QString::fromStdString(item.name))); table_->setItem(r, 2, new QTableWidgetItem(QString::fromStdString(item.category))); auto *qtyItem = new QTableWidgetItem(QString::number(item.quantity)); qtyItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); table_->setItem(r, 3, qtyItem); auto *safeItem = new QTableWidgetItem(QString::number(item.safeStock)); safeItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); table_->setItem(r, 4, safeItem); auto *priceItem = new QTableWidgetItem(QString::number(item.unitPrice, 'f', 2)); priceItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); table_->setItem(r, 5, priceItem); table_->setItem(r, 6, new QTableWidgetItem(QString::fromStdString(item.supplier))); table_->setItem(r, 7, new QTableWidgetItem(QString::fromStdString(item.lastUpdate))); } }

refreshTable()每次全量重建单元格会稍慢,但SKU几千以内时开销可以忽略。维护一个QTableWidgetItemitem_索引映射表是后话,现在这么做最直观。

3.2 新增、修改、删除和保存映射到哪些函数

增删改查按钮和实际业务函数的对应关系如下:

界面操作槽函数内存数据变化文件持久化时机
点击新增onAdditems_.push_back()用户点击保存后
选中行修改onEdit替换items_[row]用户点击保存后
删除一行onDeleteitems_.erase()用户点击保存后
保存onSave无,只读取当前内存调用store_.saveAll()

把保存动作和增删改分离是关键。每点一次新增按钮就自动存Excel,会让OpenXLSX频繁做文件的加载、解压、压缩和写回,用户一旦连续录入几十条,界面会卡到不可用。正确做法是内存中先积累变更,等用户明确点保存,再一次性写回。

void MainWindow::onDelete() { int row = table_->currentRow(); if (row < 0) { return; } items_.erase(items_.begin() + row); refreshTable(); } void MainWindow::onSave() { if (currentFilePath_.empty()) { QString path = QFileDialog::getSaveFileName(this, "保存库存表", "inventory.xlsx", "Excel文件 (*.xlsx)"); if (path.isEmpty()) return; currentFilePath_ = path.toStdString(); } try { store_.saveAll(currentFilePath_, items_); QMessageBox::information(this, "保存", "已写入Excel"); } catch (const std::exception& e) { QMessageBox::critical(this, "保存失败", e.what()); } }

这里onSave必须用try/catch包住,因为OpenXLSX打开一个被Excel占用的文件时,大概率会因为文件锁抛出异常。裸奔的代码会导致Qt程序直接崩溃,而不是弹一个友好的错误框。onAddonEdit可以用同一个QDialog表单,区别只是对话框初始数据是否来自当前行。

4. 从Excel导入到低库存预警:Qt界面与OpenXLSX的一次完整协作

4.1 打开Excel文件并刷新界面的完整流程

导入文件不是简单调一个readAll就结束。常见流程是:用户选择xlsx文件,程序校验文件能被OpenXLSX正常打开,读取所有库存条目,对数据做合法性检查,然后刷新表格并触发低库存预警。下面这段代码把这几步串在一起:

void MainWindow::openExcelFile(const QString& path) { try { auto items = store_.readAll(path.toStdString()); if (!validateItems(items)) { QMessageBox::warning(this, "数据校验", "表格中存在空编号、重复编号或负数库存,部分行可能无法正常使用。"); } items_ = std::move(items); currentFilePath_ = path.toStdString(); refreshTable(); checkStockAlerts(); } catch (const std::exception& e) { QMessageBox::critical(this, "打开失败", QString::fromStdString(e.what())); } }

这段逻辑里,validateItems先于items_赋值执行,是因为校验过程可能修改某些数据。例如把负数单价直接归零,或者给空名称补一个“未命名”。只有校验通过或按预定义规则修正后,数据才进入界面显示。

4.2 安全库存预警与非法数据拦截

校验规则不要写得太严,也不要太松。库存系统常见的脏数据主要是重复SKU、负库存、文本型数字和缺失更新时间。下列表格是一个实用的检查组合:

问题类型检查方法处理策略
重复SKU建立unordered_set扫描保留第一条,后续条目跳过
数量/安全库存负数直接比较< 0跳过该行并计入警告次数
单价小于0item.unitPrice < 0强制置为0,提示用户手工修正
名称或供应商为空判断字符串empty()允许为空,界面显示“未填写”
更新时间列缺失字符串为空读取时填入“未知”

校验函数可以写成返回bool,发生需要用户注意的问题就返回false,但程序不中断:

bool MainWindow::validateItems(std::vector<InventoryItem>& items) { std::unordered_set<std::string> seen; bool hasError = false; for (auto& item : items) { if (item.sku.empty() || !seen.insert(item.sku).second) { hasError = true; continue; } if (item.quantity < 0 || item.safeStock < 0) { hasError = true; } if (item.unitPrice < 0) { item.unitPrice = 0.0; hasError = true; } } return !hasError; }

低库存预警弹窗建议只汇总一次,不要逐条弹出。仓库里十条低库存警报如果连弹十个对话框,操作员会直接关闭程序。

void MainWindow::checkStockAlerts() { QStringList lowItems; for (const auto& item : items_) { if (item.isLowStock()) { lowItems << QString::fromStdString(item.sku + " " + item.name); } } if (!lowItems.isEmpty()) { QMessageBox::warning(this, "低库存预警", "以下条目低于安全库存:\n" + lowItems.join("\n")); } }

checkStockAlerts里不要同时读写Excel,直接遍历内存里的items_即可。它只是一次筛选操作,不需要外部依赖。

5. 拿到源码后先做这两件事:CMake集成和windeployqt发布验证

5.1 最小CMake配置里集成Qt和OpenXLSX

不管是自己新写项目,还是拿到一份Qt + OpenXLSX源码包,首先要把三方依赖关系理清。OpenXLSX是纯源码库,最常见的集成方式是用CMake把它编进来,而不是动态加载DLL。一个能用的CMakeLists接近下面这样:

cmake_minimum_required(VERSION 3.16) project(InventorySystem VERSION 1.0) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt6 REQUIRED COMPONENTS Widgets) # 三选一:系统已安装、源码包里自带、手动指定路径 # find_package(OpenXLSX REQUIRED) add_subdirectory(third_party/OpenXLSX) add_executable(InventorySystem main.cpp MainWindow.cpp ExcelStore.cpp ) target_link_libraries(InventorySystem PRIVATE Qt6::Widgets OpenXLSX::OpenXLSX )

如果源码包里已经带了third_party/OpenXLSX目录,直接使用add_subdirectory比让用户手动find_package省事得多。如果后台编译失败,先去检查OpenXLSX依赖的minizpugixml是否被一起拉进构建,这两个小库是OpenXLSX解析xlsx压缩包和XML的底层依赖。

5.2 用windeployqt做一次发布前的最小验证

Windows下发布Qt程序时,最常踩的坑就是exe拷贝到别的机器上提示找不到Qt6Widgets.dllQt6Core.dll。Qt官方提供的windeployqt工具可以自动扫描exe的依赖并拷贝对应DLL,命令很简单:

windeployqt --no-translations --release build/InventorySystem.exe

执行完检查发布目录,至少应包含InventorySystem.exeplatforms/qwindows.dllstyles目录。OpenXLSX如果以静态库方式编译,不会额外产生动态库,但如果选用动态版,记得把对应DLL放进同一目录。

发布前的验证脚本可以这样写:

cd dist ./InventorySystem.exe --help

程序能正常启动并退出后,再手动做一次“打开空表、新增一行、保存、用Excel重新打开”的闭环操作。这个动作能同时验证OpenXLSX写入和读取是否对称,比单测更能暴露实际问题。

最后检查发布目录里是否残留了多余的多语言翻译文件,只保留zh_CN.qm即可,其余语言包不会影响运行,但会让安装包体积变成两倍。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询