7.5 其他文件操作类

Qt Core 内核模块帮助文档有专门的链接页面 Input/Output and Networking,介绍输入输出和网络相关的类,本章的几节内容主要介绍了文件相关的类,其他的如图片读写、网络传输、进程读写,等到相应的章节再介绍。
本节介绍文件操作相关的其他类,比如存储器信息类 QStorageInfo、丰富文本文件写入类 QTextDocumentWriter、临时文件操作 QTemporaryDir 和 QTemporaryFile ,然后通过两个示例学习一下如何运用这几个类。

7.5.1 QStorageInfo

存储器信息类 QStorageInfo 是从 Qt 5.4.0 版本开始新增的类,主要用于枚举存储器分区列表。枚举分区的时候要区分存储器分区和文件系统两个 概念,对于 Windows 系统,通常每个磁盘分区对应一个文件系统根,如 C:、D:、E: 等就是分区的挂载点(也是多个文件系统根):
windows
对于 Linux 系统,各个磁盘分区通常挂载到文件夹,文件系统根只有一个 / ,/ 既是文件系统根,也是操作系统安装的根:
linux
Qt 类库中,QDir 和 QFileInfo 等类只与文件系统有关,而不关心磁盘分区有几个。QStorageInfo 就是专门查询存储器信息,包括磁盘、 光盘、U盘等分区信息。

QStorageInfo 大致两种用途,第一种是使用静态函数查询有几个分区:
QList<QStorageInfo> QStorageInfo::​mountedVolumes()
该函数返回存储器分区列表,列表每个元素对应一个分区。另外还可以用静态函数 root() 获取操作系统安装的根分区:
QStorageInfo QStorageInfo::​root()

第二种用途是根据一个文件路径,构造 QStorageInfo 对象,查询包含该路径的存储器分区信息:
QStorageInfo::​QStorageInfo(const QString & path)
QStorageInfo::​QStorageInfo(const QDir & dir)
除了构造函数设置文件路径,setPath() 函数也可以改变要查询的文件路径:
void QStorageInfo::​setPath(const QString & path)

如上两种用途都会得到 ​QStorageInfo 类的对象,然后就可以使用该类的函数查询分区信息,查询之前一般先判断当前分区是否已经可用:
bool QStorageInfo::​isValid() const //当前分区是否已挂载到文件系统
bool QStorageInfo::​isReady() const //当前分区的文件系统是否正常工作
然后可以查询分区的大小、空余空间、可用空间等信息:
qint64 QStorageInfo::​bytesTotal() const  //分区总大小,单位字节
qint64 QStorageInfo::​bytesFree() const //分区剩余空闲空间,单位字节
qint64 QStorageInfo::​bytesAvailable() const //当前用户可以使用该分区中多大空闲空间,单位字节
第三个 bytesAvailable() 与磁盘分区的配额有关,用户配额越大,能使用的空余空间就越大,一般 Linux 可能会使用到用户配额管理,Windows 通常不会用到。
除了空间信息,还可以查询分区的标签名、文件系统类型、设备名、挂载点等:
QByteArray QStorageInfo::​fileSystemType() const //Windows常见的文件系统类型是NTFS,Linux常用ext3等
QString QStorageInfo::​name() const //比如 C: 盘标签叫 System,D: 盘标签叫 Programs,标签名有可能为空
QString QStorageInfo::​displayName() const //有标签名就显示标签名,没标 签名显示盘符名
bool QStorageInfo::​isReadOnly() const //是否为只读分区或者无权限写入,如光盘分区
bool QStorageInfo::​isRoot() const //是否为操作系统安装的根
QByteArray QStorageInfo::​device() const //设备名,Linux系统常见为 /dev/sda* ,Windows 是一长串设备代码
QString QStorageInfo::​rootPath() const //分区的挂载点

关于 QStorageInfo 就介绍到这,7.5.4 节例子就是查询磁盘、光驱等分区信息。

7.5.2 QTextDocumentWriter

QTextEdit 和 QPlainTextEdit 都使用 QTextDocument 管理文档内容,QTextDocumentWriter 可以将 QTextDocument 管理的文档内容写入到文件中,目前支持写入三种文件格式:plaintext(普通文本,如*.txt),HTML(丰富格式文本,*.htm 或*.html),ODF(开放办公文档格式,LibreOffice、OpenOffice文档格式)。对于 QTextDocument 对象,目前是管写不管读,因为写入简单,读取比较麻烦。

