7.3 文本流QTextStream

标准 C++ 有三个常见的输入输出流:iostream 处理命令行输入输出、fstream 处理文件输出输出、sstream 处理内存字符串流的输入输出。通常将文本字符串转换成各种 C++ 变量的过程是输入,把内存中 C++ 变量表示成文本字符串的过程是输出。
Qt 提供了强大的文本流 QTextStream ,同时实现了三种 C++ 输入输出流的功能,并且还有功能增强,支持各种文本字符编码。QTextStream 一般用于操作各种编码格式的文本文件(QFile 对象)或字符串(QString、QByteArray),也可以打开 stdin、stdout 和 stderr 命令行的输入输出,并自动处理本地化编码和 Unicode 编码。
本节先将 QTextStream 类的内容分成四个小节来讲解,然后动手编写两个例子,将 QTextStream 的三种用途全部实现一遍,最后附加一个技巧,就是图形界面程序和命令行输入输出的结合,虽然图形界面程序和命令行的协作很少,但多学一点也好。

7.3.1 QTextStream 构造函数和普通读写函数

通常情况下 QTextStream 自己不会主动去打开一个文件,所以它的构造函数不接收文件名,而是接收 QFile 基类 QIODevice 的指针,可以用 QFile 对象的指针构造 QTextStream ,但需要注意在构造 QTextStream 之前要调用 QFile 对象的 open() 函数按需要的模式打开文件并检查是否正确打开。用于读写文件的 QTextStream 构造函数为:
    QTextStream(QIODevice * device)
设备指针 device 需要提前用 open() 函数打开。

对于文件设备,还有个不常用的构造函数:
    QTextStream(FILE * fileHandle, QIODevice::OpenMode openMode = QIODevice::ReadWrite)
fileHandle 是文件句柄,因为 Qt 里面的文件一般都用 QFile 处理,所以几乎不会用这个第二个函数处理文件。第二个构造函数的用途主要是和命令行的 stdin、stdout、stderr 三个文件句柄协作,处理命令行的输入输出。

以上两个构造函数第一个用于文件读写(类似 fstream),第二个一般用于命令行输入输出(类似 iostream),而 QTextStream 第三个用途就是对内存字符串的格式化输入和输出(类似 sstream)。内存字符串输入输出,使用的构造函数为:
    QTextStream(QString * string, QIODevice::OpenMode openMode = QIODevice::ReadWrite)
    QTextStream(QByteArray * array, QIODevice::OpenMode openMode = QIODevice::ReadWrite)
    QTextStream(const QByteArray & array, QIODevice::OpenMode openMode = QIODevice::ReadOnly)
注意后两个构造函数,如果传递 QByteArray 对象给构造函数,那么这个 QByteArray 对象是只读的。
如果传递 QByteArray 指针给构造函数,那么默认是可读可写的。

除了可以在构造函数指定文件设备或内存字符串,还可以在运行时修改:
void QTextStream::​setDevice(QIODevice * device)
void QTextStream::​setString(QString * string, QIODevice::OpenMode openMode = QIODevice::ReadWrite)
目前没有重新设置 QByteArray 的函数,只有修改文件设备和 QString 的函数。

我们这里约定一下概念,无论 QTextStream 构造函数里是文件设备或内存字符串,本节后面都把 QTextStream 对象称为 文本流。
如果我们需要从文本流提取各种 C++ 基本数值类型,如 short、int、long、double ,这些读操作一般用 7.3.2 节的操作子和运算符 <<、>>来实现,写入文本流时也一样。流操作子和运算符之外的读写函数就是本小节说的普通读写函数。
读取一整行文本的函数:
QString QTextStream::​readLine(qint64 maxlen = 0)
QTextStream::​readLine() 读取一行文本的时候,返回字符串末尾剔除了换行符("\n"  或 "\r\n"),而上一节 QIODevice 和 QFile 的 readLine() 函数返回字符串末尾带有一个 "\n" ,要注意区分。
如果要读取文本流中所有内容,可以用快捷函数:
QString QTextStream::​readAll()
如果希望从文本流中读取指定长度的文本,可以用函数:
QString QTextStream::​read(qint64 maxlen)
参数里是最多读取的字符数目,如果文本流里的数据不够,返回的字符串就是剩余的字符数。

在使用 QFile 对象指针构造 QTextStream 的时候,一旦把 QFile 对象指针交给 QTextStream 之后,就不要再调用 QFile 对象自己的读写函数了,因为 QTextStream 使用自己缓存的文件游标,底层 QFile 对象自己又进行读写之后,二者的文件游标很可能不 一致,会造成读写的混乱。既然使用 QTextStream 读写文件,那就只用 QTextStream 的读写函数或输入输出运算符。

QTextStream 的文件游标(其实应该叫文本流游标)可以用函数获取、移动,以及判断是否到达文件末尾:
qint64 QTextStream::​pos() const    //获取游标位置
bool QTextStream::​seek(qint64 pos)//移动游标到 pos
bool QTextStream::​atEnd() const    //是否到达文 本流末尾

对于文本流的写入操作,QTextStream 压根没有 write() 函数,写入操作全部是通过输出运算符 << 和流操作子实现的。
我们下面看看流操作子和 <<、>> 运算符。

7.3.2 QTextStream 流操作子和运算符

绝大多数情况下,QTextStream 都是利用 >> 运算符进行输入,利用 << 运算符进行输出,与这两个运算符配合的有一大堆操作子,这些操作子和 iostream 的操作子很类似,而且名字也几乎一样。流操作子一般用于控制输入输出的格式,大部分流操作子不带参数,Qt 提供额外提供了三个带参数的全局操作子,等会都列出 来。

QTextStream 支持所有 C++ 基本类型变量输入输出,并支持 Qt 自家的 QChar、QString、QByteArray,举例来说:
QTextStream & QTextStream::​operator<<(signed int i)
QTextStream & QTextStream::​operator>>(signed int & i)
第一个是整型变量的输出运算符(或叫插入运算符)函数,第二个是整型变量的输入运算符(或叫提取运算符)函数。
QTextStream 在代码里面进行输入输出,就与标准 C++ 的 cin 和 cout 一样用就可以了。例如:
    QString strIn = tr("100  29.88");
    QTextStream tsIn(&strIn);
    int nNum;
    double dblVal;
    //输入
    tsIn>>nNum>>dblVal;
    //打印
    qDebug()<<nNum<<dblVal;
