QT学习笔记(七)

  • Post author:
  • Post category:其他




第12章 输入与输出

Qt提供了读写字节块的设备类QIODevice,QIODevice类是抽象的,无法被实例化,一般是使用它的子类。它包括如下子类:

在这里插入图片描述

其中,QProcess,QTcpSocket,QUdpSocket,QSslSocket都是顺序存取设备,这意味着所存储的数据从第一个字节开始到最后一个字节为止只能被读取一次。QFile,QTemporaryFile,QBuffer则是随机存取设备,因此可以从任意位置多次读取字节位所存储的数据,随机存取设备支持使用seek()函数来定位到任意位置。QProcess类允许启动外部程序并通过标准输入、输出及标准错误通道(cin,cout和cerr)与外部程序交互。

除了设备类,Qt还提供了两个更高级别的流类,使我们可以从任意的输入输出设备读取或者写入数据:QDataStream用来读写二进制数据,QTextStream用来读写文本数据。它们提供了一种与运行平台无关的存储格式。

  1. 读取和写入二进制数据

Qt中载入和保存二进制数据最简单的方式是:通过实例化一个QFile打开文件,然后通过QDataStream对象存取它。QDataStream支持多种类型的类,如:整型和双精度浮点型等基本的C++类,QList<T>和QMap<T>等Qt容器类,以及QByteArray、QIamgae、QString和QVariant等多种Qt数据类型。

二进制文件格式提供了数据存储最通用最紧凑的方式,而且QDataStream也使得存取二进制数据非常容易。

1)下面是在一个名为facts.dat的文件中存储一个整型数据、一个QImage以及一个QMap<QString,QColor>的代码:

QImage image("philip.png");

QMap<QString,QColor> map;
map.insert("red",Qt::red);
map.insert("green",Qt::green);
map.insert("blue",Qt::blue);

QFile file("facts.dat");                      //实例化一个QFile打开文件
if(!file.open(QIODevice::WriteOnly)) {
    std::cerr << "Cannot open file for writting: "
              << qPrintable(file.errorString()) << std::endl;   // 如果不能打开文件,就通知用户并返回
    retrn;
}

QDataStream out(&file);             //如果成功打开文件,就创建QDataStream对象             
out.setVersion(QDataStream::Qt_4_3);    //设置版本号

out << quint32(0x12345678) << image << map;    //向文件中写入数据

注意:我们将二进制数据写入数据流对象或从数据流中读取二进制数据,都需要串行化,就像上例中的quint32(0x12345678)。

//串行化字符串
out << QString("the answer is");
//串行化整数
out << (qint32) 42;

可以在串行化数据时选择使用哪种字节顺序,默认是高字节在前(big-endian),可以通过调用setByteOrder()来改变,但改为小端有可能会破坏可移植性。

我们不必明确的关闭这个文件,因为当QFile变量离开它的作用域的时候,文件会自动关闭(也可以调用close()函数强制关闭文件)。如果想检验数据是否被真正的写入,可以调用刷新函数flush()并检验其返回值(若返回值为true,则表示成功写入数据)

2) 从文件中读回数据:

quint32 n;
QImage image;
QMap<QString,Qcolor> map;

QFile file("facts.dat");                  //实例化一个QFile打开文件
if(!file.open(QIODevice::ReadOnly)) {
     std::cerr << "Cannot open file for writting: "
                   << qPrintable(file.errorString()) << std::endl;   // 如果不能打开文件,就通知用户并返回
    return;
}

QDataStream in(&file);                 //如果成功打开文件,就创建QDataStream对象
in.setVersion(QDataStream::Qt_4_3);   //设置版本号

in >> n >> image >> map;    //从文件中读取数据

当从QDataStream读取数据的时候,错误处理相当容易。流有一个status()值,可以是QDataStream::OK、QDataStream::ReadPastEnd或者QDataStream::ReadCorruptData。如果错误发生,则>>操作符总是读取0值或者空值。这表示通常可以简单的读取一个文件而不用担心出错,并在最后检查status()值以确定读取的文件数据是否有效。