QTextDocumentWriter 使用方式非常简单,首先构造一个文档写入对象:
QTextDocumentWriter(QIODevice * device, const QByteArray & format)
QTextDocumentWriter(const QString & fileName, const QByteArray & format = QByteArray())
第一个构造函数的 device 是已经打开的写入文件对象,第二个构造函数的 fileName 是要写入的文件名。format 就是三种格式字符串其中之一:"plaintext" 、"HTML" 或者 "ODF" 。
如果构造函数未指定写入文件格式,可以用如下函数来设置或更改格式:
void QTextDocumentWriter::​setFormat(const QByteArray & format)
如果要更改写入的文件对象或文件名,可以用如下函数:
void QTextDocumentWriter::​setDevice(QIODevice * device)
void QTextDocumentWriter::​setFileName(const QString & fileName)
有了写入文件和写入格式之后,一般是从 QTextEdit 或 QPlainTextEdit 编辑器获取文档对象指针:
QTextDocument * QTextEdit::document() const
QTextDocument * QPlainTextEdit::document() const
最后调用 QTextDocumentWriter 对象的写入函数即可:
bool QTextDocumentWriter::​write(const QTextDocument * document)
如果写入成功会返回 true,写入失败就返回 false。
另外还有一个写入文档片段的函数:
bool QTextDocumentWriter::​write(const QTextDocumentFragment & fragment)
文档片段一般从剪贴板或者根据用户选中的高亮部分获取,本节不涉及文档片段写入。

对于文本文件,"plaintext" 和 "HTML" 格式的,可能涉及到文本的字符编码函数,Qt 默认使用 UTF-8 格式的,如果需要改变文本编码,可以自己在文档写入之前设置:
void QTextDocumentWriter::​setCodec(QTextCodec * codec)   //修改文本编码
QTextCodec * QTextDocumentWriter::​codec() const    //获取当前编码格式
指定文本编码格式的对象指针一般用如下静态函数获取:
QTextCodec * QTextCodec::​codecForName(const char * name)  //静态函数,根据编码格式名称返回文本编码器

7.5.3 QTemporaryDir和QTemporaryFile

临时文件夹和临时文件在网络传输、数据处理等方面的程序应用较多,比如网页浏览器常使用临时文件夹和临时文件缓存页面。Qt 提供 QTemporaryDir 类支持临时文件夹,提供 QTemporaryFile 类支持临时文件,下面介绍这两个类。

(1)QTemporaryDir
临时文件夹类有两个构造函数,首先看第一个:
QTemporaryDir()
第一个无参数的构造函数较为常用,它会自动根据当前应用程序名字和操作系默认统的临时文件路径构造唯一的临时文件夹,比如应用程序名字为 "testtemp",操 作系统默认的临时文件路径为
"C:/Documents and Settings/Administrator/Local Settings/Temp"
那么生成的临时文件夹就可能为:
"C:/Documents and Settings/Administrator/Local Settings/Temp/testtemp-P1XkOA"
临时文件夹末尾是六个随机字母或数字,用于确保该临时文件夹的唯一性,不会与现有文件夹重名。

第二个构造函数有参数:
QTemporaryDir(const QString & templatePath)
templatePath 是路径模板,如果路径模板是相对路径形式,那么生成的临时文件夹就是相对于应用程序工作路径,如果路径模板是绝对路径形式,那么就是绝 对路劲里面创建临时文件夹。一般使用相对路径形式即可,比如:
QTemporaryDir td("abcd");
那么生成的临时文件夹就在工作路径(QtCreator调试时为影子构建路径)里面:
"abcdl9Mfi9"
这个文件夹名中,"abcd" 是打头的模板,后面接六个随机的字母或数字确保唯一性。