这段代码执行后 nNum 就是数值 100, dblVal 就是浮点数 29.88 。qDebug() 其实就是获取调试输出流的对象,然后打印了两个数值。

QTextStream 里面重载的 << 和 >> 运算符就不一个个列举了,讲一下关于字符串读取的三个:
QTextStream & QTextStream::​operator>>(QString & str)
QTextStream & QTextStream::​operator>>(QByteArray & array)
QTextStream & QTextStream::​operator>>(char * c)
QTextStream 对于字符串的读取,其实只读取一个单词,而不是一整行。比如如果文本流原始为 "Hello world, aha !",如果用上面三个函数从头读取,那么读取的都只是第一个单词 "Hello" ,在源头的文件或内存字符串中,文本流中间都没有字符串终结符 '\0',所以 QTextStream 在输入时按照空白字符切割单词,每个单词算作一个小字符串返回。空白字符包括空格、制表符、换行符等等。

另外当文本流用 >> 读取逐个字符到 QChar 或 char 变量里面时,可以用
void QTextStream::​skipWhiteSpace()
跳过空白字符,如果不跳过空白字符,那么 QChar 和 char 也会接收到空白字符,如空格、制表符、换行符等等。

对于字符串的输出就没有上面那些问题了,因为输出时可以明确知道参数里的 QString 、QByteArray 、char * 是以 '\0' 结尾的,在 '\0' 前面的字符串都会被写入到输出文本流中。

输出文本流需要注意的是 QTextStream 有较大的输出缓冲区,如果用 stdout 或 stderr 构造 QTextStream 对象打印输出,要常用 flush  或 endl 操作子函数清空缓存,及时显示输出:
tsOut<<flush;
tsOut<<endl;
flush 和 endl 的区别是 endl 会换行,flush 不换行。

流的输入输出运算符用起来比较简单,下面重点看看控制格式的操作子,操作子本质都是函数,比如常用的换行 endl:
QTextStream & endl(QTextStream & stream)

我们这里把 QTextStream 的操作子大致列举并解释一下:
① 数值的进制和符号、小数点

操作子 描述
bin 读写二进制整数,等同于 setIntegerBase(2).
oct 读写八进制整数,等同于 setIntegerBase(8).
dec 读写十进制整数,等同于 setIntegerBase(10).
hex 读写十六进制整数,等同于 setIntegerBase(16).
showbase 输出时显示数值进制前缀,比如 "0x" 、"0"、"0b",等同于 setNumberFlags(numberFlags() | ShowBase).
forcesign 强制显示正号(正数和0前面),等同于 setNumberFlags(numberFlags() | ForceSign).
forcepoint 强制显示整数末尾的小数点,等同于 setNumberFlags(numberFlags() | ForcePoint).
noshowbase 不显示进制的前缀,等同于 setNumberFlags(numberFlags() & ~ShowBase).
noforcesign 不强制显示正号,等同于 setNumberFlags(numberFlags() & ~ForceSign).
noforcepoint 不强制显示小数点,等同于 setNumberFlags(numberFlags() & ~ForcePoint).
uppercasebase 数值进制的前缀为大写,如 "0X" 、"0B",等同于 setNumberFlags(numberFlags() | UppercaseBase).
uppercasedigits 基数大于10进制的数值里面的字母大写,比如16进制的 "FE00", 等同于setNumberFlags(numberFlags() | UppercaseDigits).
lowercasebase 数值进制的前缀小写,如 "0x" 、"0b",等同于 setNumberFlags(numberFlags() & ~UppercaseBase).
lowercasedigits 基数大于10进制的数值里面的字母小写,比如16进制的 "fe00", 等同于 setNumberFlags(numberFlags() & ~UppercaseDigits).

② 浮点数格式

操作子 描述
fixed 以定点数显示实数(比如 float、double 类型),比如 1024.1234,等同于 setRealNumberNotation(FixedNotation).
scientific 以科学计数法显示实数,比如 1.0241234e3,等同于 setRealNumberNotation(ScientificNotation).
qSetRealNumberPrecision(int precision) 这是全局操作子,带一个整型参数,指定实数显示的精度位数,等同于 QTextStream::setRealNumberPrecision(precision).

qSetRealNumberPrecision(int) 会对后续所有实数变量输出都生效,直到被改变为止,实数精度默认是 6 位,如果希望还原就设置精度为 6 。注意使用操作子 qSetRealNumberPrecision(int) 时最好一块明确指定使用 fixed 或者 scientific,这样显示结果才是符合预期的小数位精度。

③ 域宽和对齐方式、填充字符

操作子 描述
qSetFieldWidth(int width) 全局操作子,带一个整型参数,设定变量输出后的显示宽度,不足的用填充字符补齐(默认用空格补),等同于 QTextStream::setFieldWidth(width).
qSetPadChar(QChar ch) 全局操作子,带一个字符参数,如果设置的域宽比变量显示需要的宽度更宽,就用这个填充字符填补(默认是空格 ' '),等同于 QTextStream::setPadChar(ch).
left 设定域宽比变量显示需要的更宽时,变量显示时左对齐,右边空位补 PadChar, 等同于 setFieldAlignment(AlignLeft).
right 设定域宽比变量显示需要的更宽时,变量显示时右对齐,左边空位补 PadChar, 等同于 setFieldAlignment(AlignRight).
center 设定域宽比变量显示需要的更宽时,变量显示时居中对齐,两边空位补 PadChar, 等同于 setFieldAlignment(AlignCenter).

注意:qSetFieldWidth(int) 设置域宽之后,如果不重新设置,它就一直生效, 对后续所有变量输出都管用,包括 endl 都会补齐变为 指定宽 度。 如果要还原成不设置域宽的状态,就调用操作子 qSetFieldWidth(0) 。
qSetPadChar(QChar) 设置填充字符之后也是一直持续生效,直到被重新设置,如果还原成空格,就调用操作子 qSetPadChar(QChar(0x20)) 。

④ 其他操作子

