一杯清酒邀明月
天下本无事,庸人扰之而烦耳。

Qt Windows 下快速读写Excel指南

很多人搜如何读写excel都会看到用QAxObject来进行操作,很多人试了之后都会发现一个问题,就是慢,非常缓慢!因此很多人得出结论是QAxObject读写excel方法不可取,效率低。
后来我曾试过用ODBC等数据库类型的接口进行读写,遇到中文嗝屁不说,超大的excel还是会读取速度慢。
最后,看了一些开源的代码后发现,Windows下读取excel,还是用QAxObject最快!没错,就是用QAxObject读写最快!!!(读取10万单元格229ms)
大家以后读取excel时(win下),不用考虑别的方法,用QAxObject就行,速度杠杠的,慢是你操作有误!下面就说说如何能提高其读取效率。

读取excel慢的原因

这里不说如何打开或生成excel,着重说说如何快速读取excel。
网上搜到用Qt操作excel的方法,读取都是使用类似下面这种方法进行:

 1 QVariant ExcelBase::read(int row, int col)
 2 {
 3     QVariant ret;
 4     if (this->sheet != NULL && ! this->sheet->isNull())
 5     {
 6         QAxObject* range = this->sheet->querySubObject("Cells(int, int)", row, col);
 7         //ret = range->property("Value");
 8         ret = range->dynamicCall("Value()");
 9         delete range;
10     }
11     return ret;
12 }

读取慢的根源就在于sheet->querySubObject("Cells(int, int)", row, col)

试想有10000个单元就得调用10000次querySubObject,网络上90%的教程都没说这个querySubObject产生的QAxObject*最好进行手动删除,虽然在它的父级QAxObject会管理它的内存,但父级不析构,子对象也不会析构,若调用10000次,就会产生10000个QAxObject对象
得益于QT快速读取数据量很大的Excel文件此文,下面总结如何快速读写excel

快速读取excel文件

原则是一次调用querySubObject把所有数据读取到内存中
VBA中可以使用UsedRange把所有用到的单元格范围返回,并使用属性Value把这些单元格的所有值获取。

这时,获取到的值是一个table,但Qt把它变为一个变量QVariant来储存,其实实际是一个QList<QList<QVariant> >,此时要操作里面的内容,需要把这个QVariant转换为QList<QList<QVariant> >

先看看获取整个单元格的函数示意(这里ExcelBase是一个读写excel的类封装):

 1 QVariant ExcelBase::readAll()
 2 {
 3     QVariant var;
 4     if (this->sheet != NULL && ! this->sheet->isNull())
 5     {
 6         QAxObject *usedRange = this->sheet->querySubObject("UsedRange");
 7         if(NULL == usedRange || usedRange->isNull())
 8         {
 9             return var;
10         }
11         var = usedRange->dynamicCall("Value");
12         delete usedRange;
13     }
14     return var;
15 }

代码中this->sheet是已经打开的一个sheet,再获取内容时使用this->sheet->querySubObject("UsedRange");即可把所有范围都获取。

下面这个castVariant2ListListVariant函数把QVariant转换为QList<QList<QVariant> >

 1 ///
 2 /// \brief 把QVariant转为QList<QList<QVariant> >
 3 /// \param var
 4 /// \param res
 5 ///
 6 void ExcelBase::castVariant2ListListVariant(const QVariant &var, QList<QList<QVariant> > &res)
 7 {
 8     QVariantList varRows = var.toList();
 9     if(varRows.isEmpty())
10     {
11         return;
12     }
13     const int rowCount = varRows.size();
14     QVariantList rowData;
15     for(int i=0;i<rowCount;++i)
16     {
17         rowData = varRows[i].toList();
18         res.push_back(rowData);
19     }
20 }

这样excel的所有内容都转换为QList<QList<QVariant>>保存,其中QList<QList<QVariant> >QList<QVariant>为每行的内容,行按顺序放入最外围的QList中。

对于如下如的excel:

这里写图片描述

读取后的QList<QList<QVariant> >结构如下所示:

这里写图片描述

继续展开

这里写图片描述

下面看看此excel的读取速度有多高
这里有个excel,有1000行,100列,共计十万单元格,打开使用了一些时间,读取10万单元格耗时229毫秒,
读取的代码如下:(完整源代码见后面)

 1 void MainWindow::on_action_open_triggered()
 2 {
 3     QString xlsFile = QFileDialog::getOpenFileName(this,QString(),QString(),"excel(*.xls *.xlsx)");
 4     if(xlsFile.isEmpty())
 5         return;
 6     QElapsedTimer timer;
 7     timer.start();
 8     if(m_xls.isNull())
 9         m_xls.reset(new ExcelBase);
10     m_xls->open(xlsFile);
11     qDebug()<<"open cost:"<<timer.elapsed()<<"ms";timer.restart();
12     m_xls->setCurrentSheet(1);
13     m_xls->readAll(m_datas);
14     qDebug()<<"read data cost:"<<timer.elapsed()<<"ms";timer.restart();
15     QVariantListListModel* md = qobject_cast<QVariantListListModel*>(ui->tableView->model());
16     if(md)
17     {
18         md->updateData();
19     }
20     qDebug()<<"show data cost:"<<timer.elapsed()<<"ms";timer.restart();
21 }

