实例功能概述

除了文本文件之外,其他需要按照一定的格式定义读写的文件都称为二进制文件。每种格式的二进制文件都有自己的格式定义,写入数据时按照一定的顺写入,读出时也按照相应的顺读出。例如地球物理中常用的SEG-Y格式文件,必须按照其标准格式要求写入数据才符合这种文件的格式规范,读取数据时也需要按照格式定义来读出。

Qt 使用 QFile 和 QDataStream 进行二进制数据文件的读取。QFile 负责文件的 IO 设备接口,即与文件的物理交互,QDataStream 以数据流的方式读取文件内容或写入文件内容。

我们依然是用一个例子来演示二进制文件读写,界面如下:

Qt文件系统-二进制文件读写-LMLPHP

实例中以表格编辑一个数据表,采用 Model/View结构,编辑后的数据保存为二进制文件。

根据 QDataStream 保存文件时使用的数据编码的方式不同,可以保存为两种文件。

  1. 用 Qt 预定义的编码保存各种类型的数据文件,定义文件后缀为“.stm”。Qt 预定义编码是指在写入某个类型数据,如整形数、字符串等到文件流时,使用 Qt 预定义的编码。可以将这种 Qt 预定义数据格式编码类比 HTML 的标记符,Qt 写入某种类型的数据时用了 Qt 预定义的标记符,独处数据时,根据标记符读出数据。使用 Qt 预定义编码保存流文件,某些字节是 QDataStream 自己写入的,我们并不完全知道文件内每个字节的意义,但使用 QDataStream 可以读出相应的数据。
  2. 标准编码数据文件,定义文件后缀为“.dat”。在将数据写到文件时,完全使用数据的二进制原始内容,每个字节都有具体定义,在读出数据时,只需根据每个字节的定义读出数据即可。

编写的例子具有以下功能:

  • 可以在表格内编辑数据,同样的表格数据内容可以保存为两种格式的文件,Qt 预定义编码文件(stm 文件)和标准编码文件(dat 文件);
  • 界面上的表格数据可以修改,可以添加行,插入列,删除行;
  • 可以读取 stm 文件或 dat 文件,虽然文件格式不一样,但对相同的界面数据表存储的文件的实质内容是由于的。

实例的主窗口使用了 Model/View结构、标准项数据模型(QStandardItemModel)和选择模型(QItemSelectionModel),界面上使用了 QTableView 组件,还有代理组件。这些设计 Model/View

的设计,以后得文章会讲。

为了便于理解后面的程序,这里给出 MainWindow 类中自定义的一些变量和函数,具体如下:

class MainWindow : public QMainWindow
{
    Q_OBJECT
private:

//用于状态栏的信息显示
    QLabel  *LabCellPos;    //当前单元格行列号
    QLabel  *LabCellText;   //当前单元格内容

    QWIntSpinDelegate    intSpinDelegate; //整型数
    QWFloatSpinDelegate  floatSpinDelegate; //浮点数
    QWComboBoxDelegate   comboBoxDelegate; //列表选择

    QStandardItemModel  *theModel;//数据模型
    QItemSelectionModel *theSelection;//Item选择模型

    void    resetTable(int aRowCount);  //表格复位,设定行数
    bool    saveDataAsStream(QString& aFileName);//将数据保存为数据流文件
    bool    openDataAsStream(QString& aFileName);//读取数据流文件

    bool    saveBinaryFile(QString& aFileName);//保存为二进制文件
    bool    openBinaryFile(QString& aFileName);//打开二进制文件
};

Qt 预定义编码文件的读写

保存为 stm 文件

首先看文件保存功能,因为从文件保存功能的代码可以看出文件内数据的存储顺序。在 UI 界面上点击“保存 stm”文件。可以使用 Qt 预定义编码方式保存文件。此按钮响应代码如下:

void MainWindow::on_actSave_triggered()
{ //以Qt预定义编码保存数据文件
    QString curPath=QDir::currentPath();
    QString aFileName=QFileDialog::getSaveFileName(this,tr("选择保存文件"),curPath,
                 "Qt预定义编码数据文件(*.stm)");

    if (aFileName.isEmpty())
        return; //

   if  (saveDataAsStream(aFileName)) //保存为流数据文件
       QMessageBox::information(this,"提示消息","文件已经成功保存!");
}
bool MainWindow::saveDataAsStream(QString &aFileName)
{//将模型数据保存为Qt预定义编码的数据文件
    QFile aFile(aFileName);  //以文件方式读出
    if (!(aFile.open(QIODevice::WriteOnly | QIODevice::Truncate)))
        return false;

    QDataStream aStream(&aFile);
    aStream.setVersion(QDataStream::Qt_5_9); //设置版本号,写入和读取的版本号要兼容

    qint16  rowCount=theModel->rowCount(); //数据模型行数
    qint16  colCount=theModel->columnCount(); //数据模型列数

    aStream<<rowCount; //写入文件流,行数
    aStream<<colCount;//写入文件流,列数

//获取表头文字
    for (int i=0;i<theModel->columnCount();i++)
    {
        QString str=theModel->horizontalHeaderItem(i)->text();//获取表头文字
        aStream<<str; //字符串写入文件流,Qt预定义编码方式
    }


//获取数据区的数据
    for (int i=0;i<theModel->rowCount();i++)
    {
        QStandardItem* aItem=theModel->item(i,0); //测深
        qint16 ceShen=aItem->data(Qt::DisplayRole).toInt();
        aStream<<ceShen;// 写入文件流,qint16

        aItem=theModel->item(i,1); //垂深
        qreal chuiShen=aItem->data(Qt::DisplayRole).toFloat();
        aStream<<chuiShen;//写入文件流, qreal

        aItem=theModel->item(i,2); //方位
        qreal fangWei=aItem->data(Qt::DisplayRole).toFloat();
        aStream<<fangWei;//写入文件流, qreal

        aItem=theModel->item(i,3); //位移
        qreal weiYi=aItem->data(Qt::DisplayRole).toFloat();
        aStream<<weiYi;//写入文件流, qreal

        aItem=theModel->item(i,4); //固井质量
        QString zhiLiang=aItem->data(Qt::DisplayRole).toString();
        aStream<<zhiLiang;// 写入文件流,字符串

        aItem=theModel->item(i,5); //测井
        bool quYang=(aItem->checkState()==Qt::Checked);
        aStream<<quYang;// 写入文件流,bool型
    }
    aFile.close();
    return true;
}

自定义函数 saveDataAsStream()将表格的数据模型 thisModel 的数据保存为一个 stm 文件。代码首先是创建 QFile 对象 aFile 打开文件,然后创建 QDataStream 对象 aStream 与 QFile 对象关联。

在开始写数据流前,为 QDataStream 对象 aStream 设置版本号,即调用 setVersion()函数,并传递一个 QDataStream::Version 枚举类型的值。

aStream.setVersion(QDataStream::Qt_5_9);

接下来就是按照需要保存数据的顺序写入文件流。例如在文件开始,先写入行数和列数两个 qint16 的整数。因为行数和列数关系到后面的数据时如何组织的,英雌在读取文件数据时,首先读取这两个整数,然后根据数据存储方式的约定,就知道后续数据该如何读取了。向文件写入数据时,直接用流的输入操作,如:

aStream<<rowCount; //写入文件流,行数
aStream<<colCount; //写入文件流,列数

在读取各列的表头字符串之后,将其写入数据流。然后逐行扫描表格的数据模型,将每一行的列数据写入数据流。数据流写入数据时都使用运算符“<<”,不论写的是 qint16、qreal,还是其他字符串。除了可以写入基本数据类型以外,QDataStream 流操作还可以写入很多其他类型的数据,如 QBrush、QColor、QImage、QIcon 等,这些称为可序列化的数据类型。

QDataStream 以流操作写入这些数据时,我们并不知道文件里每个字节是如何存储的,但是知道数据写入顺序,以及每次写入数据的类型。在文件数据读出时,只需按照顺序和类型对应读出即可。

读取 stm 文件

按钮“打开 stm 文件”的响应代码及相关函数代码,选择需要打开的 stm 文件后,主要是调用自定义函数 openDataAsStream()将其打开。