构造临时文件夹之后,一般需要判断该临时文件夹是否可用:
bool QTemporaryDir::​isValid() const
程序运行时可以用如下函数获取该临时文件夹的名称:
QString QTemporaryDir::​path() const
可以在临时文件夹内部新建文件或者做其他操作,但要是注意,QTemporaryDir 对象生命期结束时,析构函数会将该临时文件夹内所有东西清空并删除该临时文件夹,这也是临时文件夹本身的意义所在,就是临时用一下(临时文件夹内部所有东西自动具有“临 时”的特性)。
程序运行时也可以手动删除正在使用的临时文件夹:
bool QTemporaryDir::​remove()
如果希望改变临时文件夹在析构时自动删除的特性,可以用如下函数:
void QTemporaryDir::​setAutoRemove(bool b)  //设置是否自动删除
bool QTemporaryDir::​autoRemove() const    //判断是否会自动删除,默认是析构函数自动删除临时文件夹

(2)QTemporaryFile
如果程序使用的临时文件很少,就不需要临时文件夹,直接定义几个 QTemporaryFile 对象使用即可。QTemporaryFile 是从 QFile 类派生的,拥有 QFile 类全部的读写接口函数,关于 QFile 类的读写操作可以回顾 7.2 节内容,这里不重复了。
QTemporaryFile 构造函数分为两类,第一类不带模板参数的:
QTemporaryFile()
QTemporaryFile(QObject * parent)
parent 是父对象指针。如果不使用模板参数,那么 QTemporaryFile 自动根据应用程序名字和操作系统临时文件路径,构造唯一的临时文件名,还是以上面的应用程序名和系统临时路径为例,得到的临时文件可能为:
"C:/Documents and Settings/Administrator/Local Settings/Temp/testtemp-BAIX6a"
注意这里的临时文件是没有扩展名的,所以看起来和之前的临时文件夹名字差不多,就是尾部随机的六个字母数字不一样。
第二类构造函数为:
QTemporaryFile(const QString & templateName)
QTemporaryFile(const QString & templateName, QObject * parent)
templateName 是模板名,如果模板名使用相对路径,那么是相对于应用程序的工作路径,这点是与临时文件夹类构造函数一样的。如果模板名为 "abcd",那么生成的临时文件名可能为:
"abcdDSSn9G"
打头的是模板名,后面是六个随机的字母和数字。
在 QTemporaryFile 类对象构造之后,可以用如下一对函数获取或者设置模板字符串:
QString QTemporaryFile::​fileTemplate() const
void QTemporaryFile::​setFileTemplate(const QString & name)

QTemporaryFile 类的构造函数只是生成临时文件名,还没有建立真实文件,需要调用打开函数,文件才会建立:
bool QTemporaryFile::​open()
临时文件总是以 QIODevice::ReadWrite 读写模式打开,如果打开成功就返回 true,否则返回 false。
临时文件不会在调用 close() 函数后销毁,只要没进入 QTemporaryFile 对象析构函数,临时文件一直存在,因此临时文件在 close() 函数之后可以重新打开。

程序运行时可以手动调用删除函数删除临时文件:
bool QFile::​remove()
如果不手动调用删除函数,那么临时文件在析构函数里面会自动删除,如果要改变自动删除的特性,使用如下函数:
void QTemporaryFile::​setAutoRemove(bool b)  //设置是否自动删除
bool QTemporaryFile::​autoRemove() const   //是否自动删除
关于临时文件夹和临时文件介绍到这,下面来学习两个示例程序。

7.5.4 磁盘分区信息示例

本小节示例会获取当前系统的存储器分区信息,生成丰富文本显示到 QTextEdit 编辑器里,然后提供保存按钮,将丰富文本信息存到文件中,示范 QStorageInfo 和 QTextDocumentWriter 类的使用。
打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 diskinfo,创建路径 D:\QtProjects\ch07,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
我们打开 widget.ui 界面文件,按照下图拖入一个 QTextEdit 和两个 QPushButton 控件:
ui
编辑器控件对象名默认为 textEdit,右上的按钮文本为 "获取磁盘信息",对象名 pushButtonGetDiskInfo,右下的按钮文本为 "保存磁盘信息",对象名为 pushButtonSaveDiskInfo。
界面布局方式为:先将右边两个按钮按照垂直布局排好,然后点击界面空白位置,不选中任何控件,点击上面工具栏的水平布局图标,设置主布局器为水平布局,就会得到上 图的效果。
进行布局之后,我们为两个按钮都添加 clicked() 信号对应的槽函数:
slot
添加两个槽函数之后,我们保存界面文件,关闭界面文件,回到代码编辑模式。
头文件 widget.h 内容不需要编辑。都是自动生成的,下面贴出来仅供读者对比:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