3) 还可以通过重载<<或>>操作符为用户的自定义类型增加支持。格式如下:

QDataStream& operator<< (QDataStream& out,const Painting &painting); // 重载流运算符,其中Painting为自定义类型

4)如果想一次读取或者写入一个文件,可以完全不用QDataStream而使用QIODevice的 write() 和 readAll() 函数。例如:

bool copyFile(const QString &source,const QString &dest)
{
    QFile sourceFile(source);
    if(!sourceFile.open(QIODevice::ReadOnly))
            return false;
    QFile destFile(dest);
    if(!destFile.open(QIODevice::WriteOnly))
            return false;

    destFile.write(sourceFile.readAll());

    return  sourceFile.error() == QFile::NoError
              && destFile.error() == QFile::NoError;
}

在调用readAll()的那一行中,输入文件的所有内容都被读入到一个QByteArray中,然后将它传给write()函数以写到输出文件中。虽然获得QByteArray中的数据比逐项读取数据需要更多的内存,但它也带来了一些方便。例如:在这之后可以使用qComPress()和qUnCompress()函数来压缩和解压缩数据。

  1. 读取和写入文本

虽然二进制文件格式比通常基于文本的格式更加紧凑,但是它们是机器语言,无法人工阅读或者编辑。在二进制文件无法适应的场合,可以使用文本格式来代替。Qt提供了 QTextStream类读写纯文本文件以及如HTML、XML和源代码等其他文本格式的文件。QTextStream类可以在QIODevice、QByteArray和QString上进行操作。用法:

1)下面的代码将“Thomas M. Disch:334 \n”写到文件sf-book.txt中:

QFile file("sf-book.txt");
if (!file.open(QIODevice::WriteOnly())) {
    std::cerr << "Cannot open file for writing: "
                  << qPrintable(file.errorString()) <<std::endl;
    return; 
}

QTextStream out(&file);
out << "Thomas M. Disch: " << 334 << endl;

在默认情况下,QTextStream使用系统的本地编码进行读取和写入。但可以使用setCodec()函数改变:

stream.setCodec("UTF-8");


如果想将文本直接写入QIODevice,那么必须为open()函数明确的指定QIODevice::Text标记,例如:

file.open(QIODevice::WriteOnly | QIODevice::Text);


当写入的时候,这个标记将通知QIODevice将”\n”字符转换为Windows下的“\r\n”。当读取的时候,这个标记将告知设备忽略所有平台中的换行字符,然后使用“\n”作为换行符。

QTextStream提供了流操作器,可以在流上设置并改变流数据的状态。例如:

out << showbase << uppercasedigits << hex << 12345678;  //分别设置了“显示前缀”“在十六进制数中使用大写字母”“使用十六进制”状态

也可以通过成员函数来设置这些选项:

out.setNumberFlags(QTextStream::ShowBase
                                   | QTextStream::UppercaseDigits);
out.setIntegerBase(16);
out << 12345678;

2)写入文本数据很容易,但读取文本却是一个挑战。因为文本数据与使用QDataStream写入的二进制数据不同,从根本上说是含糊而不确定的。

QTextStream使用16位的QChar类型基本数据单元。

读取文本时通常通过在QChar上使用>>来一个字符一个字符地读取数据,或者通过使用QTextStream::readLine()来逐行读取数据 。如果要处理全部的文本,且不考虑内存的使用大小或者已经知道所读文件很小的情况下,可以使用QTExtStream::readAll()一次读取整个文件。用法:

QTextStream in(&file);
QString line = in.readLine();   //一次读取一行文本数据,readLine()函数自动去掉行尾的“\n”

while(!in.atEnd())
{
    in >> ch;                   //使用while循环 和>>运算符一个一个字符的读取数据
}
  1. 遍历目录