上面的m_xls和m_datas是成员变量:

1 QScopedPointer<ExcelBase> m_xls;
2 QList< QList<QVariant> > m_datas;

读取的耗时:

1 "D:\czy_blog\czyBlog\04_fastReadExcel\src\fastReadExcelInWindows\excelRWByCztr1988.xls"
2 open cost: 1183 ms
3 read data cost: 229 ms
4 show data cost: 14 ms

10万个也就0.2秒而已

快速写入excel文件

同理,能通过QAxObject *usedRange = this->sheet->querySubObject("UsedRange");实现快速读取,也可以实现快速写入

快速写入前需要些获取写入单元格的范围:Range(const QString&)
如excel的A1为第一行第一列,那么A1:B2就是从第一行第一列到第二行第二列的范围。

要写入这个范围,同样也是通过一个与之对应的QList<QList<QVariant> >,具体见下面代码:

 1 ///
 2 /// \brief 写入一个表格内容
 3 /// \param cells
 4 /// \return 成功写入返回true
 5 /// \see readAllSheet
 6 ///
 7 bool ExcelBase::writeCurrentSheet(const QList<QList<QVariant> > &cells)
 8 {
 9     if(cells.size() <= 0)
10         return false;
11     if(NULL == this->sheet || this->sheet->isNull())
12         return false;
13     int row = cells.size();
14     int col = cells.at(0).size();
15     QString rangStr;
16     convertToColName(col,rangStr);
17     rangStr += QString::number(row);
18     rangStr = "A1:" + rangStr;
19     qDebug()<<rangStr;
20     QAxObject *range = this->sheet->querySubObject("Range(const QString&)",rangStr);
21     if(NULL == range || range->isNull())
22     {
23         return false;
24     }
25     bool succ = false;
26     QVariant var;
27     castListListVariant2Variant(cells,var);
28     succ = range->setProperty("Value", var);
29     delete range;
30     return succ;
31 }

此函数是把数据从A1开始写

函数中的convertToColName为把列数,转换为excel中用字母表示的列数,这个函数是用递归来实现的:

 1 ///
 2 /// \brief 把列数转换为excel的字母列号
 3 /// \param data 大于0的数
 4 /// \return 字母列号,如1->A 26->Z 27 AA
 5 ///
 6 void ExcelBase::convertToColName(int data, QString &res)
 7 {
 8     Q_ASSERT(data>0 && data<65535);
 9     int tempData = data / 26;
10     if(tempData > 0)
11     {
12         int mode = data % 26;
13         convertToColName(mode,res);
14         convertToColName(tempData,res);
15     }
16     else
17     {
18         res=(to26AlphabetString(data)+res);
19     }
20 }
21 ///
22 /// \brief 数字转换为26字母
23 ///
24 /// 1->A 26->Z
25 /// \param data
26 /// \return
27 ///
28 QString ExcelBase::to26AlphabetString(int data)
29 {
30     QChar ch = data + 0x40;//A对应0x41
31     return QString(ch);
32 }

看看写excel的耗时:

 1 void MainWindow::on_action_write_triggered()
 2 {
 3     QString xlsFile = QFileDialog::getExistingDirectory(this);
 4     if(xlsFile.isEmpty())
 5         return;
 6     xlsFile += "/excelRWByCztr1988.xls";
 7     QElapsedTimer timer;
 8     timer.start();
 9     if(m_xls.isNull())
10         m_xls.reset(new ExcelBase);
11     m_xls->create(xlsFile);
12     qDebug()<<"create cost:"<<timer.elapsed()<<"ms";timer.restart();
13     QList< QList<QVariant> > m_datas;
14     for(int i=0;i<1000;++i)
15     {
16         QList<QVariant> rows;
17         for(int j=0;j<100;++j)
18         {
19             rows.append(i*j);
20         }
21         m_datas.append(rows);
22     }
23     m_xls->setCurrentSheet(1);
24     timer.restart();
25     m_xls->writeCurrentSheet(m_datas);
26     qDebug()<<"write cost:"<<timer.elapsed()<<"ms";timer.restart();
27     m_xls->save();
28 }

输出:

1 create cost: 814 ms 
2 "A1:CV1000" 
3 write cost: 262 ms 

写10万个数据耗时262ms,有木有感觉很快,很强大

结论

  • Qt在windows下读写excel最快速的方法还是使用QAxObject
  • 不要使用类似
    sheet->querySubObject("Cells(int, int)", row, col);
    的方式读写excel,这是导致低效的更本原因
posted on 2021-01-21 09:01  一杯清酒邀明月  阅读(4617)  评论(1编辑  收藏  举报