QT私有方法成功消除的经验总结

工作过程中,涉及excel的读取,因此,通过搜索从网络下载开源库QXlsx来读写excel文档, 从实际的验证过程中,QXlsx支持windows、linux平台,不但能够读取excel文档,而且可以格式化excel文档,设置文档的字体类型,字体大小、字体颜色,对齐方式等,但是它有一个不好的地方,就是使用的时候,QtCreator创建的工程中pro文件需要加入gui-private, Qt官方不推荐使用的 gui-private, 并且如果升级Qt版本,gui-private提供的接口就有可能不能使用或者被删除,Qt官方不保证gui-private的稳定。所以,决定消除QXlsx使用QZipReader和QZipWriter这种私有的方法,通过尝试和研究,最终成功解决了这个问题。本文将记录其解决的过程方法,作为经验的积累和以后其他类似问题解决的借鉴。

一、前期准备工作

通过阅读QXlsx中ZipReader和ZipWriter源代码,ZipReader和ZipWriter类内部封装了Qt私有接口QZipReader和QZipWriter,而QZipReader的功能就是解压读取zip文件,QZipWriter写入文件并压缩成zip文件。因此,如果需要消除QZipReader和QZipWriter,那么首先需要找到QZipReader和QZipWriter的代替品,然后实现ZipReader和ZipWriter的功能。

通过网络搜索找到Zip Utils简化版本的zip库,它支持C++,具体下载地址可以参考文末的链接。

1、如果是在linux系统上使用,需要添加ZIP_STD宏,例如我是在QtCreator工具上使用,那么在pro文件添加如下所示的信息

DEFINES += ZIP_STD

2、如果是在window上使用,经验证可能需要添加部分接口,有编译的问题,欢迎留言一起探讨。

3、如果是在mac系统上使用,那么unzip.cpp文件中malloc.h需要修改为stdlib.h, 为了跨平台的使用,可以按照如下所示的方式进行修改

#ifdef __APPLE__
#include <stdlib.h>
#else
#include <malloc.h>
#endif

4、ZipReader的头文件定义

#include "xlsxglobal.h"

#include <QScopedPointer>
#include <QStringList>
#include <QIODevice>

#if QT_VERSION >= 0x050600
 #include <QVector>
#endif

class QZipReader;

QT_BEGIN_NAMESPACE_XLSX

class  ZipReader
{
public:
    explicit ZipReader(const QString &fileName);
    explicit ZipReader(QIODevice *device);
    ~ZipReader();
    bool exists() const;
    QStringList filePaths() const;
    QByteArray fileData(const QString &fileName) const;

private:
    Q_DISABLE_COPY(ZipReader)
    void init();
    QScopedPointer<QZipReader> m_reader;
    QStringList m_filePaths;
};

QT_END_NAMESPACE_XLSX

5、ZipWriter的头文件定义

#include <QString>
#include <QIODevice>

#include "xlsxglobal.h"

class QZipWriter;

QT_BEGIN_NAMESPACE_XLSX

class ZipWriter
{
public:
    explicit ZipWriter(const QString &filePath);
    explicit ZipWriter(QIODevice *device);
    ~ZipWriter();

    void addFile(const QString &filePath, QIODevice *device);
    void addFile(const QString &filePath, const QByteArray &data);
    bool error() const;
    void close();

private:
    QZipWriter *m_writer;
};

QT_END_NAMESPACE_XLSX

二、读取类的消除

1、修改ZipReader头文件,  其中添加USE_LOCAL_ZIP是为了方便开启和关闭我们自己加入zip utils库,如果定义了USE_LOCAL_ZIP,那么使用我们自己加入的zip utils库,否则使用原来的类。另外对比ZipReader前后修改的头文件,可以发现主要的修改就是将QZipReader修改为HZIP。

#include <QScopedPointer>
#include <QStringList>
#include <QIODevice>