bool MainWindow::openDataAsStream(QString &aFileName)
{ //从Qt预定义流文件读入数据
    QFile aFile(aFileName);  //以文件方式读出
    if (!(aFile.open(QIODevice::ReadOnly)))
        return false;

    QDataStream aStream(&aFile); //用文本流读取文件
    aStream.setVersion(QDataStream::Qt_5_9); //设置流文件版本号

    qint16  rowCount,colCount;
    aStream>>rowCount; //读取行数
    aStream>>colCount; //列数

    this->resetTable(rowCount); //表格复位

    //获取表头文字
    QString str;
    for (int i=0;i<colCount;i++)
        aStream>>str;  //读取表头字符串

    //获取数据区文字,
    qint16  ceShen;
    qreal  chuiShen;
    qreal  fangWei;
    qreal  weiYi;
    QString  zhiLiang;
    bool    quYang;
    QStandardItem   *aItem;
    QModelIndex index;

    for (int i=0;i<rowCount;i++)
    {
        aStream>>ceShen;//读取测深, qint16
        index=theModel->index(i,0);
        aItem=theModel->itemFromIndex(index);
        aItem->setData(ceShen,Qt::DisplayRole);

        aStream>>chuiShen;//垂深,qreal
        index=theModel->index(i,1);
        aItem=theModel->itemFromIndex(index);
        aItem->setData(chuiShen,Qt::DisplayRole);


        aStream>>fangWei;//方位,qreal
        index=theModel->index(i,2);
        aItem=theModel->itemFromIndex(index);
        aItem->setData(fangWei,Qt::DisplayRole);


        aStream>>weiYi;//位移,qreal
        index=theModel->index(i,3);
        aItem=theModel->itemFromIndex(index);
        aItem->setData(weiYi,Qt::DisplayRole);


        aStream>>zhiLiang;//固井质量,QString
        index=theModel->index(i,4);
        aItem=theModel->itemFromIndex(index);
        aItem->setData(zhiLiang,Qt::DisplayRole);

        aStream>>quYang;//bool
        index=theModel->index(i,5);
        aItem=theModel->itemFromIndex(index);
        if (quYang)
            aItem->setCheckState(Qt::Checked);
        else
            aItem->setCheckState(Qt::Unchecked);
    }

    aFile.close();
    return true;
}
void MainWindow::resetTable(int aRowCount)
{ //表格复位,先删除所有行,再设置新的行数,表头不变
//    QStringList     headerList;
//    headerList<<"测深(m)"<<"垂深(m)"<<"方位(°)"<<"总位移(m)"<<"固井质量"<<"测井取样";
//    theModel->setHorizontalHeaderLabels(headerList); //设置表头文字

    theModel->removeRows(0,theModel->rowCount()); //删除所有行
    theModel->setRowCount(aRowCount);//设置新的行数

    QString str=theModel->headerData(theModel->columnCount()-1,
                     Qt::Horizontal,Qt::DisplayRole).toString();

    for (int i=0;i<theModel->rowCount();i++)
    { //设置最后一列
        QModelIndex index=theModel->index(i,FixedColumnCount-1); //获取模型索引
        QStandardItem* aItem=theModel->itemFromIndex(index); //获取item
        aItem->setCheckable(true);
        aItem->setData(str,Qt::DisplayRole);
        aItem->setEditable(false); //不可编辑
    }
}

读取 stm 文件的数据之前必须设置 QDataStream 的流版本号,应该等于或高于数据保存时的流版本号。文件最早的两个数据时表格的行数和列数,读出这两个数据,就能知道数据的行数和列数,并调用自定义函数 resetTable()给数据模型复位,并设置其行数。

使用 QDataStream 的流操作方式读写文件的特点如下:

  • 读写操作都比较方便,支持读写各种数据类型,包括Qt的一些类,还可以为流数据读写扩展自定义的数据类型。读写某种类型的数据时,只要是流支持即可,而在文件内部是如何存储的,用户无需关心,由Qt预定义。
  • 写文件和读文件时必须保证使用的流版本兼容,即流的版本号相同,或读取文件的流版本号高于写文件时的流版本号。这是因为在不同的流版本中,流支持的数据类型的读写方式可能有所改变,必须保证读写版本的兼容。
  • 用这种方式保存文件时,写入数据采用Qt预定义的编码,即写入文件的二进制编码是由Qt预定义的,写多少个字节、字节是什么样的顺序,用户是不知道的。如果是由QDataStream读取数据,只需按类型读出即可。但是,如果由这种方法创建的文件是用于交换的,需要用其他的编程语言(如Matlab)来读取文件内容,则存在问题了。因为其他语言并没有与Qt的流写入完全一致的流读出功能,例如,其他语言并不知道Qt保存的QString或 QFon的内容是如何组织的。

标准编码文件的读写

保存为 dat 文件

前面采用 Qt 预定义的 stm 文件,这种方法使用简单,但是文件的格式不完全透明,不能创建用于交换的通用格式文件。

创建通用格式文件的方法是以标准编码方式创建文件,使文件的每个字节都有具体的定义。用户在读取这种文件时,按照文件格式定义读取出每个字节数据并做解析即可,不管使用什么编程语言都可以编写读写文件的程序。