操作子 描述
endl 添加换行符并清空输出缓存,把缓冲数据都写到输出文本流,等同于 stream << '\n' << flush;
flush 清空输出缓冲区,把缓冲数据都写到输出文本流,等同于 QTextStream::flush()
reset 把之前操作子或格式函数设定的格式全部重置,还原到流对象构建时的默认格式, 等同于 QTextStream::​reset()
ws 跳过输入文本流里的空白字符(包括空格、制表符、换行符等),直至遇到非空白字符, 等同于 QTextStream::​skipWhiteSpace()
bom 除非确定是写入 Unicode 文本文件否则不要设置,如果要设置,必须在所有写入操作之前调用该操作子, 等同于 setGenerateByteOrderMark(true).

bom(Byte Order Mark)只用于 Unicode 编码的文本文件,如果不是写入 Unicode 的字符编码格式(UTF-8、UTF-16、UTF-32)文本,就不要用 bom,后面的小节会讲到文本流和字符编码格式。

7.3.3 QTextStream 工作状态

本小节说明一下文本流的工作状态,以输入文本流为例,源头文本为 "Hello world, aha !" ,我们如果执行:
    //文本源
    QString strSrc = "Hello world, aha !";
    QTextStream tsIn( & strSrc );
    //变量
    QString strTemp;
    tsIn>>strTemp;
    qDebug()<<strTemp;
这段代码会从文本流提取一个单词赋值给 strTemp ,这样的输入操作当然是对的。但如果强行从非数值字符串提取数值到整型或浮点数类型,那么会发生什么呢?
    //文本源
    QString strSrc = "Hello world, aha !";
    QTextStream tsIn( & strSrc );
    //变量
    int nNum;
    tsIn>>nNum;
    qDebug()<<nNum;
    qDebug()<<tsIn.status();
strSrc 里面压根没有整型数值对应的字符串,上面代码执行之后 nNum 会被设置成默认的数值  0,而这时候 tsIn 应该报错才对,输入流发生了错误,无法为整型数提供 nNum 提供数值。
文本流是通过状态变化来反映刚才的输入或输出操作有无错误发生,获取当前文本流的状态就是用函数:
Status QTextStream::​status() const
枚举类型 Status 目前共有 4 个不同的枚举值:

Status 枚举常量 数值 描述
QTextStream::Ok 0 正常操作状态,没出错。
QTextStream::ReadPastEnd 1 底层设备已到达末尾,比如文件末尾,无数据可读了。
QTextStream::ReadCorruptData 2 读入了腐化数据,典型的就是文本源内容与目标变量类型不匹配。
QTextStream::WriteFailed 3 无法向底层设备写入数据,比如文件是只读的。

对于上面示例的代码,文本源没有整型变量对应的字符串,当 tsIn 向 nNum 输入数据时,就会发生 QTextStream::ReadCorruptData 错误,tsIn.status() 返回的数值就是 2 。
在进行文本流的输入输出时,如果出现莫名奇妙的错误,一定要注意检查文本流 QTextStream 的状态,很可能发生内容字符串与变量类型不匹配,或者无法写入底层设备等。

编程时需要仔细考虑用户的输入和程序输出是否异常!

对于错误的输入或输出应该如何处理呢?简单方法就是跳过错误的部分或用消息框提示,然后可以重新设置流的状态:
void QTextStream::​setStatus(Status status)    //把文本流设为指定状态
void QTextStream::​resetStatus()    //把状态重置为初始的 Ok 状态

7.3.4 QTextStream 与字符编码格式

请读者回顾一下 3.1 节的字符编码格式,对于 Unix/Linux 系统,默认的本地化文本字符编码是 UTF-8,对于简体中文 Windows 默认文本字符编码是 GBK,繁体中文 Windows 默认是 Big5 。QTextStream 类是比较智能的,它可以自动检测文本字符的本地化编码,在输入时,它自动把本地化编码字符串转成 Qt 需要的类型,比如 QString 默认内码是 UTF-16 ;在输出时,QTextStream 也会把 QString 变量的内码 UTF-16 转成本地化的字符串写入输出文本流。

QTextStream 不仅能对文本文件的本地化编码自动处理,也能与 操作系统本地的命令行交互,在交互过程中也会自动转码。QTextStream 还能自动检测 UTF-8、UTF-16、UTF-32 这些编码的文本,自动转为 Qt 默认的编码格式。使用 QTextStream 与操作系统里的文本文件、命令行等打交道,就不需要操心文本编码的问题。

但是如果真的要手动指定 QTextStream 输入流或输出流的编码格式,那也是可行的。如果要获取当前文本流的字符编码格式,使用函数:
QTextCodec * QTextStream::​codec() const
设置文本字符编码格式,使用函数:
void QTextStream::​setCodec(QTextCodec * codec)
QTextCodec 对象指针一般都是用 QTextCodec 类的静态函数获取:
QTextCodec * QTextCodec::​codecForName(const char * name)    //静态函数

QTextStream 类为方便程序员设定文本的字符编码格式,提供了一个更方便的 setCodec() 函数:
void QTextStream::​setCodec(const char * codecName)
这个 setCodec() 函数直接以编码的名称字符串为构造函数,内部自己调用 QTextCodec 类的静态函数获取文本字符编码器。

一般 QTextStream 不需要手动设置字符编码格式,当然,程序员如果知道应该用什么样的字符编码输入输出,就可以自己改,比如下面示范的就是按照 "UTF-8" 编码格式输出:
    QString strSrc = tr("Hello world, aha !");
    //输出
    QFile fileOut("test.txt");
    if( ! fileOut.open( QIODevice::WriteOnly ) )
    {
        return;
    }
    QTextStream tsOut( &fileOut);
    tsOut.setCodec("UTF-8");
    tsOut<<strSrc;
通常 UTF-8 格式不需要 bom,如果要为 UTF-16 或 UTF-32 编码的文本加 bom 头,一定要在所有的输出操作之前调用 tsOut<<bom ;

改变 QTextStream 的文本字符编码格式只需要一句代码而已,但是要根据实际需求来定,一般不需要自己设定字符编码格式。

QTextStream 还提供了与世界各国本地语言习惯 QLocale 相关的函数 locale() 和 setLocale(const QLocale & locale),这个与各国的语言语种有关系,最好不要修改,用默认的即可,默认的 QLocale::C 不会影响数据输入输出。

7.3.5 读写文本文件示例

本小节的例子主要实现读写自定义的文本格式,里面内容是这样的:
#姓名 岁数  体重
小明  18  50.5
小萌  19  55.8
小日  17  60.0
小月  16  55.1
小草  18  58.6
里面内容相当于是一个表格,第一行是井号打头的注释行,是预先定义好的三列表头。
然后从第二行开始是表格的数据,第一列是姓名,第二列是岁数,第三列是体重。每列数据之间用空格或者制表符分隔,每行的数据就用换行符分隔。我们下面开始编写示例 程序,能够读写这样的文本文件。