#if QT_VERSION >= 0x050600
 #include <QVector>
#endif

if defined(USE_LOCAL_ZIP)

#include "unzip.h"

QT_BEGIN_NAMESPACE_XLSX

class  ZipReader
{
public:
    explicit ZipReader(const QString &fileName);
    explicit ZipReader(QIODevice *device);
    ~ZipReader();
    bool exists() const;
    QStringList filePaths() const;
    QByteArray fileData(const QString &fileName) const;

private:
    Q_DISABLE_COPY(ZipReader)
    void init();
    HZIP m_reader;
    QStringList m_filePaths;
    ZRESULT m_result;
};

QT_END_NAMESPACE_XLSX

#endif

2、实现ZipReader类,其中需要注意的点:1> 传入的QIODevice需要先close, 再调用zip库的接口OpenZip才能打开成功; 2> QString类型的字符串转换为char *类型,需要先调用toUtf8。

#if defined(USE_LOCAL_ZIP)

#include <QFile>

QT_BEGIN_NAMESPACE_XLSX

ZipReader::ZipReader(const QString &filePath) :
#ifdef UNICODE
    m_reader(OpenZip(reinterpret_cast<const wchar_t *>(filePath.utf16()), nullptr))
#else
    m_reader(OpenZip(filePath.toUtf8().constData(), nullptr))
#endif
    , m_result(ZR_OK)
{
    init();
}

ZipReader::ZipReader(QIODevice *device)
{

    QFile *file = dynamic_cast<QFile *>(device);
    if (nullptr != file)
    {
        QString str_filename = file->fileName();
        file->close();
    #ifdef UNICODE
        m_reader = OpenZip(reinterpret_cast<const wchar_t *>(str_filename.utf16()), nullptr);
    #else
        m_reader = OpenZip(str_filename.toUtf8().constData(), nullptr);
    #endif
    }
    else
    {
        // 如果使用下面的方式,那么会读取不到数据,需要解决,目前走的是上面的分支
        m_reader = OpenZip(device->readAll().data(), device->size(), nullptr);
    }

    init();
}

ZipReader::~ZipReader()
{
    CloseZip(m_reader);
}

void ZipReader::init()
{
    ZIPENTRY entry;
    GetZipItem(m_reader, -1, &entry);
    int i_numitems = entry.index;
    for (int zi = 0; zi <i_numitems; zi++)
    {
        GetZipItem(m_reader, zi, &entry);
        m_filePaths.append(QString::fromUtf8(entry.name));
    }
}

bool ZipReader::exists() const
{
    return IsZipHandleU(m_reader);
}

QStringList ZipReader::filePaths() const
{
    return m_filePaths;
}

QByteArray ZipReader::fileData(const QString &fileName) const
{
    ZIPENTRY entry;
    int i = -1;

    // 使用fileName.toUtf8().constData(), 而不是fileName.toStdString().c_str(),否则会读取不到
    FindZipItem(m_reader, fileName.toUtf8().constData(), true, &i, &entry);
    if (entry.unc_size < 0 || i < 0)
    {
        return QByteArray("");
    }

    char *p_buf = new char[entry.unc_size + 1];
    UnzipItem(m_reader,i, p_buf, entry.unc_size);
    QByteArray byte_array("");
    byte_array.append(p_buf, entry.unc_size);
    delete[] p_buf;
    p_buf = nullptr;
    return byte_array;
}
QT_END_NAMESPACE_XLSX

#endif

三、写入类的消除

1、修改ZipWriter头文件, 对比ZipWriter前后修改的头文件,可以发现主要的修改就是将QZipWriter修改为HZIP。

#if defined(USE_LOCAL_ZIP)
#include "zip.h"

QT_BEGIN_NAMESPACE_XLSX

class ZipWriter
{
public:
    explicit ZipWriter(const QString &filePath);
    explicit ZipWriter(QIODevice *device);
    ~ZipWriter();

