ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Qt读取Excel的三种实现方案对比与实战

Qt读取Excel的三种实现方案对比与实战 简介面向Qt开发者的Excel操作封装示例基于QAxContainer模块通过ActiveX与Excel交互适合需要在Qt应用中读取、写入或批量更新Excel数据的开发者尤其适用于数据导入导出、报表生成等桌面工具场景。压缩包共6个文件包括ExcelManger.h/cpp封装类、Qt工程文件pro以及两个xlsx示例表格整体仅16KB轻量易读可直接用Qt Creator打开并对照工程调用逻辑。目前该资源已有1823人学习浏览适合正在做Qt数据交互或办公自动化开发的读者参考。资源提供完整源码和可运行的示例数据覆盖创建Excel应用实例、打开或新建工作簿、按名称或索引访问工作表、读写单个及批量单元格、处理大量数据时的批量写入策略、设置字体颜色和对齐方式、保存并关闭工作簿等高频操作同时给出异常处理与资源释放思路便于快速掌握QAxObject、dynamicCall等ActiveX交互用法稍作封装即可迁移到实际项目中。1. 从 QtExcel 压缩包说起Qt 读 Excel 表格的三条技术路线做 Qt 桌面应用十有八九会遇到「用户丢过来一个 Excel要你把它读进程序里」的需求导入产品表、读取配置文件、报表回显。标题里的「QT操作ex」「qt 数据 excel」指的正是同一件事在 Qt 应用中读取 Excel 表格。如果你手上拿到的是别人打包好的 QtExcel.rar先搞清楚里面装的往往是三种方案之一开源库 QXlsx、Windows 上的 COM 调用QAxObject、以及自己解包 xlsx 的 XML。选哪条取决于运行平台、目标机器是否装了 Office、表格大小和读写需求。这一篇按「先能跑通最小代码、再处理边界和坑」的顺序把三条路都过一遍最关键的结论是纯数据读取优先 QXlsx要求实时公式结果才上 COM发布环境受限就退回 XML 或 CSV。2. 用 QXlsx 读 xlsx 表格最小工程与按行列取值2.1 为什么开源方案先看 QtExcel 生态里的 QXlsxQXlsx 是 Qt 生态里最常用的 Excel 读写库一个QXlsx::Document对象对应一个 xlsx 文件API 风格跟 Qt 容器一致读单元格返回QVariant写单元格直接write(row, col, value)。它只依赖 Qt 的 core 和 gui 模块Qt5、Qt6 都能编跨平台而且目标机器不需要装 Office——这些正好对上「qt 数据 excel」这类检索词的诉求把 xlsx 当成普通数据文件处理而不是当成 Excel 应用去操作。它属于开源宽松许可商用前自己看一眼仓库里的 LICENSE 确认版本条款这个库没有把 Excel 封装成「数据库式」接口读大表时是逐单元格访问第 5 章会讲怎么绕开逐格读的性能问题。如果你手上的 QtExcel.rar 里只有源码和 demos一般就是把它当作静态库编进项目的形态。2.2 把 QXlsx 编进 Qt 工程源码直接挂在 .pro 里常见做法是直接拿源码编把 QXlsx 源码目录放到工程子目录后在.pro里 include 它的 pri 文件QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets CONFIG c11 include($$PWD/QXlsx/QXlsx.pri)如果用 CMake就把源码目录加进 target 的源文件列表或者用 vcpkg、conan 装现成包。.pro方式最直接断点能进到库源码里读表格时想确认某个单元格的原始格式类型也方便。QXlsx.pri内部已经处理了头文件路径和源文件列表所以代码里只需要#include xlsxdocument.h #include QDebug注意include的路径要跟你放仓库的位置对应。项目里报「找不到 xlsxdocument.h」几乎都是$$PWD写错或者源码目录层级不对先看编译器的 include 提示路径而不是去改 Qt 的全局环境。2.3 读取整个 sheet 的最小代码假设要读的文件是orders.xlsx第一个 sheet 是订单表表头在第 1 行。完整读取逻辑如下#include QCoreApplication #include QDebug #include xlsxdocument.h int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QXlsx::Document xlsx(orders.xlsx); if (!xlsx.load()) { qWarning() load fail; return 1; } QXlsx::CellRange range xlsx.dimension(); qDebug() rows: range.lastRow() cols: range.lastColumn(); for (int row 1; row range.lastRow(); row) { QStringList line; for (int col 1; col range.lastColumn(); col) { QXlsx::Cell *cell xlsx.cellAt(row, col); if (!cell) { line ; continue; } QVariant v cell-readValue(); if (!v.isValid() || v.isNull()) { line ; continue; } if (cell-isDateTime()) { line cell-dateTime().toString(yyyy-MM-dd HH:mm:ss); } else { line v.toString(); } } qDebug() line.join(,); } return 0; }代码逻辑dimension()返回有数据的区域lastRow()和lastColumn()给出行列边界cellAt(row, col)只取指定位置的单元格指针不存在的格子返回nullptr所以先判空readValue()拿到的是QVariant统一转字符串时先判断是不是日期类型是则走dateTime()得到QDateTime再按格式输出避免时间被转成「1899 历元」那种整数序列号。参数说明里最容易忘的是dimension的语义它返回的是「包含内容的区域」边界而不是工作表的最大行列号。如果某个空表只是被设置了底色cellAt返回的指针非空但readValue()无效所以指针和值要同时判。另外行和列都从 1 开始计数跟 Excel 界面的行号列号一致别用成 0 基下标。2.4 日期、空值、合并单元格三个坑的应对情况现象处理方式日期单元格readValue()返回 44701 这类序列号先判cell-isDateTime()用dateTime()取QDateTime空单元格cellAt返回nullptr或值无效判空后补默认值不要直接toString()合并单元格只有左上角有值其余格为空按「取到第一个非空值即视为区域值」处理数值精度double 显示成 1.23456789012345E16整数态转qint64输出大数用g格式化日期序列号 44701 对应 2022 年前后的日期Excel 从 1899-12-30 起算天数。QXlsx 在能识别日期格式时会把readValue()给成QDateTime但自定义格式的单元格仍可能落回 double所以isDateTime()这个分支必须存在。合并单元格没有专门 API 去展平xlsx 规范里本来就是只有左上角格子有值业务上一般遍历时取到非空值即视为覆盖了整个区域。提示QXlsx 读取时不会真的打开 Excel 进程它只是解包 xlsx 的 XML所以文件正被 Excel 占用时也能读代价是文件损坏时错误信息比较模糊要自己做好校验。3. Windows 上用 QAxObject 调 Excel COM 读取表格3.1 什么时候必须走 COM 而不是解析文件xlsx 直接解析拿不到「打开时重新计算」的结果。比如公式单元格A1B1QXlsx 读出来的要么是保存时的缓存值要么是公式字符串本身而用 QAxObject 启动本机 Excel 再取值拿到的是实时计算结果公式、格式、数据验证这些「Excel 应用层行为」都会触发。所以当目标机器是 Windows 且装了完整 Office业务要求「跟用户在 Excel 里看到的一致」时就选 COM。代价同样明确发布环境必须装 Excel没装直接失败每次读取都要拉起一个 Excel 进程速度和稳定性都不如解析库。很多项目是「优先 QXlsx 读数据遇到公式列再切 COM 补算」的组合路由而不是全身心押注某一种。3.2 最小可跑的 QAxObject 读取代码先在.pro里加模块QT core gui axcontainer然后是最简读取函数#include QAxObject #include QDebug void readExcelByCom(const QString filePath) { QAxObject *excel new QAxObject(Excel.Application, nullptr); if (excel-isNull()) { qWarning() no excel installed; return; } excel-setProperty(Visible, false); excel-setProperty(DisplayAlerts, false); QAxObject *workbooks excel-querySubObject(Workbooks); QAxObject *workbook workbooks-querySubObject(Open(const QString, int, bool), filePath, 0, true); QAxObject *sheets workbook-querySubObject(Worksheets); QAxObject *sheet sheets-querySubObject(Item(int), 1); QAxObject *used sheet-querySubObject(UsedRange); int rowCount used-querySubObject(Rows)-property(Count).toInt(); int colCount used-querySubObject(Columns)-property(Count).toInt(); for (int r 1; r rowCount; r) { QStringList line; for (int c 1; c colCount; c) { QAxObject *cell sheet-querySubObject(Cells(int,int), r, c); QVariant v cell-property(Value); if (v.canConvertQDateTime()) { line v.toDateTime().toString(yyyy-MM-dd HH:mm:ss); } else { line (v.isNull() ? QString() : v.toString()); } } qDebug() line.join(,); } workbook-dynamicCall(Close(bool), false); excel-dynamicCall(Quit()); delete used; delete sheet; delete sheets; delete workbook; delete workbooks; delete excel; }逻辑说明Open的三个参数分别是文件名、UpdateLinks、ReadOnly第二个参数传 0 表示不更新外部链接第三个传true只读打开避免程序意外改动了文件内容。UsedRange等价于用户按 CtrlEnd 到达的区域Rows和Columns的Count属性就是行列数。单元格取值用Cells(int,int)property(Value)拿QVariant日期会以QDateTime形态出现所以先做canConvertQDateTime判断。这里还有个容易忽略的点new QAxObject(Excel.Application)时如果目标机没装 ExcelisNull()为真程序必须给出明确错误提示而不是继续往下跑导致段错误或 COM 异常。3.3 释放顺序和 Excel 进程不退出COM 方式最常见的现象是程序退出后任务管理器里还挂着 EXCEL.EXE或者第二次打开同一文件时提示「文件正被占用」。根因是QAxObject释放顺序和 Excel 内部引用计数对不上。释放时要先关工作簿、再Quit()最后把used、sheet、sheets、workbook这些中间对象依次delete顺序反了会造成残留。参数/属性取值作用Visiblefalse不显示 Excel 窗口后台静默运行DisplayAlertsfalse屏蔽「是否保存」等弹窗Open第二参0不更新外部链接Open第三参true只读打开不改原文件提示排错时先看任务管理器有没有残留 EXCEL.EXE。有残留就说明这一轮的 Close/Quit 没走完整优先检查是不是提前 delete 了used或sheet对象。调试阶段可以把Visible临时改成true肉眼确认 Excel 到底停在哪一步尤其是 COM 调用抛异常的时候能直接看到当前活动单元格是哪一格。正式环境务必改回false否则用户屏幕上会反复闪出 Excel 窗口。4. 不装 Office 的兜底直接拆 xlsx 的 XML 与 CSV 编码4.1 xlsx 其实是个 zip 包三个关键文件先认齐xlsx 文件解压后是一堆 XML读数据真正要碰的就几个文件。很多人在「qt 读取表格」这条路上卡住是因为不知道单元格里存的可能是「索引」而不是「字符串」。文件路径作用什么时候用xl/workbook.xmlsheet 名称与顺序需要按 sheet 名取数据时xl/worksheets/sheet1.xml单元格引用、值、样式索引读任意单元格值xl/sharedStrings.xml共享字符串表单元格ts时查这里xl/styles.xml数字格式、日期格式定义判断某列是日期还是数字这里最反直觉的一点c rB2 tsv3/v/c表示 B2 单元格是共享字符串内容要去sharedStrings.xml里下标为 3 的si取。只有ts或tinlineStr才这样纯数字单元格的v直接就是数值字符串没有t属性。4.2 用 QXmlStreamReader 解析的示例代码不引入第三方库、自己解析时建议用QXmlStreamReader而不是 DOM前者流式处理大表内存可控。先解析共享字符串表#include QFile #include QXmlStreamReader #include QStringList QStringList parseSharedStrings(const QString path) { QStringList result; QFile file(path); if (!file.open(QIODevice::ReadOnly)) return result; QXmlStreamReader xml(file); QString current; while (!xml.atEnd()) { xml.readNext(); if (xml.isStartElement() xml.name() si) { current.clear(); } else if (xml.isStartElement() xml.name() t) { current xml.readElementText(); } else if (xml.isEndElement() xml.name() si) { result.append(current); } } return result; }逻辑说明si里可能有多个r运行块每个运行块内有自己的t所以在si开始时清空current遇到t就追加文本到/si收尾存入列表。这样带富文本格式的字符串也能拼出完整内容而不是只取第一段。解析 sheet 时同样流式走c节点看t属性决定值是数字、共享字符串还是内联字符串。这种方案的适用边界只读不写、格式要求不高、想彻底脱离外部库。代价是日期格式识别和样式解析都要自己写styles.xml里numFmtId14 到 22 是内建日期格式自定义格式串一般含y、m、d字符判断逻辑得维护一份规则表。4.3 CSV 兜底读取与 GBK/UTF-8 编码如果数据源只是导出用的 CSV比 XML 更简单但编码是最大的坑。Windows 上 Excel 导出的 CSV 常见是 GBK 或 GB18030直接按 UTF-8 读会满屏乱码QFile file(data.csv); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return; QTextStream in(file); in.setCodec(GBK); // Windows 导出的 CSV 常用 GBK while (!in.atEnd()) { QString line in.readLine(); // 按逗号拆列时注意带引号的字段 }代码要点QTextStream::setCodec在 Qt5 里直接传编码名字符串Qt6 里编解码器 API 改成QStringConverter推荐改用in.setEncoding(QStringConverter::Utf8)或对应的 GBK 编码。判断文件到底是 UTF-8 还是 GBK 的常见做法是读前三个字节看有没有 BOM无 BOM 时先按 UTF-8 严格解码失败再退回 GBK。CSV 还有个隐蔽问题字段里含逗号、换行或引号时Excel 导出会用双引号把整个字段包起来简单的split(,)会把一个字段拆成两列至少要处理「引号内逗号不拆分」的规则否则列对齐全乱。5. 让 Qt 读取 Excel 表格更稳的三个实用技巧5.1 大表别逐格动态构造 QVariantQXlsx 里read(row, col)每次都会走一次查找和QVariant构造几万行逐格读会明显变慢。常见做法是先拿到dimension循环里用cellAt(row, col)取Cell*判空后直接读cell-value()绕开重复构造的开销。再进一步如果只需要某几列就按列过滤而不是全表扫描排序、去重逻辑放在 Qt 容器里做别依赖 Excel 公式。5.2 统一把单元格转成业务字符串的解析函数Excel 用户在用 Qt 读取表格时最常抱怨的就是「时间变成数字」「金额变科学计数法」。归一化处理时加一个开关函数QString cellToDisplay(const QXlsx::Cell *cell) { if (!cell) return QString(); QVariant v cell-value(); if (!v.isValid() || v.isNull()) return QString(); if (cell-isDateTime()) { return cell-dateTime().toString(yyyy-MM-dd HH:mm:ss); } if (v.type() QVariant::Double) { double d v.toDouble(); if (qFuzzyCompare(d, std::round(d)) qAbs(d) 1e15) return QString::number(qint64(d)); // 整数时不带小数 return QString::number(d, g, 15); } return v.toString(); }这个函数把日期、整数型 double、普通字符串三种最常见形态收敛成用户预期的样子。qFuzzyCompare判断整数时注意大数超过 double 精确范围就别硬转qint64直接g格式输出保留有效位数。5.3 读完后用「三连校验」快速确认没读歪花时间调解析不如读完立刻验证。我的习惯是打印行列维度、打印第一行表头、统计每列非空数量。非空统计能快速发现整列错位——如果某列空值数量异常多基本是 sharedStrings 的索引对错了列QVectorint nonEmpty(colCount, 0); for (int c 1; c colCount; c) { for (int r 1; r rowCount; r) { QVariant v xlsx.read(r, c); if (v.isValid() !v.isNull()) nonEmpty[c - 1]; } } qDebug() non-empty per col: nonEmpty;对比 Excel 原表里每列数据的肉眼数量误差超过 5% 就去查那列的格式和合并单元格。最后一招是把解析结果导出一个临时 CSV用 Excel 打开对比能同时验证编码和列映射比看日志直观。本文还有配套的精品资源点击获取
返回列表