我们打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 tableedit,创建路径 D:\QtProjects\ch07,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
建好项目之后,先在项目文件夹里新建一个记事本文件,命名为 people.txt,把上面示范的文本内容保存到该文件里面,程序运行时会 用到。

现在回到 QtCreator,打开窗体 widget.ui 文件,进入设计模式,把主窗体大小设置为 480 * 300,然后按照下图排布,拖入控件:
ui
上面控件从左边看是两大行:
第一大行:左边是 QListWidget 控件,对象名为 listWidget,右边是一个垂直布局器,在垂直布局器里面有三个按钮和一个垂直弹簧条。竖排的第一个按钮,文本 "加载表格",对象名 pushButtonLoad,第二个按钮,文本 "保存表格",对象名 pushButtonSave ,然后是垂直的弹簧条 verticalSpacer,第三个按钮,文本 "删除行",对象名 pushButtonDelRow 。
第一大行是按照水平布局器布局。

第二大行:标签控件,文本 "姓名",单行编辑器,对象名 lineEditName;
标签控件,文本 "岁数",单行编辑器,对象名 lineEditAge;
标签控件,文本 "体重",单行编辑器,对象名 lineEditWeight;
按钮控件,文本 "添加行",对象名为 pushButtonAddRow。
第二大行也是按水平布局器排布。

主窗体是以垂直布局器作为主布局。

按照上面描述设置好界面和布局之后,首先分四次为四个按钮添加 clicked() 信号对应的槽函数:
slot1
四个按钮的用途比较明了,就是按钮文本显示的,上面两个按钮用于从文件加载表格和写入数据到文本文件;下面两个按钮用于从列表控件删除一行数据或者添加一行数据。
然后我们为列表控件 listWidget 添加 currentRowChanged(int) 信号对应的槽函数:
slot2
当用户在列表控件选中某行时,我们利用这个信号对应的槽函数,把该行的数据同步显示到三个单行编辑器里面,方便用户编辑数据,然后添加行或删除行。

按上面描述添加好槽函数之后,我们保存界面文件,关闭界面文件,回到 QtCreator 代码编辑模式。
先来看看 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_pushButtonLoad_clicked();

    void on_pushButtonSave_clicked();

    void on_pushButtonDelRow_clicked();

    void on_pushButtonAddRow_clicked();

    void on_listWidget_currentRowChanged(int currentRow);

private:
    Ui::Widget *ui;
};

#endif // WIDGET_H

然后我们打开源代码文件 widget.cpp,开始编写功能代码,首先编写头文件包含和构造函数的部分:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QMessageBox>
#include <QFileDialog>      //打开或保存文件对话框
#include <QFile>            //文件类
#include <QTextStream>      //文本流输入输出
#include <QListWidgetItem>  //列表控件条目

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //设置列表控件的选中模式,一次只能选一行
    ui->listWidget->setSelectionMode( QAbstractItemView::SingleSelection );
}

Widget::~Widget()
{
    delete ui;
}
包含的 <QFileDialog>、<QFile> 是文件对话框和文件类,<QTextStream> 是本节的文本流,<QListWidgetItem> 是列表控件条目,用于从列表条目提取信息。

构造函数只添加了一句代码,就是设置列表控件的选中模式,每次只能选中一行,这样简化我们 "删除行" 按钮的操作,每次只处理一行。

接下来是 "加载表格" 按钮对应的槽函数:
void Widget::on_pushButtonLoad_clicked()
{
    //获取打开文件名
    QString strFile = QFileDialog::getOpenFileName(
                this,
                tr("打开文件"),
                tr("."),
                tr("Text Files(*.txt);;All files(*)")
                );
    //判断文件名
    if( strFile.isEmpty() )
    {
        return;
    }
    //有文件名,定义文件对象并打开
    QFile fileIn(strFile);
    if( ! fileIn.open(QIODevice::ReadOnly) )
    {
        QMessageBox::warning(this, tr("打开文件"),
                             tr("打开文件失败:") + fileIn.errorString());
        return;
    }
    //文件已经正确打开,定义文本流
    QTextStream tsIn(&fileIn);
    //表格的列数据类型
    QString strCurName;
    int nCurAge;
    double dblCurWeight;
    //先清空旧的列表
    ui->listWidget->clear();

    //加载自定义的表格数据
    while( ! tsIn.atEnd() )
    {
        //提取打头的字符串
        tsIn>>strCurName;
        //判断空行
        if( strCurName.isEmpty() )
        {
            tsIn.skipWhiteSpace();  //跳过空白
            continue;
        }
        //判断注释行
        if( strCurName.startsWith( '#' ) )
        {
            //是注释行,跳过这一行
            tsIn.readLine();
            continue;
        }
        //现在 strCurName 是正常的人名
        //提取后续的年龄、体重
        tsIn>>nCurAge>>dblCurWeight;

        //原本可以用 arg 函数进行内存字符串格式化
        //这里用文本流格式化输出到内存字符串里面
        QString strOut;
        QTextStream tsOut( & strOut );
        tsOut<<strCurName<<"\t"<<nCurAge<<"\t";
        //定点数形式,精度为2,输出到内存字符串
        tsOut<<fixed<<qSetRealNumberPrecision(2)<<dblCurWeight;
        //内存中输出字符串填充好了,添加到列表控件
        ui->listWidget->addItem( strOut );
    }
    //提示加载完毕
    QMessageBox::information(this, tr("加载表格"), tr("加载自定义表格完毕。"));
}
这个槽函数前半部分代码是获取打开文件名 QFileDialog::getOpenFileName(),定义文件对象 fileIn 并以只读模式打开文件,然 后根据文件对象指针 定义 输入文本流 tsIn 。
得到输入文本流 tsIn 之后,定义三个列的数据 strCurName、nCurAge、dblCurWeight,用这三个变量循环加载文本流里面的行。加载数据前,先清空了列表控件的内容   ui->listWidget->clear() 。

函数后半部分代码就是用 while 循环加载数据:

对于每行数据,先读取一个单词到 strCurName,如果单词为空,那么说明是空行,就跳过空白区域;
如果单词 strCurName 以井号打头,说明注释行,用 tsIn.readLine() 跳过注释行;
如果单词 strCurName 所在行不是空行也不是注释行,那么说明 strCurName 是姓名的列内容,读取该行后续的内容到两个变量 nCurAge 和 dblCurWeight,这样就把文件中的一行读取到三个变量里面了。

对于三个变量 strCurName 、nCurAge 、dblCurWeight,我们需要构造一个内存字符串 strOut,把三项数据都打印到 strOut 里面,然后添加到列表控件显示。可以用 QString::arg() 函数实现格式化字符串,也可以用 QTextStream 格式化内存字符串,我们这里使用 QTextStream 的方法,根据 strOut 构造输出流 tsOut,然后把变量打印到输出流,变量之间用制表符 "\t" 分隔,对于浮点数 dblCurWeight 我们用定点数形式显示,显示精度为 2 。

三个变量打印到输出流 tsOut,就等于都写入到内存字符串 strOut ,我们添加 strOut 到列表控件,就可以显示了。

while 循环会一直执行到文件末尾,无数据可加载就结束循环,然后提示文件已经加载完毕。
这个函数展示了 QTextStream 的两个功能:加载已打开的文件(类似 fstream);输出变量到内存字符串(类似 sstream)。

然后我们来看看 "保存表格" 按钮对应的槽函数:
void Widget::on_pushButtonSave_clicked()
{
    //条目计数
    int nCount = ui->listWidget->count();
    if(nCount < 1)
    {
        //没有数据
        return;
    }
    //获取保存的文件名
    QString strFileSave = QFileDialog::getSaveFileName(
                this,
                tr("保存文件"),
                tr("."),
                tr("XLS files(*.xls);;Text Files(*.txt)")
                );
    if(strFileSave.isEmpty())
    {
        //文件名是空的
        return;
    }
    //打开输出文件
    QFile fileOut(strFileSave);
    if( ! fileOut.open(QIODevice::WriteOnly))
    {
        QMessageBox::warning(this, tr("保存文件"),
                             tr("打开保存文件失败:") + fileOut.errorString());
        return;
    }
    //定义输出文本流
    QTextStream tsOut( & fileOut);
    //先打印一行表头
    tsOut<<tr("#姓名\t岁数\t体重")<<endl;
    //从列表提取信息
    for(int i=0; i<nCount; i++)
    {
        QString strCurAll = ui->listWidget->item(i)->text();
        tsOut<<strCurAll<<endl;
    }
    //提示保存完毕
    QMessageBox::information(this, tr("保存文件"), tr("保存表格文件成功。"));
}
这个槽函数先获取列表控件条目的计数,如果没数据就不需要保存。
如果有数据,就获取要保存的文件名,定义文件对象 fileOut,打开文件;
然后根据已经打开的文件对象 fileOut 指针定义输出文本流 tsOut。

定义好输出文本流 tsOut 之后,先输出一行井号打头的表头信息;
然后循环输出列表控件的每一行文本到输出流。
将列表控件所有行输出到文件之后,提示保存成功信息。

这里没有将从列表控件的每行文本提取 姓名、岁数、体重三个变量,因为列表控件的每一行格式都是我们之前设计好的,都是符合自定义的表格文本规律,不需要再抽取数据,再把数据变成字符串,没必要倒来倒 去。

程序的输出永远比输入简单,因为输入可能有各种错误格式的信息,而输出时只要自己定义好了格式就行。

这里教大家一个小技巧,在获取保存文件名时,我们默认的扩展名是 *.xls 。Excel 程序生成的 xls 文件我们不能打开,因为真正的 xls 文件很复杂。但如果我们把记事本文件扩展名改为 xls,然后里面的每列用 "\t" 字符分隔,每行就用换行符,然后生成的文本文件,Excel 程序是可以打开的,并且能正常识别表格。以后我们如果需要输出数据为 Excel 表格,就可以用这种假的 xls 文件,本质是文本文件,只要列用制表符 "\t" ,行尾用普通换行符,那么 Excel 就能自动识别为表格。

接下来是 "删除行" 按钮对应的槽函数的代码:
void Widget::on_pushButtonDelRow_clicked()
{
    //选取当前选中的行
    int nCurIndex = ui->listWidget->currentRow();
    if(nCurIndex < 0)
    {
        //没有选中合法行
        return;
    }
    else
    {
        //删除行
        ui->listWidget->takeItem( nCurIndex );
    }
}
这个槽函数很简单,先获取当前选中的行,如果行的序号合法,就删除该行。列表控件删除行的函数为 takeItem() ,这个函数名比较特殊,不是 del* 也不是 remove * 之类的名字。

接下来是 "添加行" 按钮对应的槽函数代码:
void Widget::on_pushButtonAddRow_clicked()
{
    QString strName = ui->lineEditName->text().trimmed();
    QString strAge = ui->lineEditAge->text().trimmed();
    QString strWeight = ui->lineEditWeight->text().trimmed();
    //判断是否为空
    if( strName.isEmpty() || strAge.isEmpty() || strWeight.isEmpty() )
    {
        QMessageBox::warning(this, tr("添加行"), tr("请先填好三项数据再添加!"));
        return;
    }
    int nAge = strAge.toInt();  //年龄
    double dblWeight = strWeight.toDouble();    //体重
    //判断年龄体重是否正确
    if( (nAge < 0)  || (nAge > 600) )
    {
        QMessageBox::warning(this, tr("添加行"), tr("年龄不能是负数或超过600!"));
        return;
    }
    if( dblWeight < 0.1 )
    {
        QMessageBox::warning(this, tr("添加行"), tr("重量不能低于 0.1 kg !"));
        return;
    }
    //添加正确的数据,这里使用 arg 函数实现格式化字符串
    //无论用 arg 函数还是 QTextStream 都可以实现一样的效果
    QString strCurAll = tr("%1\t%2\t%3").arg(strName)
            .arg(nAge)
            .arg( dblWeight, 0, 'f', 2 );
    ui->listWidget->addItem( strCurAll );
}
在添加行的时候,先获取三个单行编辑器的内容,如果单行编辑器内容为空就不处理。
然后我们把年龄字符串转为整型数,体重字符串转为浮点数类型。
判断年龄范围 0 到 600 ,判断体重必须有 0.1 kg,这里只是简单做一下输入数据的验证。
验证数据之后,我们用 QString 类的 arg 函数实现了格式化字符串,这三个 arg 函数效果和 on_pushButtonLoad_clicked() 里面的输出文本流 tsOut 功能类似,都是把三个变量转为一个内存字符串,添加到列表控件。