QDir类提供了一种与平台无关的遍历目录并获得有关文件信息的方法。QDir类用来访问目录结构及其内容,它可以操作底层的文件系统,还可以访问Qt的资源系统。 用法:

const QString &path;  //字符串中存储给定的路径,这个路径可以是当前目录下的相对路径也可以是绝对路径
QDir dir(path);  //使用给定的路径创建一个QDir对象

Qt使用“/”作为通用的目录分隔符,QDir可以使用相对路径或者绝对路径来指向一个文件。一个目录的路径可以使用path()函数获取,使用setPath()函数可以设置新的路径。

使用mkdir()来创建目录,使用rmdir()来删除目录,使用cd()来跳转到指定目录。用法:

//显示出当前目录下的所有.h文件
QDir myDir(QDir::currentPath());
myDir.setNameFilters(QStringList("*.h"));
myDir.entryList();
//创建目录并跳转
myDir.mkdir("mydir");
myDir.cd("mydir");

目录中会包含很多条目,如文件、目录和符号链接等。一个目录中条目的个数可以使用count()来返回,所有条目的名称列表可以使用entryList()来获取。如果需要每一个条目的信息,则可以使用entrylnfoList()函数来获取一个QFileInfo类对象的列表。

QFileInfo类可以访问文件的属性,如文件的名称、大小、路径、访问权限、属主和时间戳(最近一次修改/读取的时间)等等。QFileInfo可以使用相对路径或绝对路径来指向一个文件。用法:

#include <QFileInfo>
QFileInfo info(file);
qdebug << QObject::tr("绝对路径:") << info.absoluteFilePath() << endl << QObject::tr("文件名:") << info.fileName() << endl
              << QObject::tr("后缀:") << info.suffix() << endl << QObject::tr("大小")  << info.size();

在这里插入图片描述

  1. 嵌入资源

除了利用上面的输入输出方法在外部设备中存取数据外,还可以利用Qt的资源系统在可执行文件中嵌入二进制数据或文本。嵌入的文件与文件系统中的普通文件一样也可以通过QFile读取。

通过Qt资源编译器rcc,可以将资源转换为C++代码。还可以通过下面一行代码加到.pro文件中来告诉qmake包括专门的规则以运行rcc:

RESOURCES = myresourcefile.qrc
myresourcefile.qrc文件是一个XML文件,它列出了所有嵌入到可执行文件中的文件。

在应用程序中,资源是通过”:/ “路径前缀识别的。

在可执行文件中嵌入数据具有不易丢失的优点,而且有利于可执行文件的独立性。但它有两个缺点:第一,如果要改变嵌入的数据,则整个可执行文件都要跟着替换;第二,由于必须容纳被嵌入的数据,可执行文件本身将会变得比较大。

  1. 临时文件QTemporaryFile

QTemporaryFile类可以安全地创建一个唯一的临时文件。当调用open()函数时便会创建一个临时文件(调用open()函数时,默认会使用QIODevice::ReadWrite模式),临时文件的文件名是唯一的。用法:

QTemporaryFile file;
if (file.open()) {
     //在这里对临时文件进行操作,file.fileName()可以返回唯一的文件名
}

当销毁QTemPoraryFile对象时,该文件会被自动删除掉。但调用close()函数后重新打开QTemporaryFile是安全的,只要QTemporaryFile的对象没有被销毁,那么唯一的临时文件就会一直存在且由QTemporaryFile内部保持打开。

临时文件默认会生成在系统的临时目录里,这个目录的路径可以使用QDir::tempPath()来获取。

  1. 缓冲区

QBuffer类为QByteArray提供了一个QIODevice接口,它允许使用QIODevice接口来访问QByteArray。

默认的,创建一个QBuffer时,则自动在内部创建一个QByteArray缓冲区。可以向QBuffer的构造函数中传递一个QByteArray类对象,来访问这个QByteArray缓冲区。用法:

//使用QDataStream 和 QBuffer 来向 QByteArray 写入数据
QByteArray byteArray;
QBuffer buffer(&byteArray);
buffer.open(QIODevice::WriteOnly);
QDataStream out(&buffer);
out << QApplication::palette();
//从QByteArray中读取数据
QPalette palette;
QBuffer buffer(&byteArray);
buffer.open(QIODevice::ReadOnly);
QDataStream in(&buffer);
in >> palette;

当新的数据到达了缓冲区时,QBuffer会发射readyRead()信号。通过关联这个信号,可以使QBuffer来存储临时的数据。

  1. 进程间通信

如果在当前的应用程序中调用外部的程序来实现一定的功能,这就会使用到进程。

QProcess类允许我们执行外部程序并且和他们进行通信。这个类是异步工作的,且它在后台完成它的工作,这样用户界面就可以保持响应。当外部进程完成时,QProcess会发出信号通知我们。用法:

private:
    QProcess  process;    //在窗口部件类的类声明中将QProcess类对象声明为private成员。

connect(&process,SIGNAL(error(QProcess::ProcessError)),this,SLOT(processError(QProcess::ProcessError)));
                                      // 在窗口部件类的构造函数中将QProcess对象的信号和窗口的私有槽进行连接
process.start("进程名称",参数);    //在成员函数中利用程序名称以及它所需要的参数来调用QProcess::start()函数,开始进程。

要启动一个进程,可以使用start()函数。–(执行完start()函数后,QProcess进入Starting状态)

当程序已经运行后,QProcess会发射started()信号。– (QProcess进入Running状态)

当进程退出时,会发出finished()信号,发射的finished()信号提供了进程的退出状态。QProcess::ExitStatus给出了程序结束的状态值。–(QProcess重新进入NotRunning状态(初始状态))

任何时候发生了错误,如程序不能开启,QProcess都会发出error()信号,QProcess::ProcessError给出了错误的类型值。

QProcess异步执行的意思是:我们告知QProcess去运行某个程序并立即将控制权返还给用户界面程序,这样,当进程在后台运行时可以让用户界面始终保持响应。

但在某些情况下,在自己的应用程序进一步执行前,必须先完成外部进程,这时需要同步操作QProcess。QProcess同步操作时并不需要信号和槽的连接。有两种方法:

一种方法是使用QProcess::execute()静态函数,该函数运行外部进程,并当该外部进程完成时停止。

另一种更好的方法是:创建QProcess对象,并对它调用start(),然后通过调用QProcess::waitForStarted()强制使它停止。如果成功,再调用QProcess::waitForFinished()。

waitForStarted() 阻塞,直到进程启动、waitForFinished() 阻塞,直到进程结束

这两个函数会挂起调用的线程,直到确定的信号被发射。在主线程(QApplication::exec()的线程)中调用这些函数可能会引起用户界面的冻结。

QProcess继承自QIODevice,QProcess允许将一个进程视为一个顺序I/O设备。可以调用write()向进程的标准输入进行写入,调用read(), readLine()和getChar()等从标准输出进行读取。



第14章 多线程

在多线程应用程序中,图形用户界面运行于它自己的线程中,而另外的事件处理过程则会发生在一个或多个其他线程中。这样在处理那些数据密集的事件时,应用程序也能对图形用户界面保持响应。

在单一处理器上运行时,多线程应用程序可能会比实现同样功能的单线程应用程序运行的慢一些,无法体现出优势。但在多处理器系统中,多线程应用程序可以在不同的处理器上同时执行多个线程,从而获得更好的总体性能。

  1. 创建线程

在Qt应用程序中提供多线程:只需要子类化QThread并且重实现它的run()函数就可以了。

Qt中的QThread类提供了与平台无关的线程类。一个QThread代表了一个在应用程序中可以独立控制的线程,它与进程中的其他线程分享数据,但是是独立执行的。

相对于一般的程序从main()函数开始执行,QThread从run()函数开始执行。默认的,run()通过调用exec()来开启事件循环,并在线程内运行一个Qt事件循环。

