12. 文件操作

在Linux中有一句话叫 一切皆文件

Linux 中所有内容都是以文件的形式保存和管理的: 普通文件(比如txt文本文件,pdf文件,PNG/JPG,MP3/MP4等等)是文件, 目录(文件夹)是文件, 硬件设备(键盘、监视器、硬盘、打印机)是文件, 套接字(socket)、管道(pipe)等资源也都是文件。

在Qt中提供了能够使用文件相关的类,特定的方法来读写操作,首先我们来看看文件相关的知识。

12.1. 文件

所谓文件,我们可以理解为存储在磁盘上的数据,这些数据在计算机中表示为01序列,也就是二进制。 为了方便计算机里的运算、书写以及统计,2、4、8、16、32、64这类进制数也就应运而生。 我们将四个位的二进制数组合在一起就是一个十六进制,0-F 分别表示 0000-1111

不管是01序列还是十六进制对于我们人而言,要想识别一连串进制数的含义都比较困难, 于是人就凭借自己的智慧想到了用特定的进制数来表示一个字符,比如规定 00110000 来表示字符 0 01000001 来表示字符 A ,这就是ASCII码的产生,也逐渐形成了一个约定——字节(即8个二进制位)。

下面为ASCII码对照表,图片来自网上。

file001

将这些特定的进制数组合就能表达出更丰富的含义,同时也很容易被识别,从一串二进制数到可识别的字符,我们称为字符编码。

在人类的发展历程上,产生了非常多的具有特定含义的字符,比如中文的汉字,甲骨文。 而在上面的ASCII码对照表中没有将一串二进制数识别为汉字的标准,于是就产生了GB2312,BIG5等等中文字符编码和字符集。 同样其他国家也产生了相应的标准,随着计算机发展、网络连接,各个国家都接入了遍布全球的互联网,不同的标准就会造成混乱,比如乱码。 于是聪明的人们又想到用同一套字符编码和字符集来解决这个问题,于是就有了UTF-8和Unicode。

有了字符编码,计算机中的数据就很容易解析和识别,这并不意味着计算机就直接存储着我们能够识别字符,而依然是01序列。 因此我们在读取文件时最开始得到的数据就是01序列,在读取之后再对01序列进行解码、解析。为了方便解析,我们直接通过文件类型来区分文件。 比如txt后缀的文件通常就是以某种文字编码的文本,MP3、JPG等就是符合特定二进制编码的文件, 修改文件后缀名并不会直接改变文件内部数据和格式,而仅仅只是指明当前操作系统对文件的操作方式。

类Unix操作系统,如Linux就是将设备当作特殊文件来处理的,普通文件和设备文件都是由字节序列而构成的信息载体。

12.2. 输入输出类

12.2.1. QIODevice

QIODevice 是一个抽象类(不能实例化),它为支持读写数据块的设备提供了通用的实现和规范的抽象接口,是所有输出输出类设备(input/output device)的基础类。 其他用到IO设备的类如QFile、QBuffer、QTcpSocket,都是从这个类继承而来。

访问IO设备,需要先调用open()来设置正确的 OpenMode (例如ReadOnly或ReadWrite),一些模式说明,如下表:

模式

描述

QIODevice::NotOpen

0x0000

设备没有打开

QIODevice::ReadOnly

0x0001

只读模式打开设备

QIODevice::WriteOnly

0x0002

只写模式打开设备,默认覆盖内容

QIODevice::ReadWrite

ReadOnly | WriteOnly

读写模式打开设备

QIODevice::Append

0x0004

追加模式打开设备,数据将会写入设备末尾

QIODevice::Truncate

0x0008

覆盖模式打开设备,数据将会从设备开头覆盖写入,原有数据全部清除

QIODevice::Text

0x0010

文本模式打开设备

打开设备后后,使用write()或putChar()写入数据到文件和设备,并通过调用read(),readLine()或readAll()进行读取;使用完设备后,还需调用close()。

