Graphics View Framework
Graphics View 提供了一种接口,用于管理大量自定义的 2D 图形元素,并与之进行交互;还提供了用于将这些元素进行可视化显示的观察组件,并支持缩放和旋转。
Graphics View 框架包含了一套完整的事件体系,可以用于与场景中的元素进行双精度的交互。这些元素同样支持键盘事件、鼠标事件等。Graphics View 使用了 BSP 树(Binary Space Partitioning tree,这是一种被广泛应用于图形学方面的数据结构)来提供非常快速的元素发现,也正因为如此,才能够实现一种上百万数量级元素的实时显示机制。
Graphics View 是一个基于元素(item)的 MV 架构的框架。它可以分成三个部分:元素 item、场景 scene 和视图 view。
基于元素的意思是,它的每一个组件都是一个独立的元素。这是与QPainter状态机机制不同。使用QPainter绘图,大多是采用一种面向过程的描述方式:首先使用drawLine()画一条直线,然后使用drawPolygon()画一个多边形。对于 Graphics View,相同的过程可以是,首先创建一个场景(scene),然后创建一个直线对象和一个多边形对象,再使用场景的add()函数,将直线和多边形添加到场景中,最后通过视图进行观察。
MV 架构的意思是,Graphics View 提供一个 model 和一个 view,所谓模型(model)就是我们添加的种种对象;所谓视图(view)就是我们观察这些对象的视口。同一个模型可以由很多视图从不同的角度进行观察。
Graphics View 提供了QGraphicsScene作为场景,即是允许我们添加图形的空间,相当于整个世界;QGraphicsView作为视口,也就是我们的观察窗口,相当于照相机的取景框,这个取景框可以覆盖整个场景,也可以是场景的一部分;QGraphicsItem作为图形元件,以便添加到场景中去,Qt 内置了很多图形,比如直线、多边形等,它们都是继承自QGraphicsItem。
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsScene scene;
scene.addLine(0, 0, 150, 150);
QGraphicsView view(&scene);
view.setWindowTitle("Graphics View");
view.resize(500, 500);
view.show();
return app.exec();
}
这段代码很简单:首先创建一个场景,也就是QGraphicsScene对象。然后我们使用addLine()函数向场景中添加了一个直线,起始点和终点坐标分别是 (0, 0) 和 (150, 150)。通过这两步,我们已经有了场景和元素。之后,我们创建一个GraphicsView对象,绑定到一个场景上(也就是我们前面创建的 scene 对象)。
注意,QGraphicsScene不是QWidget的子类,因此该构造函数并不是调用的QGraphicsView(QWidget *parent)。
我们看到,这个直线自动在视图居中显示。这并不需要我们进行任何额外的代码。如果不想这么做,我们可以给 scene 设置一下sceneRect()属性:
QGraphicsScene scene;
scene.setSceneRect(0, 0, 300, 300);
scene.addLine(0, 0, 150, 150);
QGraphicsView view(&scene);
view.setWindowTitle("Graphics View");
// view.resize(500, 500);
view.show();
不仅如此,我们还去掉了view.resize()一行。QGraphicsScene的sceneRect属性供QGraphicsView确定视图默认的滚动条区域,并且协助QGraphicsScene管理元素索引。之所以去掉view.resize()一行,是因为我们让系统去决定视图的最小尺寸(否则的话,我们需要手动将窗口标题栏等的大小同时考虑设置)。
文件
- QIODevice:所有 I/O 设备类的父类,提供了字节块读写的通用操作以及基本接口;
- QFlie:访问本地文件或者嵌入资源;
- QTemporaryFile:创建和访问本地文件系统的临时文件;
- QBuffer:读写QByteArray;
- QProcess:运行外部程序,处理进程间通讯;
- QAbstractSocket:所有套接字类的父类;
- QTcpSocket:TCP协议网络数据传输;
- QUdpSocket:传输 UDP 报文;
- QSslSocket:使用 SSL/TLS 传输数据;
- QFileDevice:Qt5新增加的类,提供了有关文件操作的通用实现。
这其中,QProcess、QTcpSocket、QUdpSoctet和QSslSocket是顺序访问设备。所谓“顺序访问”,是指它们的数据只能访问一遍:从头走到尾,从第一个字节开始访问,直到最后一个字节,中途不能返回去读取上一个字节;QFile、QTemporaryFile和QBuffer是随机访问设备,可以访问任意位置任意次数,还可以使用QIODevice::seek()函数来重新定位文件访问位置指针。
在所有的 I/O 设备中,文件 I/O 是最重要的部分之一。QFile提供了从文件中读取和写入数据的能力。Qt5 新加入的QFileDevice类,则将这部分公共操作放到了这个单独的类中。
我们通常会将文件路径作为参数传给QFile的构造函数。不过也可以在创建好对象最后,使用setFileName()来修改。QFile需要使用 / 作为文件分隔符,不过,它会自动将其转换成操作系统所需要的形式。
QFile主要提供了有关文件的各种操作,比如打开文件、关闭文件、刷新文件等。我们可以使用QDataStream或QTextStream类来读写文件,也可以使用QIODevice类提供的read()、readLine()、readAll()以及write()这样的函数。值得注意的是,有关文件本身的信息,比如文件名、文件所在目录的名字等,则是通过QFileInfo获取,而不是自己分析文件路径字符串。
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QFile file("in.txt");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "Open file failed.";
return -1;
} else {
while (!file.atEnd()) {
qDebug() << file.readLine();
}
}
QFileInfo info(file);
qDebug() << info.isDir();
qDebug() << info.isExecutable();
qDebug() << info.baseName();
qDebug() << info.completeBaseName();
qDebug() << info.suffix();
qDebug() << info.completeSuffix();
return app.exec();
}
首先使用QFile创建了一个文件对象。这个文件名字是 in.txt。然后,我们使用open()函数打开这个文件,打开形式是只读方式,文本格式。
程序的第二部分,我们使用QFileInfo获取有关该文件的信息。QFileInfo有很多类型的函数,我们只举出一些例子。比如这里,isDir()检查该文件是否是目录;isExecutable()检查该文件是否是可执行文件等。baseName()可以直接获得文件名;suffix()则直接获取文件后缀名。我们可以由下面的示例看到,baseName()和completeBaseName(),以及suffix()和completeSuffix()的区别:
QFileInfo fi("/tmp/archive.tar.gz");
QString base = fi.baseName(); // base = "archive"
QString cbase = fi.completeBaseName(); // base = "archive.tar"
QString ext = fi.suffix(); // ext = "gz"
QString ext = fi.completeSuffix(); // ext = "tar.gz"
二进制文件
Qt还提供了更高一级的操作:用于二进制的流QDataStream和用于文本流的QTextStream。
QDataStream提供了基于QIODevice的二进制数据的序列化。数据流是一种二进制流,这种流完全不依赖于底层操作系统、CPU 或者字节顺序(大端或小端)。
QDataStream既能够存取 C++ 基本类型,如 int、char、short 等,也可以存取复杂的数据类型,例如自定义的类。实际上,QDataStream对于类的存储,是将复杂的类分割为很多基本单元实现的。
结合QIODevice,QDataStream可以很方便地对文件、网络套接字等进行读写操作。
QFile file("file.dat");
file.open(QIODevice::WriteOnly);
QDataStream out(&file);
out << QString("the answer is");
out << (qint32)42;
为性能起见,数据只有在文件关闭时才会真正写入。因此,我们必须在最后添加一行代码:
file.close(); // 如果不想关闭文件,可以使用 file.flush();
文件读取:
QFile file("file.dat");
file.open(QIODevice::ReadOnly);
QDataStream in(&file);
QString str;
qint32 a;
in >> str >> a;
需要注意的是,必须按照写入的顺序,将数据读取出来。也就是说,程序数据写入的顺序必须预先定义好。在这个例子中,我们首先写入字符串,然后写入数字,那么就首先读出来的就是字符串,然后才是数字。顺序颠倒的话,程序行为是不确定的,严重时会直接造成程序崩溃。
文本文件读写
文本文件是一种人可读的文件。为了操作这种文件,我们需要使用QTextStream类。
QTextStream会自动将 Unicode 编码同操作系统的编码进行转换,这一操作对开发人员是透明的。它也会将换行符进行转换,同样不需要自己处理。QTextStream使用 16 位的QChar作为基础的数据存储单位,同样,它也支持 C++ 标准类型,如 int 等。实际上,这是将这种标准类型与字符串进行了相互转换。
QTextStream同QDataStream的使用基本一致,例如下面的代码将把“The answer is 42”写入到 file.txt 文件中:
QFile data("file.txt");
if (data.open(QFile::WriteOnly | QIODevice::Truncate)) {
QTextStream out(&data);
out << "The answer is " << 42;
}
open()函数中增加了QIODevice::Truncate打开方式。我们可以从下表中看到这些打开方式的区别:
枚举值 | 描述 |
---|---|
QIODevice::NotOpen | 未打开 |
QIODevice::ReadOnly | 以只读方式打开 |
QIODevice::WriteOnly | 以只写方式打开 |
QIODevice::ReadWrite | 以读写方式打开 |
QIODevice::Append | 以追加的方式打开,新增加的内容将被追加到文件末尾 |
QIODevice::Truncate | 以重写的方式打开,在写入新的数据时会将原有数据全部清除,游标设置在文件开头。 |
QIODevice::Text | 在读取时,将行结束符转换成 \n;在写入时,将行结束符转换成本地格式,例如 Win32 平台上是 \r\n |
QIODevice::Unbuffered | 忽略缓存 |
默认情况下,QTextStream的编码格式是 Unicode,如果我们需要使用另外的编码,可以使用:
stream.setCodec("UTF-8");
为方便起见,QTextStream同std::cout一样提供了很多描述符,被称为 stream manipulators。因为文本文件是供人去读的,自然需要良好的格式(相比而言,二进制文件就没有这些问题,只要数据准确就可以了)。这些描述符是一些函数的简写,我们可以从文档中找到:
描述符 | 等价于 |
---|---|
bin | setIntegerBase(2) |
oct | setIntegerBase(8) |
dec | setIntegerBase(10) |
hex | setIntegerBase(16) |
showbase | setNumberFlags(numberFlags() | ShowBase) |
forcesign | setNumberFlags(numberFlags() | ForceSign) |
forcepoint | setNumberFlags(numberFlags() | ForcePoint) |
noshowbase | setNumberFlags(numberFlags() & ~ShowBase) |
noforcesign | setNumberFlags(numberFlags() & ~ForceSign) |
noforcepoint | setNumberFlags(numberFlags() & ~ForcePoint) |
uppercasebase | setNumberFlags(numberFlags() | UppercaseBase) |
uppercasedigits | setNumberFlags(numberFlags() | UppercaseDigits) |
lowercasebase | setNumberFlags(numberFlags() & ~UppercaseBase) |
lowercasedigits | setNumberFlags(numberFlags() & ~UppercaseDigits) |
fixed | setRealNumberNotation(FixedNotation) |
scientific | setRealNumberNotation(ScientificNotation) |
left | setFieldAlignment(AlignLeft) |
right | setFieldAlignment(AlignRight) |
center | setFieldAlignment(AlignCenter) |
endl | operator<<(‘\n’)和flush() |
flush | flush() |
reset | reset() |
ws | skipWhiteSpace() |
bom | setGenerateByteOrderMark(true) |
这些描述符只是一些函数的简写。例如,我们想要输出 12345678 的二进制形式,那么可以直接使用
out << bin << 12345678;
就可以了。这等价于
out.setIntegerBase(2);
out << 12345678;
更复杂的,如果我们想要舒服 1234567890 的带有前缀、全部字母大写的十六进制格式(0xBC614E),那么只要使用
out << showbase << uppercasedigits << hex << 12345678;
即可。
不仅是QIODevice,QTextStream也可以直接把内容输出到QString。例如
QString str;
QTextStream(&str) << oct << 31 << " " << dec << 25 << endl;
这提供了一种简单的处理字符串内容的方法。
存储容器
存储容器(containers)有时候也被称为集合(collections),是能够在内存中存储其它特定类型的对象,通常是一些常用的数据结构,一般是通用模板类的形式。C++ 提供了一套完整的解决方案,作为标准模板库(Standard Template Library)的组成部分,也就是常说的 STL。
Qt 提供了另外一套基于模板的容器类。相比 STL,这些容器类通常更轻量、更安全、更容易使用。如果你对 STL 不大熟悉,或者更喜欢 Qt 风格的 API,那么你就应该选择使用这些类。当然,你也可以在 Qt 中使用 STL 容器,没有任何问题。
Qt 的容器类都不继承QObject,都提供了隐式数据共享、不可变的特性,并且为速度做了优化,具有较低的内存占用量等。另外一点比较重要的,它们是线程安全的。这些容器类是平台无关的,即不因编译器的不同而具有不同的实现;隐式数据共享,有时也被称作“写时复制(copy on write)”,这种技术允许在容器类中使用传值参数,但却不会出现额外的性能损失。
Qt 提供了顺序存储容器:QList,QLinkedList,QVector,QStack和QQueue。对于绝大多数应用程序,QList是最好的选择。虽然它是基于数组实现的列表,但它提供了快速的向前添加和向后追加的操作。如果你需要链表,可以使用QLinkedList。如果你希望所有元素占用连续地址空间,可以选择QVector。QStack和QQueue则是 LIFO 和 FIFO 的。
Qt 还提供了关联容器:QMap,QMultiMap,QHash,QMultiHash和QSet。带有“Multi”字样的容器支持在一个键上面关联多个值。“Hash”容器提供了基于散列函数的更快的查找,而非 Hash 容器则是基于二分搜索的有序集合。
另外两个特例:QCache和QContiguousCache提供了在有限缓存空间中的高效 hash 查找。
- QList< T>:这是至今为止提供的最通用的容器类。它将给定的类型 T 的对象以列表的形式进行存储,与一个整型的索引关联。QList在内部使用数组实现,同时提供基于索引的快速访问。我们可以使用 QList::append()和QList::prepend()在列表尾部或头部添加元素,也可以使用QList::insert()在中间插入。相比其它容器类,QList专门为这种修改操作作了优化。QStringList继承自QList。
- QLinkedList< T>:类似于 QList,除了它是使用遍历器进行遍历,而不是基于整数索引的随机访问。对于在中部插入大量数据,它的性能要优于QList。同时具有更好的遍历器语义(只要数据元素存在,QLinkedList的遍历器就会指向一个合法元素,相比而言,当插入或删除数据时,QList的遍历器就会指向一个非法值)。
- QVector< T>:用于在内存的连续区存储一系列给定类型的值。在头部或中间插入数据可能会非常慢,因为这会引起大量数据在内存中的移动。
- QStack< T>:这是QVector的子类,提供了后进先出(LIFO)语义。相比QVector,它提供了额外的函数:push(),pop()和top()。
- QQueue< T>:这是QList的子类,提供了先进先出(FIFO)语义。相比QList,它提供了额外的函数:enqueue(),dequeue()和head()。
- QSet< T>:提供单值的数学上面的集合,具有快速的查找性能。
- QMap< Key, T>:提供了字典数据结构(关联数组),将类型 T 的值同类型 Key 的键关联起来。通常,每个键与一个值关联。QMap以键的顺序存储数据;如果顺序无关,QHash提供了更好的性能。
- QMultiMap< Key, T>:这是QMap的子类,提供了多值映射:一个键可以与多个值关联。
- QHash< Key, T>:该类同QMap的接口几乎相同,但是提供了更快的查找。QHash以字母顺序存储数据。
- QMultiHash< Key, T>:这是QHash的子类,提供了多值散列。
所有的容器都可以嵌套。例如,QMap< QString, QList< int> >是一个映射,其键是QString类型,值是QList< int>类型,也就是说,每个值都可以存储多个 int。这里需要注意的是,C++ 编译器会将连续的两个 > 当做输入重定向运算符,因此,这里的两个 > 中间必须有一个空格。
能够存储在容器中的数据必须是可赋值数据类型。所谓可赋值数据类型,是指具有默认构造函数、拷贝构造函数和赋值运算符的类型。绝大多数数据类型,包括基本类型,比如 int 和 double,指针,Qt 数据类型,例如QString、QDate和QTime,都是可赋值数据类型。但是,QObject及其子类(QWidget、QTimer等)都不是。也就是说,你不能使用QList< QWidget>这种容器,因为QWidget的拷贝构造函数和赋值运算符不可用。如果你需要这种类型的容器,只能存储其指针,也就是QList< QWidget *>。
根据数据结构的相关内容,我们有必要对这些容器类的算法复杂性进行定量分析。算法复杂度关心的是在数据量增长时,容器的每一个函数究竟有多快(或者多慢)。
常量时间:O(1)。如果一个函数的运行时间与容器中数据量无关,我们说这个函数是常量时间的。QLinkedList::insert()就是常量时间的。
- 对数时间:O(log n)。如果一个函数的运行时间是容器数据量的对数关系,我们说这个函数是对数时间的。qBinaryFind()就是对数时间的。
- 线性时间:O(n)。如果一个函数的运行时间是容器数据量的线性关系,也就是说直接与数量相关,我们说这个函数是限行时间的。QVector::insert()就是线性时间的。
- 线性对数时间:O(n log n)。线性对数时间要比线性时间慢,但是要比平方时间快。
- 平方时间:O(n2)。平方时间与容器数据量的平方关系。
查找 | 插入 | 前方添加 | 后方追加 | |
---|---|---|---|---|
QLinkedList< T> | O(n) | O(1) | O(1) | O(1) |
QList< T> | O(1) | O(n) | 统计 O(1) | 统计 O(1) |
QVector< T> | O(1) | O(n) | O(n) | 统计 O(1) |
上表中,所谓“统计”,意思是统计意义上的数据。例如“统计 O(1)”是说,如果只调用一次,其运行时间是 O(n),但是如果调用多次(例如 n 次),则平均时间是 O(1)。
查找键 | 插入 | |||
---|---|---|---|---|
平均 | 最坏 | 平均 | 最坏 | |
QMap< Key, T> | O(log n) | O(log n) | O(log n) | O(log n) |
QMultiMap< Key, T> | O(log n) | O(log n) | O(log n) | O(log n) |
QHash< Key, T> | 统计 O(1) | O(n) | O(1) | 统计 O(n) |
QSet< Key, T> | 统计 O(1) | O(n) | O(1) | 统计 O(n) |
STL 风格的遍历器:
STL 风格的遍历器从 Qt 2.0 就开始提供。这种遍历器能够兼容 Qt 和 STL 的通用算法,并且为速度进行了优化。Qt 提供了两种 STL 风格的遍历器:一种是只读访问,一种是读写访问。我们推荐尽可能使用只读访问,因为它们要比读写访问的遍历器快一些。
容器 | 只读遍历器 | 读写遍历器 |
---|---|---|
QList< T>,QQueue< T> | QList< T>::const_iterator | QList< T>::iterator |
QLinkedList< T> | QLinkedList< T>::const_iterator | QLinkedList< T>::iterator |
QVector< T>,QStack< T> | QVector< T>::const_iterator | QVector< T>::iterator |
QSet< T> | QSet< T>::const_iterator | QSet< T>::iterator |
QMap< Key, T>,QMultiMap< Key, T> | QMap< Key, T>::const_iterator | QMap< Key, T>::iterator |
QHash< Key, T>,QMultiHash< Key, T> | QHash< Key, T>::const_iterator | QHash< Key, T>::iterator |
QList<QString> list;
list << "A" << "B" << "C" << "D";
QList<QString>::iterator i;
for (i = list.begin(); i != list.end(); ++i) {
*i = (*i).toLower();
}
容器的begin()函数返回指向该容器第一个元素的遍历器;end()函数返回指向该容器最后一个元素之后的元素的遍历器。end()实际是一个非法位置,永远不可达。这是为跳出循环做的一个虚元素。
QList<QString>::const_iterator i;
for (i = list.constBegin(); i != list.constEnd(); ++i) {
qDebug() << *i;
}
QMap<int, int> map;
QMap<int, int>::const_iterator i;
for (i = map.constBegin(); i != map.constEnd(); ++i) {
qDebug() << i.key() << ":" << i.value();
}
QLinkedList<QString> list;
...
QString str;
foreach (str, list) {
qDebug() << str;
}
QLinkedList<QString> list;
...
QLinkedListIterator<QString> i(list);
while (i.hasNext()) {
qDebug() << i.next();
}