实例:

//子类化QThread
#include <QThread>
class Thread : public QThread
{
    Q_OBJECT
public:
    explicit Thread(QObject *parent = 0);            //构造函数
    void stop();
protect:
    void run();          //重实现run()函数
private:
    volatile bool stopped;        //volatile关键字表示编译器不会对访问该变量的代码进行优化,当要求使用改变量的值的时候,系统总是重新从它所在的内存                                                     读取数据
}

可以在外部创建线程的实例,然后调用start()函数开始执行该线程,start()默认调用run()函数。当从run()函数返回后,线程便执行结束,就像应用程序离开main()函数一样。

QThread提供了一个terminate()函数,该函数可以在一个线程还在运行的时候就终止它的执行。我们不推荐使用terminate(),因为它可以随时停止线程的执行而不给这个线程自我清空的机会。一种更安全的方法是像上面那样使用stopped变量和stop()函数。

QThread会在开始、结束和终止时发射started()、finished()和terminated()信号(区别:terminate()函数),可以使用isFinished()和isRunning()来查询线程的状态。可以使用wait()来阻塞,直到线程结束执行。

实例:

//创建线程
Thread threadA;    //创建线程实例
Thread threadB;

threadA.strat();     //开始线程,start()默认调用run()函数
threadA.stop();     //结束线程
threadA.wait();     //阻塞
  1. 同步线程

对于多线程应用程序,一个最基本的要求是能实现几个线程的同步执行。虽然使用线程的思想是多个线程可以尽可能地并发执行,但总有一些时刻一些线程必须停止来等待其他线程,例如,如果两个线程尝试同时访问相同的全局变量。Qt中的QMutex、QReadWriteLock、QSemaphore和QWaitCondition提供了同步线程的方法。

1)QMutex类提供了一个互斥量。

它提供了一种保护一个变量或者一段代码的方法,每次只让一个线程读取它。这个类提供了一个lock()函数来锁住互斥量(mutex),unlock()函数解锁互斥量。如果互斥量是解锁的,那么当前线程就立即占用并锁定它。否则,当前线程就阻塞,直到掌握这个互斥量的线程对它解锁为止。用法:

QMutex mutex;
mutex.lock();
mutex.unlock();

2)QReadWriteLock类提供了一个读-写锁。

与QMutex很相似,只不过它将对共享数据的访问区分为“读”访问与“写”访问,允许多个线程同时对数据进行“读”访问。在可能的情况下使用QReadWriteLock代替QMutex,可以提高多线程程序的并发度。

3)QSemaphore类提供了一个信号量。

QSemaphore类用于保护一定数量的相同资源。信号量(semaphore)是互斥量的另外一种泛化表现形式。通过acquire()函数获取一个资源,本信号量获取的资源将不能被其他的信号量获取,直到资源被release()函数释放。用法:

QSemaphore semaphore(1);
semaphore.acquire();
semaphore.release();

通过把1传递给构造函数,就告诉这个信号量它控制了一个单一的资源。使用信号量的优点是可以传递1以外的数字给构造函数,然后可以多次调用acquire()来获取大量的资源。

4)QWaitCondition即等待条件

它允许一个线程在一些条件满足时唤醒其他线程。它与互斥锁搭配使用,可以比只使用互斥锁提供更精确的控制。

以上的方法适用于多个线程访问相同的全局变量的情况。但是在一些多线程的应用程序中,需要一个在不同线程中保存不同数值的全局变量,这种变量通常称为线程本地存储。QThread<T>类提供了这样一种功能。

  1. 与主线程通信

当Qt应用程序开始执行的时候,只有主线程是在运行的。主线程创建QApplication或QCoreApplication对象,之后对创建的对象调用exec(),主线程进入事件循环状态。通过创建一些QThread子类的对象,主线程可以开始一些新的线程。如果这些新的线程之间需要进行通信,可以使用含有互斥量、读-写锁、信号或者循环等待条件的共享变量。但在这些技术中没有一个可以用来与主线程进行通信,因为他们会锁住事件循环并且会冻结用户界面。在次线程和主线程之间通信的一个解决方案是线程之间使用信号-槽连接。

