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

实例功能概述

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

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

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

实例中以表格编辑一个数据表,采用 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 类中自定义的一些变量和函数,具体如下:

cpp 复制代码
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 预定义编码方式保存文件。此按钮响应代码如下:

cpp 复制代码
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 枚举类型的值。

cpp 复制代码
aStream.setVersion(QDataStream::Qt_5_9);

注意:以 Qt 的预定义类型编码保存的文件需要指定流版本号,因为每个版本的 Qt 对数据类型的编码可能有差别,需要保证写文件和读文件的流版本兼容。

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

cpp 复制代码
aStream<<rowCount; //写入文件流,行数
aStream<<colCount; //写入文件流,列数

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

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

读取 stm 文件

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

cpp 复制代码
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 文件的代码是:

cpp 复制代码
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 文件的代码:

cpp 复制代码
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 指向的存储区。例如:
cpp 复制代码
qint16 rowCount;
aStream.readRawData((char*)& rowCount, sizeof(qint16);
  • readBytes() 函数:读取字符串时使用 readByres() 函数时,会自动读取前 4 个字节数据作为 quint32 的数据,并复制给 len 参数,因为 len 是引用方式传递的参数,所以,len 返回读取的数据的字节数。然后根据 len 的大小读取相应字节的数据,存储到指针 s 指向的存储区。
    参考资料:https://it.0voice.com
相关推荐
SunkingYang2 小时前
QT程序如何将事件和消息发送给MFC程序,MFC程序如何接收消息和事件
qt·mfc·消息·事件·通信·通讯·传递
凯子坚持 c4 小时前
Qt 5.14.0 入门框架开发全流程深度解析
开发语言·qt
深蓝海拓4 小时前
PySide6从0开始学习的笔记(十四)创建一个简单的实用UI项目
开发语言·笔记·python·qt·学习·ui·pyqt
小尧嵌入式4 小时前
Linux网络介绍网络编程和数据库
linux·运维·服务器·网络·数据库·qt·php
海涛高软5 小时前
Qt中使用QListWidget列表
开发语言·qt
010米粉0105 小时前
Qt之构建方式
qt
凯子坚持 c6 小时前
Qt 信号与槽机制深度解析
开发语言·qt
世转神风-6 小时前
qt-初步编译运行报错-When executing step “Make“-无法启动进程“make“
开发语言·qt
一然明月17 小时前
QT之基础控件
开发语言·qt
深蓝海拓1 天前
PySide6从0开始学习的笔记(十) 样式表(QSS)
笔记·python·qt·学习·pyqt