对于多个变量转为内存字符串,无论用 QTextStream 还是 QString::arg() 函数都可以,看程序员的喜好。
如果从内存字符串提取多个数值变量,那么毫无疑问是用 QTextStream 方便,因为 QTextStream 自动分隔单词,自动转换为目标数值类型。QString 的 to***() 函数只能转换为一个变量,没法同时转为多个变量。

最后一个函数就是列表控件选中的行变化时执行的槽函数:
//选中的行变化时,把当前选中行的数据显示到下方编辑器里 面
void Widget::on_listWidget_currentRowChanged(int currentRow)
{
    if( currentRow < 0 )
    {
        //非法的行号不处理
        return;
    }
    //正常行号,读取行文本
    QString strCurAll = ui->listWidget->item( currentRow )->text();
    //根据字符串构造输入流
    QTextStream tsLine( &strCurAll );
    //三项数据
    QString strName;
    int nAge;
    double dblWeight;
    //从当前行文本提取三项数据
    tsLine>>strName>>nAge>>dblWeight;

    //显示到三个编辑器
    ui->lineEditName->setText( strName );
    ui->lineEditAge->setText( tr("%1").arg( nAge ) );
    ui->lineEditWeight->setText( tr("%1").arg(dblWeight) );
}
这个函数先判断参数里的当前行序号 currentRow ,如果行号不合法就不处理。
对于合法行号,提取当前的文本字符串 strCurAll ,这个字符串里面其实同时有三个变量对应的字符串,从内存字符串同时提取三个变量,用 QTextStream 实现更方便。
我们根据 strCurAll 指针构造输入文本流 tsLine,然后直接用 >> 运算符提取三个变量就行了。
得到三个变量 strName 、nAge 、dblWeight 之后,把它们显示到相应的单行编辑器。

可能有读者会觉得上面把变量和内存字符串倒来倒去有点重复操作,做无用功,是不是无用功不要紧。这个例子主要展示的就是如何用 QTextStream 读写文件和 做内存字符串的格式化输入输出,把 QTextStream 的用法学会是主要目的。

以上就是这个例子的代码,我们现在生成并运行例子看看效果,加载我们之前放好的 people.txt:
run
其他按钮功能读者自行测试一下,这里不截图了。这个例子展示了 QTextStream 的两个主要功能,读写文本文件(类似 fstream),内存字符串的格式化输入输出(类似 sstream),还剩下一个在命令行输入输出(类似 iostream)的功能,作为下面的例子。

7.3.6 命令行输入输出示例

我们之前一直用 QtCreator 新建图形界面的程序,现在我们新建一个 Qt 命令行程序,展示 QTextStream 的第三种用途。我们重新打开 QtCreator,点击 "文件" --> "新建文件或项目",在新建对话框里,左边选择 "Application",中间选择 "Qt 控制台应用",如下图所示:
new1
然后点击右下角的 Choose 按钮,进入下面界面:
new2
项目名称设置为 console,创建路径为 D:\QtProjects\ch07,点击 "下一步",进入如下界面:
new3
选中所有的套件,点击 "下一步",进入下面界面:
new4
点击 "完成" 按钮,这样新的 console 控制台程序就建好了。
Qt 控制台项目很简单,只有 console.pro 和 main.cpp 是源代码文件,我们可以看看控制台项目 pro 文件内容:
#-------------------------------------------------
#
# Project created by QtCreator 2015-12-02T15:29:43
#
#-------------------------------------------------

QT       += core

QT       -= gui

TARGET = console
CONFIG   += console
CONFIG   -= app_bundle

TEMPLATE = app


SOURCES += main.cpp

里面使用的模块只有 core,不使用 gui 模块。CONFIG 相关的行是项目类型的配置代码,
CONFIG   += console
就是明确指定使用命令行。
CONFIG   -= app_bundle
是指不使用 app_bundle ,这个 app_bundle (Puts the executable into a bundle)是苹果操作系统里的应用配置,对本例子没影响。

QtCreator 新建的控制台项目默认只有一个 main.cpp ,因为不涉及图形界面,里面的内容很简单:
#include <QCoreApplication>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    return a.exec();
}
QCoreApplication 是最基本的应用程序类型,对象 a 代表 Qt 控制台程序自己,main() 函数里没有其他代码。

我们这个例子内容也比较简单,就是编写 main.cpp 文件里面的代码,首先是添加头文件包含和一个前置的函数声明:
#include <QCoreApplication>
#include <QTextStream>

//函数声明,根据不同功能代码实现输入输出功能
void Funcs(int nCode, QTextStream &tsIn, QTextStream &tsOut);

Func() 函数接收一个功能代号 nCode,根据代号的不同,实现不同的功能,tsIn 是输入流(类似 cin),tsOut 是输出流(类似 cout)。根据功能代码,从输入流 tsIn 提取不同类型的数值,然后打印对应的信息到输出流 tsOut ,原理就是这么简单。

接下来看看修改之后的 main() 函数代码:
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QTextStream tsIn(stdin);    //类似 cin
    QTextStream tsOut(stdout);  //类似 cout

    while(true)
    {
        //显示程序功能
        QString strFuns = a.tr( "功能代码:\n"
                                "1. 输入整型数\n"
                                "2. 输入浮点数\n"
                                "3. 输入单词\n"
                                "4. 输入整行句子\n"
                                "9. 退出程序\n"
                                "请输入功能代码: "
                                );
        //显示输出时一定要带上 <<flush 或者 <<endl
        tsOut<<strFuns<<flush;
        //获取用户输入功能代码
        int nCode;
        tsIn>>nCode;
        if( 9 == nCode )
        {
            tsOut<< a.tr("程序结束。")<<endl;
            //结束
            return 0;
        }
        else
        {
            Funcs(nCode, tsIn, tsOut);
        }
    }

    return a.exec();
}
main() 函数开头的地方,stdin 就是命令行标准输入设备的文件句柄,stdout 就是标准输出设备的文件句柄,我们根据 stdin 和 stdout 分别构造输入流 tsIn 和输出流 tsOut ,在命令行进行输入输出直接使用这两个 QTextStream 对象即可,就是这么简单的两句。