    void addFile(const QString &filePath, QIODevice *device);
    void addFile(const QString &filePath, const QByteArray &data);
    bool error() const;
    void close();

private:
    HZIP m_writer;
    ZRESULT m_result;

};

QT_END_NAMESPACE_XLSX
#endif

2、实现ZipWriter类,其中需要注意的点:1> 传入的QIODevice需要先close, 再调用zip库的接口CreateZip才能创建成功; 2> QString类型的字符串转换为char *类型,需要先调用toUtf8。

#if defined(USE_LOCAL_ZIP)

#include <QFile>
#include <QDebug>

QT_BEGIN_NAMESPACE_XLSX

ZipWriter::ZipWriter(const QString &filePath)
{
#ifdef UNICODE
    m_writer = CreateZip(reinterpret_cast<const wchar_t *>(filePath.utf16()), nullptr);
#else
    m_writer = CreateZip(filePath.toUtf8().constData(), nullptr);
#endif
}

ZipWriter::ZipWriter(QIODevice *device)
{

    QFile *file = dynamic_cast<QFile *>(device);
    if (nullptr != file)
    {
        QString str_filename = file->fileName();
        // 需要先close,否则Create会失败
        file->close();
    #ifdef UNICODE
        m_writer = CreateZip(reinterpret_cast<const wchar_t *>(str_filename.utf16()), nullptr);
    #else
        m_writer = CreateZip(str_filename.toUtf8().constData(), nullptr);
    #endif
    }
    else
    {
    }

}

ZipWriter::~ZipWriter()
{
    if (m_writer)
    {
        CloseZip(m_writer);
        m_writer =nullptr;
    }
}

bool ZipWriter::error() const
{
    return !IsZipHandleZ(m_writer);
}

void ZipWriter::addFile(const QString &filePath, QIODevice *device)
{
    ZipAdd(m_writer, filePath.toUtf8().constData(),  (void *)device->readAll().data(), device->size());
}

void ZipWriter::addFile(const QString &filePath, const QByteArray &data)
{
    ZipAdd(m_writer, filePath.toUtf8().constData(),  (void *)data.data(), data.size());
}

void ZipWriter::close()
{
    if (m_writer)
    { 
        CloseZip(m_writer);
        m_writer =nullptr;
    }
}

QT_END_NAMESPACE_XLSX

#endif

四、总结

总的来说,消除私有方法,首先需要找到代替的类,如果是简单的功能,可以考虑自己实现,否则试试从网络上查找相关的开源代码。接着就是利用代替类重新实现相同的接口,另外,最好提前写好测试类,方便对比测试前后修改的功能。最后需要注意unicode字符类型和utf8字符类型的转换,否则有可能出现程序崩溃,功能不正常等问题。

五、参考链接

读写excel文档入门讲解一

zip_utils

读写excel文档入门讲解一

最近工作中需要将数据以各种格式存放到excel文档中,工程是基于C++,并且采用QT设计的界面,因此,通过网络搜索寻找到基于QT来读写excel文档的开源库QXlsx,本文将介绍开源库QXlsx的简单使用和效果,一方面提供入门知识,另一方面也给自己的学习成果做一个总结记录,方便后续查找复习。

一、简单的例子

1、 网上下载QXlsx开源库后,需要将QXlsx加载到Qt Creator工具,直接参考开源库代码中的HowToSetProject.md文件即可

2、展示写入数据,并且生成excel文档的例子

// 头文件
#include "xlsxdocument.h"
#include "xlsxformat.h"
#include "xlsxworkbook.h"

QXlsx::Document xlsx;
// 通过单元格名写入字符串
xlsx.write("A1", "Hello!");
// 通过行列方式写入字符串
xlsx.write(2, 1, "Everyone!");

std::string str_file_name = JPathHandler::GetDebugPath() + "excel_hello.xlsx";
// 生成excel文档
xlsx.saveAs(QString::fromStdString(str_file_name));