private slots:
    void on_pushButtonGetDiskInfo_clicked();

    void on_pushButtonSaveDiskInfo_clicked();

private:
    Ui::Widget *ui;
};

#endif // WIDGET_H
下面来编辑源代码文件 widget.cpp,添加需要的代码,首先是头文件包含和构造函数:
#include "widget.h"
#include "ui_widget.h"
#include <QStorageInfo> //获取存储器信息
#include <QTextDocumentWriter> //将信息写入文件
#include <QMessageBox>
#include <QFileDialog>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
}

Widget::~Widget()
{
    delete ui;
}
头文件有本节学习的 QStorageInfo 类和 QTextDocumentWriter 类的头文件,其他头文件都用过多次,就不解释了。
构造函数没有添加代码,读者如果希望编辑器是只读的,可以自行加一句编辑器 ui->textEdit 的 setReadOnly(true) 函数调用。

关于存储器信息的获取代码,都在 "获取磁盘信息" 按钮对应的槽函数里面,这个函数会枚举系统中的各个分区并依次获取各个分区的信息:
void Widget::on_pushButtonGetDiskInfo_clicked()
{
    //获取所有分区信息
    QList<QStorageInfo> listDisks = QStorageInfo::mountedVolumes();
    //信息字符串和临时串,使用 HTML 字符串
    QString strInfo;
    QString strTemp;
    //枚举各个分区信息
    int nCount = listDisks.count();
    for(int i=0; i<nCount; i++)
    {
        // <br> 是换行
        strTemp = tr("分区 %1 信息:<br>").arg(i);
        strInfo += strTemp;
        //设备字符串,这个是固定有的信息
        strTemp = listDisks[i].device();
        strInfo += tr("设备:") + strTemp + tr("<br>");
        //挂载点
        strTemp = listDisks[i].rootPath();
        strInfo += tr("挂载点:") + strTemp + tr("<br>");
        //判断是否可用
        if( (listDisks[i].isValid()) && (listDisks[i].isReady()) )
        {
            //可用的,读取更多信息
            //分区显示名
            strTemp = tr("卷名:%1<br>").arg( listDisks[i].displayName() );
            strInfo += strTemp;
            //文件系统类型
            strTemp = tr("文件系统类型:%1<br>").arg( QString( listDisks[i].fileSystemType() ) );
            strInfo += strTemp;
            //是否为操作系统根
            if( listDisks[i].isRoot() )
            {
                //红色粗体
                strTemp = tr("<font color=red><b>系统根:是</b></font><br>");
            }
            else
            {
                strTemp = tr("系统根:否<br>");
            }
            strInfo += strTemp;
            //是否只读
            if( listDisks[i].isReadOnly() )
            {
                strTemp = tr("只读:是<br>");
            }
            else
            {
                strTemp = tr("只读:否<br>");
            }
            strInfo += strTemp;
            //分区的空间信息
            double dblAllGB = 1.0 * listDisks[i].bytesTotal() /(1024*1024*1024);
            double dblFreeGB = 1.0 * listDisks[i].bytesFree() / (1024*1024*1024);
            strTemp = tr("总空间(GB):%1 已用:%2 剩余:%3<br>")
                    .arg(dblAllGB, 0, 'f', 3)
                    .arg(dblAllGB - dblFreeGB, 0, 'f', 3)
                    .arg(dblFreeGB, 0, 'f', 3);
            strInfo += strTemp;
        }
        else
        {
            //不可用,<b>...</b>字符串是粗体
            strTemp = tr("<b>设备不可用或未加载。</b><br>");
            strInfo += strTemp;
        }
        //下一个分区,换行
        strInfo += tr("<br>");
    }
    //信息收集完毕,显示
    ui->textEdit->setText(strInfo);
}
该槽函数开始直接用静态函数 QStorageInfo::mountedVolumes() 获取分区信息列表,存到 listDisks 。
然后用 for 循环,依次获取每个分区(即各个 QStorageInfo 对象)的信息:
    device() 函数获取设备字符串;
    rootPath() 获取分区的挂载点;
    上面 device() 和 rootPath() 两个函数不需要判断分区可用性就可以直接调用,后面大部分信息是需要判断可用性,在分区可用时,再获取如下信息:
        displayName() 获取显示名,这个函数如果有分区标签就显示分区标签,没分区标签就显示挂载点;
        fileSystemType() 获取文件系统类型,比如 NTFS 或 CDFS 等;
        isRoot() 判断是否为操作系统安装的根分区;
        isReadOnly() 判断是否为只读分区,只读是指可能是无权限写入或者只读的光盘;
        bytesTotal() 获取分区总大小,bytesFree() 获取剩余空间大小,已使用的空间是这两个数值的差值。

