虽然二进制文件格式比通常基于文本的格式更加紧凑,但是它们是机器语言,无法人工阅读或者编辑。在二进制文件格式无法适用的场合,可以使用文本格式来代替。 Qt 提供了 QTextStream类读写纯文本文件以及如 HTML XML 和源代码等其他文本格式的文件。
QTextStream考虑了Unicode 编码与系统的本地编码或其他任意编码之间的转换问题,并且明确地处理了因使用不同操作系统而导致不同的行尾符之间的转换(在 Windows 操作系统上行尾符是"\r\n",即UNIX Mac X 操作系统上是"\n")。QTextStream 使用 16 位 QChar 类型基本数据单元。除了字符和字符串之外, QTextStream还支持 C++ 基本数字类型,它可以进行基本数字类型和字符串之间的转换。例如,下面的代码将"Thomas M.Disch:334 \n" 写到文件 sf-book.txt 中:
QFile file("sf-book.txt");
if(!file.opem(QIODevice::WriteOnly))
{
std::cerr << "Cannot open file for writing: "
<< qPrintable(file.errorString()) << std::endl;
return;
}
QTextStream out(&file);
out << "Thomas M.Disch: " << 334 << endl;
写入文本数据非常容易,但读取文本却是一个挑战。因为文本数据(与使用 QDataStream写入的二进制数据不同)从根本上说就是含糊而不确定的。让我们来看看下面的例子:
out << "Denmark" << "Norway";
如果 out 为 QTextStream,则实际上被写入的数据是字符串 “DemarkNorway” 。我们真的不能期望下面的代码能够正确地读回数据:
in >> str1 >> str2;
实际上,由获得了整个词 “DenmarkNorway”,而 str2什么也没有得到。使用 QDataStream则不会发生这个问题,因为它在字符串数据前面保存了每个字符串的长度。
对于复杂的文件格式,成熟的解析器也许是必需的。解析器通常通过在QChar上使用>>来一个字符一个字符地读取数据,或者通过使用 QTextStream::readLine() 来逐行读取数据。在本节的最后,将给出两个简单的例子,其中一个是逐行地读取输入文件,另二个则是一个字符一个字符地读取。对于一个处理全部文本的解析器,如果不考虑内存的使用大小或者已经知道所读文件很小的话,可以使用 QTextStream::readAll()一次读取整个文件。
在默认情况下, QTextStrearn 使用系统的本地编码(例如,在美国以及欧洲的大部分地区都可使ISO 8859-1或150 8859-15) 进行读取与写入。当然这可以通过使用如下的setCode()而改变:
stream.setCodec("UTF-8");
本例中使用的UTF-8 编码是一种与ASCII兼容的编码方式,它代表整个 Unicode 字符集。
QTextStream 有各种各样的模仿<iostream>的选项。这些选项可以通过传递专门的对象,也称为流操作器,在流上设置以改变它的状态,或者是通过调用列在图 12.1 中的函数。下面的例子是在整型数 12 345 678 输出前设置了 showbase、uppercasedigits以及 hex 选项,生成的输出文本为"OxBC614E":
out << showbase << uppercasedigits << hex << 12345678;
也可以通过成员函数来设置这些选项:
out.setNumberFlags(QTextStream::ShowBase|QTextStream::UppercaseDigits);
out.setIntegerBase(16);
out << 12345678;
QTextStream 相似,QTextStream 也是在 QIODevice子类上运作的,它可以是 QFile、QTemporary、QBuffer、QProcess、QTcpSocket 或者 QUdpSocket。此外,它还可以直接在QString上使用。例如:
QString str;
QTextStream(&str) << oct << 31 <<" " << dec << 25 << endl;
这就使 str 中的内容为 “37 25\n”,因为十进制数的 31 表示八进制中的 37。这种情况下,就不必再为流设置编码,因为 QString 总是 Unicode 编码。
让我们看一个简单的基于文本文件格式的例子。在本书第一部分中所介绍的 Spreadsheet应用程序中,使用了二进制数格式存储电子数据表格的数据。这种数据类由一个三元序列(行、列、公式)组成,每一非空单元都有同样的数据类型。以文本格式写入数据是显而易见的,下面是如何从一个修改的 Spreedsheet::writeFile() 版本中提取数据的例子:
QTextStream out(&file);
for(int row = 0; row < RowCount; ++row)
{
for(int column = 0; column < ColumnCount; ++column)
{
QString str = formula(row, column);
if(!str.isEmpty())
out << row << " " << column << " " << str <<endl;
}
}
我们采用了一种简单的格式,每一行代表一个单元,而在行与列以及列与公式之间由空格分隔。公式可以包含空格,但是假设它不含"\n" ( "\n"是用来终止一行的符号) 。现在看看相应的读取代码:
QTextStream in(&file);
while (!in.atEnd())
{
QString line = in.readLine();
QStringList fields = line.split(' ');
if(fields.size() >= 3)
{
int row = fields.takeFirst().toInt();
int column = fields.takeFirst().toInt();
setFormula(row, column, fiels.join(' '));
}
}
我们一次读取一行电子数据表格的数据。 readIine()函数将自动去掉行尾的"\n"。QString::split()返回一个字符串列表,只要哪里有给定的分隔符出现,它就会在哪里分隔字符串。例如,行"5 19 Total value"会变成含4个元素项的列表[“5”, “19” “Total”, “value”]。
如果在列表中包含三个以上的字段,则就准备开始提取数据了。 QStringList::takeFirst ()函数去掉列表中第一项并返回被去掉的项。我们使用它来提取行和列上的数据。我们并不执行任何的错误检查,如果读入了一个非整数的行或列的值,QString::tolnt() 就返回0值。当调用setFormula()时,必须将剩下的宇段连接成一个单独的字符串。
在第二个 QTextStream例子中,将采用逐个字符的方法执行一个读取文本文件并输出相同文本的程序,其间将去除每行行尾的空格并将所有制表符用空格代替。这个程序的工作可以通过 tidyFile() 函数来完成:
void tidyFile(QIODevice *inDevice, QIODevice *outDevice)
{
QTextStream in(inDevice);
QTextStream out(outDevice);
const int TabSize = 8;
int endlCount = 0;
int spaceCount = 0;
int column = 0;
QChar ch;
while (!in.atEnd()) {
in >> ch;
if (ch == '\n') {
++endlCount;
spaceCount = 0;
column = 0;
} else if (ch == '\t') {
int size = TabSize - (column % TabSize);
spaceCount += size;
column += size;
} else if (ch == ' ') {
++spaceCount;
++column;
} else {
while (endlCount > 0) {
out << endl;
--endlCount;
column = 0;
}
while (spaceCount > 0) {
out << ' ';
--spaceCount;
++column;
}
out << ch;
++column;
}
}
out << endl;
}
我们基于传递给函数的 QIODevice 来创建一个输入和输出 QTextStream。除了当前的字符外,还支持三个状态跟踪变量:一个计算新行数(换行数) ,一个计算空格数,一个标记当前所在行的当前列位置(用于将制表符转换为恰当数目的空格数)。
通过在 while 循环中对输入文件的每个宇符进行迭代来完成整个解析过程。在某些地方的代码非常巧妙。例如,尽管将 TabSize 设置成了 8,但只是用正好足够的空格数来代替到下一制表符边界的制表符,而不是粗略地用 8个空格来代替每一个制表符。如果获得一个换行符、一个制表符或者一个空格,仅仅只更新或修正状态数据。只有在获得另一种字符时才输出,并且在写出这种字符前,我们先写出未决的换行符和空格(以考虑空行和保留缩进)并更新状态数据。
int main()
{
QFile inFile;
QFile outFile;
inFile.open(stdin, QFile::ReadOnly);
outFile.open(stdout, QFile::WriteOnly);
tidyFile(&inFile, &outFile);
return 0;
}
这个例子不需要使用 QApplication 对象,因为我们只是使用Qt的工具类。我们已经假设这个程序是被用作过滤器的,例如:
tidy < cool.cpp > cooler.cpp
如果给出了文件名,这个程序可以很容易地扩展用来处理命令行上的文件名,另外还可以过滤cin到cout。
因为这是一个控制台应用程序,它的.pro 文件与我们所见过的用于图形用户界面应用程序的略微有些不同。
TEMPLATE = app
QT = core
CONFIG += console
CONFIG -= app_bundle
SOURCES = tidy.cpp
我们只关联 QtCore,因为没有使用任何的图形用户界面功能。然后指定我们想在 Windows 系统下实现控制台输出,并且不希望在 Mac OS X 系统下同时存在许多应用程序。
对于读取和写入纯必ASCII文件或者 ISO 8859-1 (Latin-1) 文件,可以直接使用 QIODevice 的应用编程接口来代替使用。QTextStream。但这样做并不很明智,因为大多数应用程序都会在某处需要对其他编码的支持,而只有 QTextStream 对这些编码提供了完全连续的无缝支持。如果仍然想将文本
直接写入 QIODevice,那么必须为 open()函数明确地指定。IODevice::Text 标记,例如:
file.open(QIODevice::WriteOnly|QIODevice::Text);
当写入的时候,这个标记将通知 QIODevice 将"\n" 字符转换为 Windows 下的"\r\n" 序列。当读取的时候,这个标记将告知设备忽略所有平台环境中的 “\n” 字符。然后我们就可以假设:不管操作系统采用哪种行尾符转换,每一行结束都将使用一个"\n"换行符表示。
tidy.cpp
#include <QtCore>
#include <cstdio>
void tidyFile(QIODevice *inDevice, QIODevice *outDevice)
{
QTextStream in(inDevice);
QTextStream out(outDevice);
const int TabSize = 8;
int endlCount = 0;
int spaceCount = 0;
int column = 0;
QChar ch;
while (!in.atEnd()) {
in >> ch;
if (ch == '\n') {
++endlCount;
spaceCount = 0;
column = 0;
} else if (ch == '\t') {
int size = TabSize - (column % TabSize);
spaceCount += size;
column += size;
} else if (ch == ' ') {
++spaceCount;
++column;
} else {
while (endlCount > 0) {
out << endl;
--endlCount;
column = 0;
}
while (spaceCount > 0) {
out << ' ';
--spaceCount;
++column;
}
out << ch;
++column;
}
}
out << endl;
}
int main()
{
QFile inFile;
QFile outFile;
inFile.open(stdin, QFile::ReadOnly);
outFile.open(stdout, QFile::WriteOnly);
tidyFile(&inFile, &outFile);
return 0;
}