实现次线程时要谨慎对待,因为必须使用互斥量来保护成员变量,而且必须使用一个等待条件以在适当的时候停止或触发线程。

跨线程的信号和槽:

可以通过向connect()函数传递附加的参数来指定关联类型。Qt支持几种信号和槽的关联机制:

Auto Connection(默认) 。如果信号发射和信号接收的对象在同一个线程,那么执行方式与Direct Connection相同。否则,执行方式与Queued Connection相同。

Direct Connection。信号发射后,槽立即被调用。槽在信号发送者的线程中执行,而接收者并非必须在同一个线程。

Queued Connection。控制权返回接收者线程的事件循环后才调用槽。槽在接收者的线程中被执行。

Blocking Queued Connection。槽的调用与Queued Connection相同,不同的是当前线程会阻塞直到槽返回。注意:使用这种方式关联在相同线程中的对象时会引起死锁。

Unique Connection。执行方式与Auto Connection相同,只不过关联必须是唯一的。

  1. 可重入与线程安全


线程安全

(thread-safe)的函数:一个线程安全的函数可以被多个线程调用,即使它们使用了共享数据


可重入

(reentrant)的函数:一个可重入的函数可以同时被多个线程调用,但是只能在每个调用使用自己的数据的情况下

(一个线程安全的函数总是可重入的,但一个可重入的函数不总是线程安全的)


线程安全

的类:如果即使所有的线程使用一个类的相同实例,该类的成员函数也可以被多个线程安全的调用,那么这个类被称为线程安全的


可重入

的类:如果每个线程使用一个类的不同实例,则该类的成员函数可以被多个线程安全的调用,那么这个类被称为可重入的

(注意:如果一个函数没有被标记为线程安全或者可重入的,则它不应被多个线程使用;如果一个类没有标记为线程安全的或者可重入的,则该类的一个特定的实例不应该被多个线程访问)

原子操作:就是一个不会被其他线程中断的操作

  1. QObjects的可重入性

QObjects是可重入的。它的大多数非GUI子类,例如:QTimer、QProcess等也都是可重入的。对于GUI类,尤其是QWidget及其所有子类,是不可重入的,它们只能在主线程中使用。QCoreApplication::exec()也必须在主线程中调用。

QObject可重入有3个约束条件:

  • QObject的子对象必须在创建它的父对象的线程中创建。这意味着,永远不要将QThread对象(this)作为在该线程中创建的对象的父对象(因为QThread对象本身是在其他线程中创建的)
  • 事件驱动对象只能在单一线程中使用。这意味着,不能在对象所在的线程QObject::thread()以外的其他线程中启动一个定时器。
  • 必须确保删除QThread对象以前删除在该线程中创建的所有对象,可以通过在run()函数中的栈上创建对象来保证这一点。
  1. 每个线程的事件循环

每一个线程都可以有它自己的事件循环。

初始化线程使用QCoreApplication::exec()来开启它的事件循环(对于独立的对话框界面程序也可以使用QDialog::exec()开启事件循环);其他的线程可以使用QThread::exec()来开启一个事件循环。

在一个线程中使用事件循环,使得该线程可以使用那些需要事件循环的非GUI类(如:QTimer、QProcess),也使得该线程可以关联任意一个线程的信号到一个指定线程的槽。

如果在一个线程中创建了一个QObject对象,那么这个QObject对象被称为居住在该线程(live in the thread)。发往这个对象的事件由该线程的事件循环进行分派。如果没有运行事件循环,则事件将不会传送到对象。

上一篇:QT学习笔记(六)

https://blog.csdn.net/weixin_44787158/article/details/99110713



版权声明:本文为weixin_44787158原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。