在 for 循环里获取的信息都添加到 strInfo ,最后把这个 strInfo 显示到编辑器控件里面。
另外注意 root 这个词有两种不同的意思:
函数 isRoot() 是判断该分区是否为操作系统安装的分区(操作系统根);
而函数  rootPath() 是获取当前分区在文件系统的挂载点(分区根)。

在 HTML 语法中, "<b> ... </b>"  是加粗字体,"<br>" 是换行的意思,"<font color=red> ... </font> " 是红色字体,成对的标记是可以嵌套使用的,比如 "<font color=red><b> ... </b></font>" 。

信息显示到编辑器之后,我们就可以点击 "保存磁盘信息" 按钮,把信息都存到文件中,"保存磁盘信息" 按钮的槽函数代码如下:
void Widget::on_pushButtonSaveDiskInfo_clicked()
{
    //获取保存文件名
    QString strFileName = QFileDialog::getSaveFileName(
                this,
                tr("保存信息"),
                tr("."),
                tr("Html files(*.htm);;Text files(*.txt);;ODF files(*.odf)")
                );
    //判断文件名长度
    if( strFileName.length() < 1 )
    {
        return;
    }
    //定义文档保存对象,写入文件
    QTextDocumentWriter tw(strFileName);
    bool bRes = tw.write( ui->textEdit->document() );
    //提示保存是否成功
    if( bRes )
    {
        QMessageBox::information(this, tr("保存成功"), tr("信息已成功保存到文件。"));
    }
    else
    {
        QMessageBox::warning(this, tr("保存出错"), tr("保存到文件出错!"));
    }
}
这个槽函数先获取要保存的文件名,并判断文件名的长度。
文件名不为空的情况下,定义文档保存对象 tw,然后把编辑器的文档写入到文件即可。
QTextDocumentWriter 使用非常简单,写文档只用两句代码。
槽函数末尾判断写入过程是否成功,并提示消息框。

QTextDocumentWriter 比较智能,它会根据文件名 strFileName 内部的扩展名,如 *.htm 或 *.txt 或 *.odf ,自动判断应该用哪种格式写入到文件中,所以不需要我们自己判断文件的扩展名。
例子代码就上面那些,我们生成运行例子来看看,点击 "获取磁盘信息",并把窗口拉大一些:
run
对于 Windows 系统,设备字符串可以在 Windows 注册表里面搜索到,磁盘分区的文件系统一般是 NTFS,如 C 盘和 D 盘;
E 盘是没有光盘的物理光驱;光盘文件系统一般是 CDFS,上图最后一个 F 盘是虚拟光驱,所以没有设备字符串,光盘镜像是只读的。
"保存磁盘信息" 按钮的功能请读者自己动手试试,这里不截图了,下面来看看临时文件的示例。

7.5.5 临时文件使用示例

本小节示例是关于 π (用英文表示为 PI)计算的,可以计算 1000、2000、4000、8000 四种位数的 π 值,存储到临时文件或临时文件夹,并对计算的过程进行计时,将计时显示到界面编辑框。在讲例子之前,首先说明一下关于 PI 的计算,计算方法和代码参考:
http://www.sharejs.com/codes/cpp/6626
我将这段代码稍微封装了一下,并增加了将结果写入文件的函数,封装后得到 CalcPI 类,两个文件为 calcpi.h 和 calcpi.cpp ,放到本示例源码下载的页面:
https://lug.ustc.edu.cn/sites/qtguide/QtProjects/ch07/temppi/
本小节示例需要用到这两个计算 PI 的代码文件,请读者先下载好,等会添加到例子项目里。