需要注意的是 tsOut 输出流,QTextStream 对象默认有较大的输出缓冲区,如果希望及时地显示输出消息,那需要在输出变量或文本时,加上操作子 <<flush 或者 <<endl ,保证清空 tsOut 内部输出缓存,立即在命令行显示消息。

在定义好 tsIn 和 tsOut 之后,是一个 while 循环,条件一直为真。
while 循环里的代码是先打印一条提示消息 strFuns ,告知用户可以输入哪些功能代码。
然后使用 tsIn 从输入流提取一个数值给 nCode ,判断 nCode 数值:
如果 nCode 为 9 ,就结束程序;
否则调用 Funcs() 函数,让 Funcs() 函数根据不同功能代码进行后续输入输出操作。

现在我们来看看这个功能函数:
//功能函数定义
void Funcs(int nCode, QTextStream &tsIn, QTextStream &tsOut)
{
    tsOut<<endl;    //开始时换行
    QString strOut; //打印字符串

    QString strIn;  //接收单词和整行字符串
    int nNum;           //接收整数
    double dblValue;    //接收浮点数

    switch (nCode) {
    case 1:
        strOut = qApp->tr("请输入整数: ");
        tsOut<<strOut<<flush;
        tsIn>>nNum;
        //显示
        strOut = qApp->tr("您刚输入的是:%1").arg( nNum );
        tsOut<<strOut<<endl;
        break;

    case 2:
        strOut = qApp->tr("请输入浮点数: ");
        tsOut<<strOut<<flush;
        tsIn>>dblValue;
        //显示
        strOut = qApp->tr("您刚输入的是:%1").arg( dblValue );
        tsOut<<strOut<<endl;
        break;

    case 3:
        strOut = qApp->tr("请输入一个单词: ");
        tsOut<<strOut<<flush;
        tsIn>>strIn;    //读取空白分隔的一个单词
        //显示
        strOut = qApp->tr("您刚输入的是: %1").arg( strIn );
        tsOut<<strOut<<endl;
        break;

    case 4:
        strOut = qApp->tr("请输入一行字符串: ");
        tsOut<<strOut<<flush;
        //跳过用户输入功能代码时敲的回车键字符
        tsIn.skipWhiteSpace();
        strIn = tsIn.readLine();    //读取一行
        //显示
        strOut = qApp->tr("您刚输入的是: %1").arg( strIn );
        tsOut<<strOut<<endl;
        break;

    default:
        strOut = qApp->tr("未知功能代码  %1 ,不处理。").arg(nCode);
        tsOut<<strOut<<endl;
        break;
    }
    //结束时再换一行
    tsOut<<endl;

    /**判断一下输入流的状态**/
    if( tsIn.status() != QTextStream::Ok)
    {
        //有可能出现用户输入错误,把错误的输入数据跳过去
        tsIn.readLine();    //跳过一行
        tsIn.resetStatus(); //重置状态
    }
}
Funcs()  代码主要就是一大段 switch-case 代码,如果 nCode 为 1,就提示用户输入一个整数,然后从 tsIn 输入流提取一个数值给 nNum,然后构造 strOut 字符串,打印回显消息。
当 nCode 为 2、3、4 时,代码也是差不多的,只是接收的数据类型稍有差异。

nCode 为 4 的时候,case 段落代码比其他情况多一行:tsIn.skipWhiteSpace();
这是因为当用户之前输入功能代码 '4' 的时候,会按一下回车键,这个回车键字符还在输入流里面,所以先要把这个回车键字符跳过去,再提取后面非空白的一行字符 串。

如果用户输入了未知的功能代码,那么打印提示信息,告知用户没法处理。

在 Funcs() 函数末尾有一小段非常重要的排错代码,如果用户在前面过程输入了不合法的数据,比如把英文字母当作整数或浮点数输入,那么 tsIn 的状态会变成 QTextStream::ReadCorruptData  状态。在这种情况下,输入流 tsIn 里面腐化的数据,比如一堆英文字母,还会留在缓冲区,而且文本流的游标会卡在这段英文字母前面,一直无法移动,整个文本流会卡死在这。

如果不使用 tsIn.readLine() 跳过一行腐化数据,那么 Funcs() 返回之后 ,main()  函数里的 while 会一直持续循环打印错误信息,程序就失控了,用户就无法再操作。Func()  函数末尾有这段状态判断和跳过腐化数据代码,就保证程序不会因为错误的 输入而一直失控。

整个示例程序的代码就是上面那些,注意打印字符串是一定要用 tr() 函数封装或者转成 QString 变量打印,因为 QTextStream 会对输入输出做本地化转码,使用 QString 类型保证字符串的输入输出不乱码。
Funcs() 函数里面调用 tr() 函数的方式是 qApp->tr()  ,这里的 qApp 是 Qt 程序默认定义的全局宏,代表应用程序对象自身指针,就是 main() 函数里的那个对象 a 的全局指针。QCoreApplication 类内部也有静态函数 tr() ,与其他所有窗口和控件类的 tr() 函数是一样使用的。

我们生成并运行一下示例程序看看,功能代码输入 3,本来程序提示输入单词,我们输入一整句: Hello world !
看到效果如下:
run
在输入一句 "Hello World !" 之后按回车,因为代码
    case 3:
        strOut = qApp->tr("请输入一个单词: ");
        tsOut<<strOut<<flush;
        tsIn>>strIn;    //读取空白分隔的一个单词
        //显示
        strOut = qApp->tr("您刚输入的是: %1").arg( strIn );
        tsOut<<strOut<<endl;
        break;
只会提取一个单词到  strIn 里面,strIn 就等于 "Hello" ,后面的 "World !" 还在输入流里,没有提取。
然后回到 while 循环,这时候 while 循环开头打印了关于功能代码的提示信息,
程序会自动从输入流 "World !" 抽取一个整型数值给 nCode ,但输入流没有整型数对应的字符串,出现腐化数据输入的状态,nCode 会被自动设置为 0,所以命令行出现了 "未知功能代码 0,不处理" 的信息。
之后在 Func()  函数末尾会对错误的输入流状态进行判断,并跳过错误的输入行,就把腐化数据 "World !"  跳过去,接下来就是功能正常的 while 循环了。