主窗口工具栏上“保存 dat 文件“按钮将表格中的数据保存为标准编码文件,文件后缀是”.dat“。保存 dat 文件的代码是:

void MainWindow::on_actSaveBin_triggered()
{//保存二进制文件
    QString curPath=QDir::currentPath();
    //调用打开文件对话框选择一个文件
    QString aFileName=QFileDialog::getSaveFileName(this,tr("选择保存文件"),curPath,
                                                   "二进制数据文件(*.dat)");
    if (aFileName.isEmpty())
        return; //

    if  (saveBinaryFile(aFileName)) //保存为流数据文件
        QMessageBox::information(this,"提示消息","文件已经成功保存!");
}
bool MainWindow::saveBinaryFile(QString &aFileName)
{ //保存为纯二进制文件
    QFile aFile(aFileName);  //以文件方式读出
    if (!(aFile.open(QIODevice::WriteOnly)))
        return false;

    QDataStream aStream(&aFile); //用文本流读取文件
//    aStream.setVersion(QDataStream::Qt_5_9); //无需设置数据流的版本
    aStream.setByteOrder(QDataStream::LittleEndian);//windows平台
//    aStream.setByteOrder(QDataStream::BigEndian);//QDataStream::LittleEndian

    qint16  rowCount=theModel->rowCount();
    qint16  colCount=theModel->columnCount();

    aStream.writeRawData((char *)&rowCount,sizeof(qint16)); //写入文件流
    aStream.writeRawData((char *)&colCount,sizeof(qint16));//写入文件流


//获取表头文字
    QByteArray  btArray;
    QStandardItem   *aItem;
    for (int i=0;i<theModel->columnCount();i++)
    {
        aItem=theModel->horizontalHeaderItem(i); //获取表头item
        QString str=aItem->text(); //获取表头文字
        btArray=str.toUtf8(); //转换为字符数组
        aStream.writeBytes(btArray,btArray.length()); //写入文件流,长度uint型,然后是字符串内容
    }

//获取数据区文字,
    qint8   yes=1,no=0; //分别代表逻辑值 true和false
    for (int i=0;i<theModel->rowCount();i++)
    {
        aItem=theModel->item(i,0); //测深
        qint16 ceShen=aItem->data(Qt::DisplayRole).toInt();//qint16类型
        aStream.writeRawData((char *)&ceShen,sizeof(qint16));//写入文件流

        aItem=theModel->item(i,1); //垂深
        qreal chuiShen=aItem->data(Qt::DisplayRole).toFloat();//qreal 类型
        aStream.writeRawData((char *)&chuiShen,sizeof(qreal));//写入文件流

        aItem=theModel->item(i,2); //方位
        qreal fangWei=aItem->data(Qt::DisplayRole).toFloat();
        aStream.writeRawData((char *)&fangWei,sizeof(qreal));

        aItem=theModel->item(i,3); //位移
        qreal weiYi=aItem->data(Qt::DisplayRole).toFloat();
        aStream.writeRawData((char *)&weiYi,sizeof(qreal));

        aItem=theModel->item(i,4); //固井质量
        QString zhiLiang=aItem->data(Qt::DisplayRole).toString();
        btArray=zhiLiang.toUtf8();
        aStream.writeBytes(btArray,btArray.length()); //写入长度,uint,然后是字符串
//        aStream.writeRawData(btArray,btArray.length());//对于字符串,应使用writeBytes()函数

        aItem=theModel->item(i,5); //测井取样
        bool quYang=(aItem->checkState()==Qt::Checked); //true or false
        if (quYang)
            aStream.writeRawData((char *)&yes,sizeof(qint8));
        else
            aStream.writeRawData((char *)&no,sizeof(qint8));
    }

    aFile.close();
    return true;
}
  • 字节序:需要用 setByteOrder()函数指定字节序,并且与写入文件时用的字节序一致。
  • writeRawData() 函数:
  • writeBytes() 函数:在将字符串数据写入文件时,使用的是 writeBytes() 函数,而不是 writeRawData(),writeBytes() 函数的原型定义为 QDataStream &DataStream::writeBytes(const char *s, uint len)其中参数 s 是一个指向字节型数据的指针,len 是字节数据的长度 。writeBytes()在写入数据时,会先将 len 作为一个 quint32 类型写入数据流,然后在写入 len 个从指针 s 获取的数据。writeBytes()适合于写入字符串数据,因为在写入字符串之前要先写入字符串的长度,这样在读取文件时,就能知道字符串的长度,以便正确读取字符串。