第二个需要说明的就是 Qt 的耗时计时器 QElapsedTimer,这个类用途较多,本例子中使用最简单的用法,就是三步:“开始计时”-->“计算PI”--“获取耗时”,开始计时的函数为:
void QElapsedTimer::​start()
计算 PI 值的函数由 CalcPI 类提供,计算结束后,调用如下函数获取中间计算过程花费的时间,以毫秒为单位:
qint64 QElapsedTimer::​elapsed() const

下面我们开始这个示例,重新打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 temppi,创建路径 D:\QtProjects\ch07,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
建好项目之后,把刚才下载 calcpi.h 和 calcpi.cpp 放到项目文件夹 D:\QtProjects\ch07\temppi,然后在 QtCreator 代码编辑模式左边的项目管理边栏,右击项目名 “temppi” ,右键菜单选择 “添加现有文件”,把 calcpi.h 和 calcpi.cpp 添加到项目。

接下来打开界面文件 widget.ui,进入界面设计模式,按照下图拖入控件:
ui
界面上有三行控件:
第一行,标签控件,文本为 "长度";单选按钮,文本 "1000位",对象名 radioButton1k;单选按钮,文本 "2000位",对象名 radioButton2k;单选按钮,文本 "4000位",对象名 radioButton4k;单选按钮,文本 "8000位",对象名 radioButton8k;第一行的控件按水平布局器排布。
第二行,按压按钮,文本 "计算PI" ,对象名 pushButtonCalcPI;按压按钮,文本 "计算四种PI",对象名 pushButtonCalcAll;第二行也按照水平布局器排列。
第三行,只有一个丰富文本编辑器,对象名 textEdit。
最后,界面的主布局器按照垂直布局,就得到上图效果。

设置好界面之后,为两个按压按钮都添加 clicked() 信号对象的槽函数:
slot
这里简要说明一下两个槽函数的用途,"计算PI" 按钮对应的槽函数会根据单选框指定的长度,计算一种长度的 PI 值,耗时信息显示到编辑器,PI值结果存到临时文件中。"计算四种PI" 按钮对应的槽函数会计算四种长度的 PI 值,并将四个耗时信息显示到编辑器,四个长度的 PI 值需要存到四个文件中,这四个文件都放到临时文件夹里。

我们保存界面文件,关闭界面文件,回到代码编辑模式,首先编辑头文件 widget.h,编辑后内容如下:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include "calcpi.h"

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

private slots:
    void on_pushButtonCalcPI_clicked();

    void on_pushButtonCalcAll_clicked();

private:
    Ui::Widget *ui;
    //用于计算pi的类对象
    CalcPI m_calcPI;
    //获取单选按钮对应的PI长度
    int GetPILength();
};

#endif // WIDGET_H
widget.h  添加了头文件包含 "calcpi.h" ,这是计算 PI 的类头文件,然后在窗口类的私有声明区域,添加私有变量 m_calcPI,用于计算各种长度的 PI 值,最后添加一个私有函数 GetPILength(),这个函数根据单选按钮判断需要计算的 PI 长度。

然后编辑 widget.cpp ,先添加头文件包含和构造函数里的代码:
#include "widget.h"
#include "ui_widget.h"
#include <QTemporaryFile>//临时文件
#include <QTemporaryDir> //临时文件夹
#include <QElapsedTimer> //耗时计时器
#include <QDebug>
#include <QMessageBox>
#include <QDir>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    ui->radioButton1k->setChecked(true);    //默认计算一千位
    //打印应用程序名
    qDebug()<<tr("应用程序名称:")<<qApp->applicationName();
    //打印操作系统临时文件路径
    qDebug()<<tr("系统临时文件路径:")<<QDir::tempPath();
}

Widget::~Widget()
{
    delete ui;
}
新增了本节学习的类头文件 <QTemporaryFile>、<QTemporaryDir>,耗时计时器的类头文件是 <QElapsedTimer>,其他头文件都用过,不赘述了。
构造函数里,先设置 ui->radioButton1k 单选按钮为选中状态,初始化时设置计算的长度为 1000 位;
然后打印了调试输出,qApp->applicationName() 就是当前应用程序的名字,QDir::tempPath() 是操作系统默认的临时文件路径,对于 QTemporaryFile 和 QTemporaryDir,如果构造时不带模板参数,那么这两个类对象自动根据 QDir::tempPath() 和 qApp->applicationName() 构造唯一的临时文件(夹)名字。

