简介:面向Qt初学者以及需要快速实现Excel数据展示的开发者,这份封装示例围绕“读取Excel表格并呈现在TableWidget中”展开,适合课程设计、小型管理工具原型和日常数据查看等场景,也可作为理解Qt界面控件与数据解析协作方式的练手材料。压缩包共8个文件,约8KB,以3个cpp和2个h源码为主体,搭配1个ui界面文件、1个pro工程文件及1个user配置,工程结构与界面布局基本齐备,便于直接对照学习。已有3000人学习下载,说明其在Qt表格读写类需求中具备一定参考价值。读者可从中获取Excel读取逻辑的封装方式、TableWidget数据填充与界面联动的实现线索,以及工程组织与配置的基本范例;还能据此梳理从数据解析到控件展示的调用链,学习拆分数据处理与界面逻辑,并参考作者关于Qt5环境的说明完成配置与测试。
1. 从一份三列表格说起:qt 读取 excel 到 tablewidget 的最小闭环
把 qt 读取 excel 并显示到 tablewidget 的封装和 demo 丢进一个现场项目,最先暴露的往往不是语法,而是文件被锁、日期变数字、进程残留这些脏活。这套资源把 QAxObject 的读取过程收进一个 ExcelReader 类,对外只暴露 open、sheetNames、readSheet 三个动作,返回 QList<QList >,再由 demo 主窗口一次性灌进 QTableWidget。它适合需要快速把表格数据搬进桌面工具的人,也适合想看清 COM 调用边界、不想在业务代码里散落 querySubObject 的开发者。下面按选型、封装、落表、排错、验证的顺序拆,代码可直接抄,参数按你的表结构改。
2. 两条读取路线:QAxObject 与 QXlsx 的封装取舍
2.1 QAxObject 路线:借本机表格软件干活
QAxObject 的本质是 Qt 对 COM 的封装。创建Excel.Application之后,读取工作簿、工作表、单元格都走跨进程调用,本机安装的表格软件负责解析文件。这样做的直接好处是格式兼容性最好,.xls和.xlsx都能读,合并单元格、日期格式、公式缓存值这些细节由表格软件处理,封装层不用自己啃压缩包和 XML。代价也很明确:目标机器必须能创建 COM 组件,读取速度受跨进程调用次数影响,对象释放顺序一旦写错,后台进程就可能残留。
我一般把这条路线放在内部桌面工具、数据导入助手、报表查看器这类场景里。表不算特别大,格式比较杂,使用者机器上本来就有表格软件,优先选 QAxObject 能省很多格式适配。工程文件里要加 ActiveQt 模块,只在支持 COM 的构建里加,避免把不相关平台一起拖进来。
QT += core gui widgets CONFIG += c++17 win32 { QT += axcontainer }axcontainer是 QAxObject 所在模块;c++17不是硬性要求,但后面的结构化代码和容器写法会舒服一些。把这段放进.pro后先编译一个空窗口,确认#include <QAxObject>能找到,再往下写封装。很多人一上来就写读取逻辑,结果编译期卡在模块没加,浪费半小时。
2.2 QXlsx 路线:纯 Qt 解析,依赖和边界要提前算
QXlsx 走的是另一条路:把.xlsx当成压缩包和 XML 来解析,不依赖本机表格软件,跨平台部署更干净。它适合只读.xlsx、目标环境不能保证有表格软件、或者需要把程序塞进受控环境的项目。缺点也要提前算:.xls不支持;公式通常只能拿缓存值;复杂格式、合并单元格、日期系统需要自己补逻辑;第三方源码要跟着工程一起编译,升级和裁剪都要有人管。
两条路线的取舍可以压成一张表,选型时对着看比拍脑袋可靠:
| 维度 | QAxObject | QXlsx |
|---|---|---|
| 运行依赖 | 本机表格软件与 COM 组件 | 仅 Qt 与接入的源码 |
| 文件格式 | .xls、.xlsx | 以.xlsx为主 |
| 跨平台 | 受 COM 限制 | 可跨平台 |
| 大表性能 | 跨进程调用多,偏慢 | 解析快,内存占用看实现 |
| 格式兼容 | 高,复杂表更稳 | 基础格式为主,复杂表要补 |
| 封装重点 | 对象释放、线程套间 | 日期、合并单元格、列号转换 |
这套封装和 demo 以 QAxObject 为主,因为标题场景是“读取并显示到 TableWidget”,现场更常见的是格式不一的业务表。封装接口不绑定具体后端,后面如果换 QXlsx,只要保持ExcelSheetData的返回结构,上层填表代码不用动。
2.3 封装接口怎么定:先定返回值再定类
不要让读取类直接持有QTableWidget。读和显示绑死之后,导出、校验、分页都没法复用,测试也得拖着一个窗口。我的习惯是让读取层返回一个纯数据结构:表头、行数据、错误信息、成功标记。单元格值用QVariant而不是QString,日期、数字、空值的类型信息才不会在读取阶段丢光。
// excelreader.h #ifndef EXCELREADER_H #define EXCELREADER_H #include <QObject> #include <QVariant> #include <QStringList> class QAxObject; struct ExcelSheetData { QString name; QStringList headers; QList<QList<QVariant>> rows; QString error; bool ok = false; }; class ExcelReader : public QObject { Q_OBJECT public: explicit ExcelReader(QObject *parent = nullptr); ~ExcelReader() override; bool open(const QString &filePath); QStringList sheetNames() const; ExcelSheetData readSheet(const QString &sheetName, int maxRows = -1, int maxCols = -1); ExcelSheetData readSheet(int index, int maxRows = -1, int maxCols = -1); void close(); QString lastError() const; private: QVariant cellValue(QAxObject *sheet, int row, int col); QVariant normalizeValue(const QVariant &raw); void setError(const QString &message); QAxObject *m_excel = nullptr; QAxObject *m_workbook = nullptr; QAxObject *m_sheets = nullptr; QString m_filePath; QString m_lastError; }; #endif // EXCELREADER_HExcelSheetData里的headers和rows分开存,填表时先设列数再灌数据,比一边读一边setItem可控。maxRows、maxCols默认-1表示不限制,实际项目里一定要传上限,防止一张几万行的表把界面拖死。readSheet(int index)的索引从 0 开始,内部转成工作表名再走同一套逻辑,调用方不用记 COM 的 1 基索引。
3. 封装 ExcelReader:从工作簿到单元格值的代码实现
3.1 类定义与 COM 初始化
构造函数只做父对象绑定,真正的 COM 对象在open()里创建。这样做的好处是对象生命周期清晰:打开失败就清理,析构时再兜底一次close()。Visible=false让表格软件不弹窗,DisplayAlerts=false压掉覆盖提示,ScreenUpdating=false减少刷新开销。三个属性都不设置也能跑,但在批量读取时体验差别很明显。
#include "excelreader.h" #include <QAxObject> #include <QDir> #include <QFileInfo> #include <QDate> #include <QDateTime> #include <QMetaType> #include <QDebug> ExcelReader::ExcelReader(QObject *parent) : QObject(parent) {} ExcelReader::~ExcelReader() { close(); } void ExcelReader::setError(const QString &message) { m_lastError = message; qWarning() << "[ExcelReader]" << message; } QString ExcelReader::lastError() const { return m_lastError; } bool ExcelReader::open(const QString &filePath) { close(); m_filePath = filePath; QFileInfo fi(filePath); if (!fi.exists() || !fi.isFile()) { setError(QStringLiteral("文件不存在:%1").arg(filePath)); return false; } if (fi.size() == 0) { setError(QStringLiteral("文件为空:%1").arg(filePath)); return false; } m_excel = new QAxObject(QStringLiteral("Excel.Application")); if (!m_excel || m_excel->isNull()) { setError(QStringLiteral("创建 Excel.Application 失败,检查本机表格软件和 COM 组件")); delete m_excel; m_excel = nullptr; return false; } m_excel->setProperty("Visible", false); m_excel->setProperty("DisplayAlerts", false); m_excel->setProperty("ScreenUpdating", false); QAxObject *workbooks = m_excel->querySubObject("Workbooks"); if (!workbooks) { setError(QStringLiteral("获取 Workbooks 集合失败")); close(); return false; } const QString nativePath = QDir::toNativeSeparators(fi.absoluteFilePath()); QAxObject *workbook = workbooks->querySubObject("Open(const QString&)", nativePath); delete workbooks; if (!workbook) { setError(QStringLiteral("打开工作簿失败:%1").arg(nativePath)); close(); return false; } m_workbook = workbook; m_sheets = m_workbook->querySubObject("Worksheets"); if (!m_sheets) { setError(QStringLiteral("获取 Worksheets 集合失败")); close(); return false; } return true; }这段代码的关键参数是nativePath。Qt 内部用/,COM 接口在桌面环境里对分隔符更敏感,QDir::toNativeSeparators把路径转成当前平台习惯的形式,再配合absoluteFilePath()避免相对路径受工作目录影响。workbooks是临时集合对象,拿到workbook后立刻delete,不要留给析构函数猜。m_sheets是常驻的 Worksheets 集合,close()里按顺序释放。
3.2 工作表枚举与范围读取
工作表名枚举本身不复杂,坑在索引。COM 集合从 1 开始,Qt 容器从 0 开始,封装层要把这个差异吃掉。readSheet先拿UsedRange,再取它的Row、Column和行列Count。UsedRange的起始行列不一定是 1,前面有空行空列时可能是 3 行 2 列开始,直接按1..count读会错位。
QStringList ExcelReader::sheetNames() const { QStringList names; if (!m_sheets) return names; const int count = m_sheets->property("Count").toInt(); for (int i = 1; i <= count; ++i) { QAxObject *sheet = m_sheets->querySubObject("Item(int)", i); if (!sheet) continue; names << sheet->property("Name").toString(); delete sheet; } return names; } ExcelSheetData ExcelReader::readSheet(const QString &sheetName, int maxRows, int maxCols) { ExcelSheetData out; out.name = sheetName; if (!m_sheets) { out.error = QStringLiteral("工作簿尚未打开"); return out; } QAxObject *sheet = m_sheets->querySubObject("Item(const QString&)", sheetName); if (!sheet) { out.error = QStringLiteral("找不到工作表:%1").arg(sheetName); return out; } QAxObject *usedRange = sheet->querySubObject("UsedRange"); if (!usedRange) { out.error = QStringLiteral("获取 UsedRange 失败"); delete sheet; return out; } const int startRow = usedRange->property("Row").toInt(); const int startCol = usedRange->property("Column").toInt(); QAxObject *rowsObj = usedRange->querySubObject("Rows"); QAxObject *colsObj = usedRange->querySubObject("Columns"); int rowCount = rowsObj ? rowsObj->property("Count").toInt() : 0; int colCount = colsObj ? <p> <a href="https://download.csdn.net/download/rubberfeng/10229165" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>