QIODevice区分两种类型的设备:随机访问设备(Random-access devices)和顺序设备(Sequential devices):

  • 随机访问设备,支持使用 seek() 搜索任意位置,文件中的当前位置可通过调用pos()获得,例如QFile和QBuffer就是随机访问设备;

  • 顺序设备,不支持查询任意位置,必须一次性读取数据,例如QTcpSocket和QProcess就是顺序设备。

可以使用 isSequential() 函数来判断设备的类型。

QIODevice在使用的时候,当有新数据可读取时,QIODevice发出readyRead()信号,调用bytesAvailable()查看当前能读取的字节数; 每次将有效负载数据写入设备时,QIODevice都会发出bytesWritten()信号,调用bytesToWrite()来查看待写入的字节数。

QIODevice的相关类继承关系:

qtfile

12.2.2. QBuffer

QBuffer 类将QByteArray对象当做一个IO设备来读写, 默认情况下,创建QBuffer时会创建一个内部QByteArray缓冲区,我们可以通过QIODevice提供的通用接口来操作该缓冲区。

QBuffe缓冲区一般用在:在线程间进行不同类型的数据传递;缓存外部设备中的数据返回;数据读取速度小于数据写入速度。

12.3. 文件操作相关类

在Qt中提供了与文件操作相关的类,通过这些类我们可以获取文件信息,修改文件名称,删除,复制等操作。

12.3.1. QFile

QFile 是用于文件的创建、读写、复制、删除等等,可以实现文本文件和二进制文件的读写。

QFile的接口函数可以参考下Qt帮助文档,下面程序是读取普通文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//创建 QFile 对象,同时指定要操作的文件
QFile file("~/test.txt");

//打开文件,以只读方式,并且读取文件时行尾结束符转换成'\n',写入时会将行尾结束符转换成本地格式(Unix 系统中是 "\n",Windows 系统中是 "\r\n")
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
    return;

//向文件中写入
file.write("野火嵌入式Qt应用开发实战\n");
file.write("https://doc.embedfire.com/linux/rk356x/Qt/zh/latest/index.html");

//关闭文件
file.close();

//重新打开文件,对文件进行读操作
if(!file.open(QIODevice::ReadOnly|QIODevice::Text))
    return;

while (!file.atEnd()) {
    QByteArray line = file.readLine();
    process_line(line);  //处理获取到的一行字符串
}

//关闭文件
file.close();

使用QFile类读写文件,只有一些基础的文件读写函数,使用不是很方便, Qt还提供了另外两个文件流操作类 QTextStreamQDataStream , 前者用来读写文件文件,后者用来读写二进制文件,QFile可以和它们搭配使用,从整体上提高读写文件的开发效率。

12.3.2. QTextSteam

QTextStream 类提供了用于读取和写入文本的便捷接口。 QTextStream可以操作QIODevice, QByteArray 和 QString,调用QTextStream的流操作可以方便的读取文字、行、数字等,还提供了文本填充和对齐的相关格式。

QTextStream内部使用基于Unicode的缓冲区,并且QTextStream使用 QTextCodec 自动支持不同的字符集。 默认情况下,QTextCodec::codecForLocale()用于读取和写入,但是您也可以通过调用setCodec()来设置编解码器。

QTextStream还支持自动Unicode检测,启用此功能(默认行为)后,QTextStream将检测UTF-16或UTF-32 BOM(Byte Order Mark,字节顺序标记),并在读取时切换到适当的UTF编解码器。 QTextStream默认情况下不编写BOM,但是您可以通过调用setGenerateByteOrderMark(true)来启用它,当QTextStream直接在QString上运行时,将禁用编解码器。

lubancat_qt_tutorial_code/File/mainwindows.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 用文本流写入指定文件
void MainWindow::on_btn_QTextStream_write_clicked()
{
    QFile file(m_textstreampath);
    if (!file.open(QIODevice::WriteOnly|QIODevice::Text))
        return;

    ui->lineEdit->setText(m_textstreampath);
    QTextStream out(&file);               //用文本流读取文件内容
    out << ui->textEdit->toPlainText();
    file.close();
}