接下来手动添加 GetPILength() 函数的代码:
//获取单选按钮指定的PI长度
int Widget::GetPILength()
{
    if( ui->radioButton1k->isChecked() )
    {
        return 1000;
    }
    else if( ui->radioButton2k->isChecked() )
    {
        return 2000;
    }
    else if( ui->radioButton4k->isChecked() )
    {
        return 4000;
    }
    else
    {
        return 8000;
    }
}
这个函数判断各个单选按钮的选中状态,并返回对应的长度。

然后编辑 "计算PI" 按钮的代码:
void Widget::on_pushButtonCalcPI_clicked()
{
    //要计算的长度
    int nPILength = GetPILength();
    //定义耗时计时器
    QElapsedTimer eTimer;
    eTimer.start();
    //计算PI
    m_calcPI.Calc( nPILength );
    //耗时,毫秒
    qint64 nms = eTimer.elapsed();

    //定义临时文件
    QTemporaryFile tf;  //默认的即可
    if( ! tf.open() )   //打开并判断是否正常
    {
        QMessageBox::information(this, tr("打开临时文件"), tr("打开临时文件失败!"));
        return;
    }
    //设置为不自动删除,通常应该用默认自动删除的临时文件,这里仅用于观察临时文件路径
    tf.setAutoRemove(false);
    //把PI输出到临时文件
    m_calcPI.WriteToFile(tf);

    //构造信息串
    QString strInfo;
    QString strTemp;
    //耗时
    strTemp = tr("计算 %1  PI 耗时 %2 毫秒\r\n").arg(nPILength).arg(nms);
    strInfo += strTemp;
    //保存位置
    strTemp = tr("保存到临时文件:\r\n%1\r\n").arg( tf.fileName() );
    strInfo += strTemp;
    //显示到编辑器
    ui->textEdit->setText( strInfo );
}
这个槽函数先获取要计算的 PI 长度;
定义耗时计时器 eTimer,调用 eTimer.start(),开始计时;
调用 m_calcPI.Calc( nPILength ) ,计算参数指定长度的 PI 值;
然后调用 eTimer.elapsed() 获取上面计算过程花费的时间,存到 nms 。
函数前半部完成 PI 计算和耗时记录,后半部分完成临时文件写入和信息显示:
使用无参数的构造函数定义临时文件 tf;
打开 tf 文件对象,判断返回值是否正确;
将 tf 临时文件设置为非自动删除模式,这样生成的临时文件会保留,等会可以看到;
调用 m_calcPI.WriteToFile(tf) ,把 PI 值写入到文件中;
构造耗时信息字符串,添加到 strInfo ;
构造临时文件路径信息字符串,添加到 strInfo ;
最后把 strInfo 显示到编辑器中。

这里说明两点,tf 文件对象定义没有用模板参数,临时文件的路径文件名会根据 QDir::tempPath() 和 qApp->applicationName() 自动生成;本例为了查看临时文件,所以调用 setAutoRemove(false),临时文件不会自动删除,而临时文件实际应用中通常不需要这句。