如果 Func() 末尾不判断输入流状态,不跳过一行腐化数据,那么程序输入就会卡死在 "World !" ,while 就会不断打印错误信息,直到强制结束进程为止。

即使是最简单的命令行程序,涉及到用户输入也要特别小心,因为非正常的输入信息容易导致无法预料的程序错误。程序员在编写代码时一定要注意检查用户的异常输入,异 常输入处理不当,轻则程序崩溃,重则留下严重漏洞,为黑客留下可乘之机,所以要特别注意程序的输入检查。

7.3.7 附加技巧:图形程序和命令行输入输出协作

一般情况下,使用图形界面就没必要使用命令行进行输入输出,因为有丰富的控件来接受输入和显示输出信息。图形界面的程序很少从命令行接收输入或打印输出,但是为了 以防万一,这里教大家一个小技巧,可以让图形界面程序同时使用命令行的输入输出,虽然暂时不知道有什么用,就当一个附加技巧吧。
这个示例首先按照正常的图形界面程序创建项目,然后向 pro 文件添加一行代码,再修改项目的运行配置,就能实现我们预期的效果。下面开始这个示范过程。

我们重新打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 guicui,创建路径 D:\QtProjects\ch07,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。

在 QtCreator 代码编辑模式,我们打开 guicui.pro 文件,向其中添加一句代码:
#-------------------------------------------------
#
# Project created by QtCreator 2015-12-04T17:00:52
#
#-------------------------------------------------

QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = guicui
TEMPLATE = app


SOURCES += main.cpp\
        widget.cpp

HEADERS  += widget.h

FORMS    += widget.ui

CONFIG   += console

最后一句  CONFIG   += console,就是新增的,必须在 pro 文件里加这一句,才能正常使用 stdin、stdout 和 stderr 在命令行里进行输入输出。

保存 pro 文件,关闭 pro 文件,然后我们点击 QtCreator 左侧的 "项目" 按钮,进入项目配置界面:
conf1
在项目配置界面,点击上面 Qt 5.4.0 套件下方 "运行" 按钮,进入该套件运行环境配置:
conf2
把 "Run in terminal" 的复选框勾选就行了。

然后点击 QtCreator 左边的 "编辑" 按钮,回到 QtCreator 代码编辑模式,先不编写代码,我们直接生成并运行例子:
run1
因为选中了在命令行终端运行,所以现在程序运行时先启动一个命令行,然后在运行图形界面程序,如上图所示。

我们关闭运行中的图形界面程序和命令行,现在开始设计图形界面。
打开 widget.ui ,进入界面设计模式,按照下图拖入控件:
ui
总共只有一行控件:左边的是按钮控件,文本 "接收输入",对象名 pushButtonIn;
中间的是单行编辑器,对象名 lineEditMsg;
右边的是按钮控件,文本 "打印输出",对象名 pushButtonOut 。
窗口的主布局器就是水平布局器,点击窗口空白区域,再点击上面水平布局按钮就行了。

这个界面的意义就是点击 "接收输入" 按钮,从命令行提取一行输入,显示到单行编辑器;
点击 "打印输出" 按钮,我们把单行编辑器里的内容再打印到命令行里面,实现从命令行输入到输出的一套过程。

现在控件和布局设置好了,我们为两个按钮都添加 clicked() 信号对应的槽函数:
slot
添加好槽函数之后,保存界面文件,关闭界面文件,回到 QtCreator 代码编辑模式。
我们打开 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_pushButtonIn_clicked();
    
    void on_pushButtonOut_clicked();
    
private:
    Ui::Widget *ui;
};

#endif // WIDGET_H

下面我们打开 widget.cpp ,开始编写功能代码,首先是包含的头文件和构造函数:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QTextStream>

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

Widget::~Widget()
{
    delete ui;
}
头文件就增加了调试类 <QDebug> 和 文本流 <QTextStream> ,构造函数和析构函数没有修改代码。

我们来看看 "接收输入" 按钮对应的槽函数代码:
void Widget::on_pushButtonIn_clicked()
{
    //输入和输出流
    QTextStream tsIn(stdin);
    QTextStream tsOut(stdout);
    //在命令行提示用户输入一行字符串
    QString strOut = tr("请输入一行字符串:");
    tsOut<<strOut<<endl;
    //接收输入
    QString strMsg = tsIn.readLine();
    //显示到单行编辑器
    ui->lineEditMsg->setText( strMsg );
}
因为要在命令行提示用户输入一行字符串,所以需要同时使用输入流 tsIn 和输出流 tsOut。
定义好 tsIn 和 tsOut 之后,先打印一行提示用户输入的信息到命令行,然后从命令行接收一行字符串输入,保存到 strMsg ,最后将 strMsg 显示到单行编辑器。
该函数里与命令行的输入输出交互就是完全通过 QTextStream 两个对象实现的,交互过程很简单,和平常的命令行程序没啥区别。

最后来看看 "打印输出" 按钮对应的槽函数代码:
void Widget::on_pushButtonOut_clicked()
{
    //只需要输出流
    QTextStream tsOut(stdout);
    //打印一行字符串
    QString strMsg = ui->lineEditMsg->text();
    tsOut<<endl<<tr("输出信息:")<<strMsg<<endl;

    //打印调试信息,也会自动显示到命令行
    qDebug()<<endl<<tr("这行是调试信息。")<<endl;
}
因为只需要输出,所以只用到了 tsOut 。
我们获取单行编辑器里的文本,然后打印到输出流 tsOut 就搞定了。
最后的一句 qDebug() 打印调试信息,经过我们上面对项目的修改和运行设置,现在 qDebug() 会默认把调试信息打印到命令行里面,程序运行时可以在命令行看到调试输出的信息。

例子代码只有上面那些,比较简单,我们生成运行例子,把从输入到输出的过程走一遍,可以看到效果:
run2
运行的效果很直观,可以从命令行提取输入信息到单行编辑器,也可以把单行编辑器的信息打印到命令行,qDebug() 调试信息也会自动打印到命令行显示了。

关于 QTextStream 的内容和例子就讲到这,本节前面内容比如一大堆操作子等列表,不用死记硬背,到需要用的时候再查询文档就可以了。本节三个例子里面关于 QTextStream 的用法要认真学,比如对文本文件的读写、内存字符串的格式化输入输出、命令行输入输出,这三大用途一定要学会。我们下一节介绍串行化数据流 QDataStream。




prev
contents
next