// 用文本流读取指定文件内容
void MainWindow::on_btn_QTextStream_read_clicked()
{
    QFile file(m_textstreampath);
    if (!file.open(QFile::ReadOnly|QIODevice::Text))
        return;

    QTextStream in(&file);            //用文本流读取文件内容
    ui->lineEdit->setText(m_textstreampath);
    ui->textEdit->clear();
    QString str = in.readAll();       //读取全部内容
    ui->textEdit->append(str);        //显示内容
    file.close();
}

void MainWindow::on_cBox_Codec_currentIndexChanged(const QString &arg1)
{
    QTextCodec::setCodecForLocale(QTextCodec::codecForName(arg1.toLocal8Bit()));
}

12.3.3. QDataSteam

QDataStream 类用于对数据进行二进制格式的读/写操作。 QDataStream可以序列化C++的基本数据类型的功能,也就是可以将数据按照某种特定的二进制编码进行解析或存储,另外也可以读取/写入未编码的原始二进制数据。

QDataStream 以数据流的方式读写文件,使用Qt预定义的编码方式或者是原始二进制的数据方式, 下面读取和写入文件,先使用QFile这种普通读写,然后转成数据流或者字节流读取,这样便于操作,而且不会乱码。

lubancat_qt_tutorial_code/File/mainwindows.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void MainWindow::on_btn_QDataStream_write_clicked()
{
    static int writeCount=0;

    QFile file(m_datastreampath);
    ui->lineEdit->setText(m_datastreampath);
    if(file.open(QIODevice::WriteOnly))
    {
        QDataStream out(&file);
        out << ui->textEdit->toPlainText();
        out << ++writeCount;
        file.close();
    }
}

void MainWindow::on_btn_QDataStream_read_clicked()
{
    QFile file(m_datastreampath);
    ui->lineEdit->setText(m_datastreampath);
    if(file.open(QIODevice::ReadOnly))
    {
        QDataStream in(&file);
        QString str;
        int writeCount;
        in >> str >> writeCount;
        file.close();

        ui->textEdit->clear();
        ui->textEdit->append(str);
        ui->textEdit->append(QString::number(writeCount));
    }
}

12.3.3.1. 文件夹相关类

上面我们了解到了文件的概念,计算机种存储着大量的文件和数据,为了方便管理就引入了文件系统。 文件系统实现对磁盘空间的统一管理,一方面文件系统对磁盘空间进行统一规划,另外一方面文件系统提供给普通用户人性化的接口,我们通过文件系统就能够对文件进行增删改查。

文件系统对文件的管理通常是利用文件夹来实现的, 简单点说文件夹就是一种特殊的文件,内容就是文件列表和文件的索引信息。

12.3.4. QDir

QDir 用于操纵路径名,访问有关路径和文件的信息以及操纵底层文件系统。

创建文件夹:

lubancat_qt_tutorial_code/File/mainwindows.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void MainWindow::on_btn_creatdir_clicked()
{
    QString curPath=QDir::currentPath();//获取应用程序的路径
    QString dlgTitle="选择文件夹"; //对话框标题
    QString dirpath=QFileDialog::getExistingDirectory(this,dlgTitle,curPath);
    if (dirpath.isEmpty())
        return;

    dlgTitle="新建文件夹";
    QString str="新建文件夹";
    QString defaultstr="NEWDIR";
    QLineEdit::EchoMode echoMode=QLineEdit::Normal;

    bool choose=false;
    QString dirinput = QInputDialog::getText(this, dlgTitle,str, echoMode,defaultstr, &choose);
    if (choose)
    {
        if(dirinput.isEmpty())
            return;
    }

    QDir *dir = new QDir;
    if(!dir->exists(dirpath+"/"+dirinput))
    {
        dir->mkdir(dirpath+"/"+dirinput);
    }
}

遍历文件夹:

lubancat_qt_tutorial_code/File/mainwindows.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void MainWindow::on_btn_dirinfo_clicked()
{
    QString curPath=QDir::currentPath();//获取应用程序的路径
    QString dlgTitle="选择文件夹"; //对话框标题
    QString dirpath=QFileDialog::getExistingDirectory(this,dlgTitle,curPath);
    if (dirpath.isEmpty())
        return;

    QDir dir(dirpath);
    QStringList files = dir.entryList();

    ui->textEdit->clear();
    foreach(QString path,files)
    {
        QFileInfo info(path);
        if(!info.baseName().isEmpty())
        {
            ui->textEdit->append("文件名:"+info.baseName()+" "+"绝对路径:"+info.absoluteFilePath());
        }
    }
}

12.3.5. QFileInfo

QFileInfo类提供与系统无关的文件信息, 获取关文件在文件系统中的名称和位置(路径),以及文件的权限和文件类型,文件的大小和上次修改/读取的时间也通过接口获取。 QFileInfo可以指向具有相对或绝对文件路径的文件。

lubancat_qt_tutorial_code/Data/File/mainwindows.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void MainWindow::on_btn_QFileInfo_file_clicked()
{
    QString curPath=QDir::currentPath();//获取应用程序的路径
    QString dlgTitle="选择文件"; //对话框标题
    QString filter="文本文件(*.txt);;图片文件(*.jpg *.gif *.png);;所有文件(*.*)"; //文件过滤器
    QString filename=QFileDialog::getOpenFileName(this,dlgTitle,curPath,filter);
    if (filename.isEmpty())
        return;

    QFileInfo fileinfo(filename);
    if(!fileinfo.exists())
        return;

    if(fileinfo.isSymLink())
    {
        fileinfo.setFile(fileinfo.symLinkTarget());
    }

    ui->textEdit->clear();
    ui->textEdit->append("文件名:"+fileinfo.baseName()+" "+"文件格式:"+fileinfo.completeSuffix()+" "+"文件大小:"+QString::number(fileinfo.size()));
    ui->textEdit->append("绝对路径:"+fileinfo.absoluteFilePath());
    ui->textEdit->append("文件所有者:"+fileinfo.owner()+" "+"所有者ID:"+QString::number(fileinfo.ownerId())+" "+"文件所属组:"+fileinfo.group()+" "+"所属组ID:"+QString::number(fileinfo.groupId()));
    ui->textEdit->append("文件权限:"+QString::number(fileinfo.permissions()));
    ui->textEdit->append("文件创建时间:"+fileinfo.birthTime().toString("yyyy-MM-dd hh:mm:ss"));
    ui->textEdit->append("上次修改时间:"+fileinfo.lastModified().toString("yyyy-MM-dd hh:mm:ss"));
    ui->textEdit->append("上次读取时间:"+fileinfo.lastRead().toString("yyyy-MM-dd hh:mm:ss"));
    ui->textEdit->append("元数据更时间:"+fileinfo.metadataChangeTime().toString("yyyy-MM-dd hh:mm:ss"));
}

12.3.6. QFileSystemWatcher

QFileSystemWatcher通过观察指定路径的列表来监视文件系统中文件和目录的更改, 当文件已被修改,重命名或从磁盘中删除就会发送 fileChanged()。 类似地,当目录或其内容被修改或删除时,会发出directoryChanged()信号。

需要注意的是,一旦文件被重命名或从硬盘删除或者是目录从磁盘上删除,QFileSystemWatcher将停止监控。 QFileSystemWatcher的使用示例:

lubancat_qt_tutorial_code/Data/File/mainwindows.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void MainWindow::on_btn_Watch_clicked()
{
    QString curPath=QDir::currentPath();//获取应用程序的路径
    QString dlgTitle="选择文件夹"; //对话框标题
    QString dirname=QFileDialog::getExistingDirectory(this,dlgTitle,curPath);
    if (dirname.isEmpty())
        return;

    QFileSystemWatcher *fileSystemWatcher = new QFileSystemWatcher(this);
    fileSystemWatcher->addPath(dirname);

    connect(fileSystemWatcher,SIGNAL(directoryChanged(const QString &)),this, SLOT(handDirChanged(const QString &)));
    connect(fileSystemWatcher,SIGNAL(fileChanged(const QString &)),this, SLOT(handFileChanged(const QString &)));
}