最后是 "计算四种PI" 按钮对应的槽函数代码:
//计算四次,把计算结果放到临时文件夹
void Widget::on_pushButtonCalcAll_clicked()
{
    //建一个临时文件夹,把四个长度的PI都存到该文件夹
    QTemporaryDir td("PI-");    // "PI-" 是模板
    QString strTempDir; //临时文件夹的路径字符串
    //判断临时文件夹是否可用
    if( td.isValid() )
    {
        strTempDir = td.path();
    }
    else
    {
        //没有正确建立临时文件夹
        QMessageBox::warning(this, tr("新建临时文件夹"), tr("新建临时文件夹失败!"));
        return;
    }
    //设置为不自动删除
    td.setAutoRemove(false);
    //字符串
    QString strInfo;
    QString strTemp;

    //临时文件夹建立正常,下面计算四种长度的PI
    for(int i=1; i<=8; i*=2)
    {
        int nPILength = i*1000;
        QElapsedTimer eTimer;
        //计算PI,并计算耗时
        eTimer.start();
        m_calcPI.Calc(nPILength);
        qint64 nms = eTimer.elapsed();
        //构造文件名,保存到文件
        QString strCurName = strTempDir + tr("/%1.txt").arg(nPILength);
        QFile fileCur(strCurName);
        if( ! fileCur.open( QIODevice::WriteOnly ) )
        {
            QMessageBox::warning(this, tr("新建文件"), tr("新建存储PI的文件失败!"));
return; //出错直接返回,感谢 XT A (wangweilong1996@gmail.com) 纠正。
        }
        //写入到文件
        m_calcPI.WriteToFile(fileCur);

        //构造结果信息
        strTemp = tr("计算 %1  PI,耗时 %2 毫秒\r\n存到 %3 \r\n")
                .arg( nPILength )
                .arg( nms )
                .arg( fileCur.fileName() );
        strInfo += strTemp;
        strInfo += tr("\r\n");
    }
    //显示结果信息
    ui->textEdit->setText( strInfo );
}
因为要计算四种长度的 PI 值,需要把四种值存成四个文件,所以定义了临时文件夹对象 td,四个文件都放到临时文件夹中。
td 定义时使用了模板参数 "PI-" ,这是相对路径,最后会在程序工作路径生成类似 "PI-P1XkOA" 名字的临时文件夹,名字后六个是随机的字母数 字。
定义临时文件夹 td 之后,先要判断该文件夹是否可用 td.isValid();
然后也将临时文件夹设置为非自动删除模式,td.setAutoRemove(false) ;
接着使用 for 循环,计算 1000、2000、4000、8000 位四种长度的 PI 值:
    当前要计算的长度为 nPILength ;
    定义耗时计时器 eTimer,并开始计时 eTimer.start();
    计算 PI 值 m_calcPI.Calc(nPILength);
    获取计算耗时 eTimer.elapsed(),存到变量 nms;
    在临时文件夹里面新建要写入的文件,文件名就是 位数长度.txt ;
    调用 m_calcPI.WriteToFile(fileCur) 把 PI 值写入文件中;
    构造结果信息字符串,添加到 strInfo 。

for 循环结束后,将结果信息显示到编辑器里。

关于这个槽函数生成的四个文件 1000.txt 、2000.txt 、4000.txt 、8000.txt ,因为都放在临时文件夹里面,虽然是普通的文件,但会自动继承临时文件夹的 “临时” 特性,如果没有 td.setAutoRemove(false) 这句代码,四个结果文件都会伴随临时文件夹的自动删除而删除。
实际应用中,如果用到较多的临时文件,那么就可以新建一个临时文件夹,内部放些普通文件(文件名自己指定),这些普通文件就自动具有临时特性,而不需要专门定义一 大堆 QTemporaryFile 对象;如果用到的临时文件较少,直接定义 QTemporaryFile 对象就更方便。
widget.cpp 的代码就是上面那些,最后稍微提一下 CalcPI 类,有三个公开接口:
void CalcPI::Calc(int length) //计算指定长度的 PI
void CalcPI::PrintPI()  //打印结果 PI 值到命令行
void CalcPI::WriteToFile(QFile &fileOut) //将结果 PI 写入文件中
注意写入文件函数 WriteToFile(),参数是 QFile 引用,临时文件对象 QTemporaryFile 的读写和它的基类 QFile 对象读写没任何区别,直接用 QFile 的读写函数就可以了。
例子代码讲到这里,我们生成运行例子,点击 "计算四种PI" 按钮,可以看到类似下面效果:
run
"计算四种PI" 按钮生成的临时文件夹在项目的影子构建目录,而 "计算PI" 按钮生成的临时文件在操作系统临时文件路径中,读者可以自己去测试并查看文件内容。

本节的内容主要就是上面那些,后面附加三个小练习。下一章我们重点学习基于条目的控件,就是设计师界面左边的 Item Widgets(Item-Based) 一栏的三个控件。


tip 练习
1. 本节两个示例的编辑器都是可以编辑的,用户可以修改结果信息,这个练习要做的就是修改例子代码将编辑器设置为只读的。

2. 对于临时文件使用示例,两个按钮槽函数都调用了 setAutoRemove(false),把两个按钮的槽函数中这句函数调用代码注释掉,然后生成运行例子,试试看还能不能找到例子生成的临时文件和临时文件夹。

3. 对于临时文件使用示例,原本是用 Debug 模式构建并运行例子,现在在 QtCreator 左下角选择 Release 模式构建并运行例子,对比 Debug 和 Release 模式计算各个长度 PI 花费的时间。



prev
contents
next