读取 dat 文件

对于保存的 dat 文件,主窗口的“打开 dat 文件”可以打开保存的 dat 文件,下面是打开 dat 文件的代码:

void MainWindow::on_actOpenBin_triggered()
{//打开二进制文件
    QString curPath=QDir::currentPath();//系统当前目录
    QString aFileName=QFileDialog::getOpenFileName(this,tr("打开一个文件"),curPath,
                                                   "二进制数据文件(*.dat)");
    if (aFileName.isEmpty())
        return; //

    if  (openBinaryFile(aFileName)) //保存为流数据文件
        QMessageBox::information(this,"提示消息","文件已经打开!");
}
bool MainWindow::openBinaryFile(QString &aFileName)
{//打开二进制文件
    QFile aFile(aFileName);  //以文件方式读出
    if (!(aFile.open(QIODevice::ReadOnly)))
        return false;

    QDataStream aStream(&aFile); //用文本流读取文件
//    aStream.setVersion(QDataStream::Qt_5_9); //设置数据流的版本
    aStream.setByteOrder(QDataStream::LittleEndian);
//    aStream.setByteOrder(QDataStream::BigEndian);

    qint16  rowCount,colCount;
    aStream.readRawData((char *)&rowCount, sizeof(qint16));
    aStream.readRawData((char *)&colCount, sizeof(qint16));

    this->resetTable(rowCount);


    //获取表头文字,但是并不利用
    char *buf;
    uint strLen;  //也就是 quint32
    for (int i=0;i<colCount;i++)
    {
        aStream.readBytes(buf,strLen);//同时读取字符串长度,和字符串内容
        QString str=QString::fromLocal8Bit(buf,strLen); //可处理汉字
    }

//获取数据区数据
    QStandardItem   *aItem;

    qint16  ceShen;
    qreal  chuiShen;
    qreal  fangWei;
    qreal  weiYi;
    QString  zhiLiang;
    qint8   quYang; //分别代表逻辑值 true和false
    QModelIndex index;

    for (int i=0;i<rowCount;i++)
    {
        aStream.readRawData((char *)&ceShen, sizeof(qint16)); //测深
        index=theModel->index(i,0);
        aItem=theModel->itemFromIndex(index);
        aItem->setData(ceShen,Qt::DisplayRole);

        aStream.readRawData((char *)&chuiShen, sizeof(qreal)); //垂深
        index=theModel->index(i,1);
        aItem=theModel->itemFromIndex(index);
        aItem->setData(chuiShen,Qt::DisplayRole);

        aStream.readRawData((char *)&fangWei, sizeof(qreal)); //方位
        index=theModel->index(i,2);
        aItem=theModel->itemFromIndex(index);
        aItem->setData(fangWei,Qt::DisplayRole);

        aStream.readRawData((char *)&weiYi, sizeof(qreal)); //位移
        index=theModel->index(i,3);
        aItem=theModel->itemFromIndex(index);
        aItem->setData(weiYi,Qt::DisplayRole);

        aStream.readBytes(buf,strLen);//固井质量
        zhiLiang=QString::fromLocal8Bit(buf,strLen);
        index=theModel->index(i,4);
        aItem=theModel->itemFromIndex(index);
        aItem->setData(zhiLiang,Qt::DisplayRole);

        aStream.readRawData((char *)&quYang, sizeof(qint8)); //测井取样
        index=theModel->index(i,5);
        aItem=theModel->itemFromIndex(index);
        if (quYang==1)
            aItem->setCheckState(Qt::Checked);
        else
            aItem->setCheckState(Qt::Unchecked);
    }

    aFile.close();
    return true;
}
  • 字节序:在流创建后,需要用 setByteOrder()函数指定字节序,并且与写入文件时用的字节序一致。
  • readRawData() 函数:在读取基本类型的数据时,使用 QDataStream 的 readRawData() 函数,该函数原型为 int QDataStream::readRawData(char* s, int len),他会读取 len 个字节的数据,并且保存到指针 s 指向的存储区。例如:
qint16 rowCount;
aStream.readRawData((char*)& rowCount, sizeof(qint16);
  • readBytes() 函数:读取字符串时使用 readByres() 函数时,会自动读取前 4 个字节数据作为 quint32 的数据,并复制给 len 参数,因为 len 是引用方式传递的参数,所以,len 返回读取的数据的字节数。然后根据 len 的大小读取相应字节的数据,存储到指针 s 指向的存储区。
    参考资料:https://it.0voice.com
11-12 10:03