void MainWindow::handFileChanged(const QString &path)
{
    QString dlgTitle="information";
    QString str="文件改变:"+path;
    QMessageBox::information(this, dlgTitle, str,QMessageBox::Yes);
}

void MainWindow::handDirChanged(const QString &path)
{
    QString dlgTitle="information";
    QString str="文件夹改变:"+path;
    QMessageBox::information(this, dlgTitle, str,QMessageBox::Yes);
}

12.3.7. QTemporaryFile和QTemporaryDir

QTemporaryFile用于安全地创建唯一的临时文件,该文件本身是通过调用open()创建的。

  • 临时文件的名称保证是唯一的(即,保证您不会覆盖现有文件),并且在销毁QTemporaryFile对象时将删除该文件。

  • 临时文件的打开方式为QIODevice::ReadWrite

  • 这样可避免将数据存储在临时文件中的应用程序破坏数据。

使用QTemporaryFile创建临时文件非常简单,可以使用静态函数QTemporaryFile::createTemplateFile()创建文件,会返回一个QTemporaryFile对象。 可以像普通文件一样进行读写操作,使用open()打开,当不再需要使用临时文件时,调用close() 方法关闭文件即可。

需要注意的是,临时文件在程序结束后会被自动删除(QTemporaryFile对象销毁时),因此不需要手动删除临时文件。 如果需要手动控制临时文件的删除时间,可以使用 QFile::setAutoRemove() 方法设置是否自动删除文件。

示例程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <QTemporaryFile>
#include <QFile>

// 创建临时文件
QTemporaryFile file;
if (!file.open()) {
    qDebug() << "Failed to create temporary file.";
    return -1;
}

// 写入数据到临时文件
QFile tempFile(file.fileName());
if (!tempFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
    qDebug() << "Failed to open temporary file for writing.";
    return -1;
}
tempFile.write("Test, temporary file", -1);
tempFile.close();

// 读取临时文件中的数据并输出
if (!file.open()) {
    qDebug() << "Failed to open temporary file for reading.";
    return -1;
}
qDebug() << "Temporary file content:" << file.readAll();
file.close();

// 查看文件信息
QFileInfo info(tempFile);

qDebug() << info.isFile();
qDebug() << info.path();
qDebug() << info.fileName();

同样,QTemporaryDir用于安全地创建唯一的临时目录,操作类似,参考下Qt帮助手册

12.3.8. QCoreApplication

QCoreApplication 是无UI应用程序提供事件循环的类,是所有应用程序的基类。 对于GUI程序,应该用QGuiApplication,而对于采用了Qt Widget模块的程序,应该使用QApplication,例如:我们使用Qt Creator创建的Qt Widget Application都是基于QApplication。

它们之间的继承关系是:QCoreApplication -> QGuiApplicatioin -> QApplication。

QCoreApplication提供了一些有用的静态函数,可以用来获取应用程序的名称,启动路径等信息。 继承于此类的子类,也可以使用QCoreApplication的相关方法,如applicationDirPath()获取可执行应用文件所在目录;applicationFilePath()获取可执行应用文件路径。

lubancat_qt_tutorial_code/Data/File/mainwindows.cpp
1
2
3
4
// 默认文件路径和名称
m_datastreampath = qApp->applicationDirPath()+"/datastream.dat";
m_textstreampath = qApp->applicationDirPath()+"/textstream.txt";
m_filepath = qApp->applicationDirPath()+"/document";

12.4. 例程说明

例程演示Qt文件相关类的使用,例如QFile打开关闭文件,使用QFile和QTextSteam读写文件等。

12.4.1. 测试例程

在PC ubuntu20.04上,打开Qt Creator点击编译运行例程,使用 Desktop Qt 5.15.2 GCC 64bit 编译套件,构建编译后运行:

result001

在lubancat_rk板卡上运行,选择 Lubancat_rk_debian10 交叉编译套件,编译出可执行程序后,复制到板卡运行。