3、最后生成的excel文档的内容如下图所示

二、自动换行

有时候写入单元格的内容比较长,超过了单元格的展示范围,而我们又不想扩大单元格的宽度,怎么办呢,可以通过设置自动换行的方式,让写入单元格的内容自动适配当前的单元格。

1、携带配置好的格式写入到单元格

QXlsx::Document xlsx2;

// 初始化格式
QXlsx::Format format2;
format2.setFontSize(10);
format2.setHorizontalAlignment(QXlsx::Format::AlignHDistributed);

// 用上面的格式来初始化下面的单元格
xlsx2.write("A1", "onetwothree,onetwothree,onetwothree", format2);

// 最后生成excel文档(注意JPathHandler::GetDebugPath()自定义的类,用于获取路径,不是开源库的接口)
std::string str_file_name2 = JPathHandler::GetDebugPath() + "excel_align.xlsx";
xlsx2.saveAs(QString::fromStdString(str_file_name2));

2、最后生成的excel文档的内容效果如下图所示

三、多个表单

当需要多个表单来保存不同的数据的时候,这就涉及到创建多个表单的问题。那么将用创建两个表单的例子来作为简单的入门知识点。

1、生成两个表单,并且分别往表单中写入数据

QXlsx::Document xlsx3;

// 第一个表单
xlsx3.addSheet("first year");
xlsx3.write("A1", "first");

// 第二个表单
xlsx3.addSheet("second year");
xlsx3.write("A1", "second");

// 最后生成excel文档
std::string str_file_name3 = JPathHandler::GetDebugPath() + "excel_addsheet.xlsx";
xlsx3.saveAs(QString::fromStdString(str_file_name3));

2、最后生成的excel文档的内容效果如下图所示

四、读取excel文档

写入数据到excel文档之后,肯定有需求场景需要读取excel文档数据来展示,所以,接下来将说明读取excel文档的所有数据的方法。

1、先读取excel文档,然后获取每一个表单,读取每一个表单中的单元格内容

// 读取上一个步骤中生成的excel文档
QXlsx::Document xlsx4( QString::fromStdString(str_file_name3) );

int i_index_sheet = 0;
// 循环读取excel中的每一个表单,最后打印输出每个单元格的内容
foreach( QString current_sheetname, xlsx4.sheetNames() )
{
    QXlsx::AbstractSheet* p_current_sheet = xlsx4.sheet( current_sheetname );
    p_current_sheet->workbook()->setActiveSheet(i_index_sheet);
    QXlsx::Worksheet* wsheet = (QXlsx::Worksheet*) p_current_sheet->workbook()->activeSheet();

    int i_max_row = -1;
    int i_max_col = -1;
    QVector<QXlsx::CellLocation> clList = wsheet->getFullCells( &i_max_row, &i_max_col );
    for ( int i_cellindex = 0; i_cellindex < clList.size(); ++i_cellindex )
     {
        QXlsx::CellLocation cell_location = clList.at(i_cellindex);
        QVariant var = cell_location.cell.data()->value();
        QString str = var.toString();
        LOG(INFO)<< std::string( str.toLocal8Bit() );
    }
    i_index_sheet++;
}

2、最后输出的打印信息如下所示

[2019-10-20 21:09:48,039626] [bool JQtAttr::TestXlsx():135] first
[2019-10-20 21:09:48,039675] [bool JQtAttr::TestXlsx():135] second

五、总结

从上面的例子中,可以看出QXlsx::Document是控制整个excel文档的类,它可以控制表单的宽高,控制表单等功能,而QXlsx::Format则是控制单元格格式的类,它可以设置字体、字体大小,对齐方式等。

QXlsx的局限性,它不支持多线程,当表单比较多的时候,加载表单会很耗时。

参考链接:

Handling Microsoft Excel file format

QXlsx