Qt5 学习7 之 Graphics View Framework、存储容器、遍历容器

Graphics View Framework

Graphics View 提供了一种接口,用于管理大量自定义的 2D 图形元素,并与之进行交互;还提供了用于将这些元素进行可视化显示的观察组件,并支持缩放和旋转。我们通常所说的 Linux 的 KDE 桌面环境,就是建立在 Graphics View 基础之上的(尽管新版本的 KDE 有向 QML 迁移的趋势)。

Graphics View 框架包含了一套完整的事件体系,可以用于与场景中的元素进行双精度的交互。这些元素同样支持键盘事件、鼠标事件等。Graphics View 使用了 BSP 树(Binary Space Partitioning tree,这是一种被广泛应用于图形学方面的数据结构)来提供非常快速的元素发现,也正因为如此,才能够实现一种上百万数量级元素的实时显示机制。

Graphics View 最初在 Qt 4.2 引入,来取代 Qt 3 中的 QCanvas。当然,在最新的 Qt5 中,Qt3 的代码已经不能继续使用了(尽管在一定程度上, Qt4 还是可以使用这些遗留代码)。

Graphics View 是一个基于元素(item)的 MV 架构的框架。它可以分成三个部分:元素 item、场景 scene 和视图 view。

基于元素的意思是,它的每一个组件都是一个独立的元素。这是与我们之前讲到过的QPainter状态机机制不同。回忆一下,使用QPainter绘图,大多是采用一种面向过程的描述方式:首先使用drawLine()画一条直线,然后使用drawPolygon()画一个多边形。对于 Graphics View,相同的过程可以是,首先创建一个场景(scene),然后创建一个直线对象和一个多边形对象,再使用场景的add()函数,将直线和多边形添加到场景中,最后通过视图进行观察,就可以看到了。乍看起来,后者似乎更加复杂,但是,如果你的图像中包含了成千上万的直线、多边形之类,管理这些对象要比管理QPainter的绘制语句容易得多。并且,这些图形对象也更加符合面向对象的设计要求:一个很复杂的图形可以很方便的复用。

MV 架构的意思是,Graphics View 提供一个 model 和一个 view(正如 MVC 架构,只不过 MV 架构少了 C 这么一个组件)。所谓模型(model)就是我们添加的种种对象;所谓视图(view)就是我们观察这些对象的视口。同一个模型可以由很多视图从不同的角度进行观察,这是很常见的需求。使用 QPainter 很难实现这一点,这需要很复杂的计算,而 Graphics View 可以很容易的实现。

Graphics View 提供了QGraphicsScene作为场景,即是允许我们添加图形的空间,相当于整个世界;QGraphicsView作为视口,也就是我们的观察窗口,相当于照相机的取景框,这个取景框可以覆盖整个场景,也可以是场景的一部分;QGraphicsItem作为图形元件,以便添加到场景中去,Qt 内置了很多图形,比如直线、多边形等,它们都是继承自QGraphicsItem。

下面我们通过一段代码看看 Graphics View 的使用。

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)。可以想象,这是一个边长 150px 的正方形的对角线。通过这两步,我们已经有了场景和元素。之后,我们创建一个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()一行,是因为我们让系统去决定视图的最小尺寸(否则的话,我们需要手动将窗口标题栏等的大小同时考虑设置)。

存储容器

存储容器(containers)有时候也被称为集合(collections),是能够在内存中存储其它特定类型的对象,通常是一些常用的数据结构,一般是通用模板类的形式。C++ 提供了一套完整的解决方案,作为标准模板库(Standard Template Library)的组成部分,也就是常说的 STL。

Qt 提供了另外一套基于模板的容器类。相比 STL,这些容器类通常更轻量、更安全、更容易使用。如果你对 STL 不大熟悉,或者更喜欢 Qt 风格的 API,那么你就应该选择使用这些类。当然,你也可以在 Qt 中使用 STL 容器,没有任何问题。

本章的目的,是让你能够选择使用哪个容器,而不是告诉你这个类都哪些函数。这个问题可以在文档中找到更清晰的回答。

Qt 的容器类都不继承QObject,都提供了隐式数据共享、不可变的特性,并且为速度做了优化,具有较低的内存占用量等。另外一点比较重要的,它们是线程安全的。这些容器类是平台无关的,即不因编译器的不同而具有不同的实现;隐式数据共享,有时也被称作“写时复制(copy on write)”,这种技术允许在容器类中使用传值参数,但却不会出现额外的性能损失。遍历是容器类的重要操作。Qt 容器类提供了类似 Java 的遍历器语法,同样也提供了类似 STL 的遍历器语法,以方便用户选择自己习惯的编码方式。相比而言,Java 风格的遍历器更易用,是一种高层次的函数;而 STL 风格的遍历器更高效,同时能够支持 Qt 和 STL 的通用算法。最后一点,在一些嵌入式平台,STL 往往是不可用的,这时你就只能使用 Qt 提供的容器类,除非你想自己创建。顺便提一句,除了遍历器,Qt 还提供了自己的 foreach 语法(C++ 11 也提供了类似的语法,但有所区别,详见这里的 foreach 循环一节)。

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 查找。

我们将 Qt 提供的各个容器类总结如下:

QList:这是至今为止提供的最通用的容器类。它将给定的类型 T 的对象以列表的形式进行存储,与一个整型的索引关联。QList在内部使用数组实现,同时提供基于索引的快速访问。我们可以使用 QList::append()和QList::prepend()在列表尾部或头部添加元素,也可以使用QList::insert()在中间插入。相比其它容器类,QList专门为这种修改操作作了优化。QStringList继承自QList。
QLinkedList:类似于 QList,除了它是使用遍历器进行遍历,而不是基于整数索引的随机访问。对于在中部插入大量数据,它的性能要优于QList。同时具有更好的遍历器语义(只要数据元素存在,QLinkedList的遍历器就会指向一个合法元素,相比而言,当插入或删除数据时,QList的遍历器就会指向一个非法值)。
QVector:用于在内存的连续区存储一系列给定类型的值。在头部或中间插入数据可能会非常慢,因为这会引起大量数据在内存中的移动。
QStack:这是QVector的子类,提供了后进先出(LIFO)语义。相比QVector,它提供了额外的函数:push(),pop()和top()。
QQueue:这是QList的子类,提供了先进先出(FIFO)语义。相比QList,它提供了额外的函数:enqueue(),dequeue()和head()。
QSet:提供单值的数学上面的集合,具有快速的查找性能。
QMap<Key, T>:提供了字典数据结构(关联数组),将类型 T 的值同类型 Key 的键关联起来。通常,每个键与一个值关联。QMap以键的顺序存储数据;如果顺序无关,QHash提供了更好的性能。
QMultiMap<Key, T>:这是QMap的子类,提供了多值映射:一个键可以与多个值关联。
QHash<Key, T>:该类同QMap的接口几乎相同,但是提供了更快的查找。QHash以字母顺序存储数据。
QMultiHash<Key, T>:这是QHash的子类,提供了多值散列。

所有的容器都可以嵌套。例如,QMap<QString, QList >是一个映射,其键是QString类型,值是QList类型,也就是说,每个值都可以存储多个 int。这里需要注意的是,C++ 编译器会将连续的两个 > 当做输入重定向运算符,因此,这里的两个 > 中间必须有一个空格。

能够存储在容器中的数据必须是可赋值数据类型。所谓可赋值数据类型,是指具有默认构造函数、拷贝构造函数和赋值运算符的类型。绝大多数数据类型,包括基本类型,比如 int 和 double,指针,Qt 数据类型,例如QString、QDate和QTime,都是可赋值数据类型。但是,QObject及其子类(QWidget、QTimer等)都不是。也就是说,你不能使用QList这种容器,因为QWidget的拷贝构造函数和赋值运算符不可用。如果你需要这种类型的容器,只能存储其指针,也就是QList<QWidget *>。

如果要使用QMap或者QHash,作为键的类型必须提供额外的辅助函数。QMap的键必须提供operator<()重载,QHash的键必须提供operator==()重载和一个名字是qHash()的全局函数。

作为例子,我们考虑如下的代码:

struct Movie
{
    int id;
    QString title;
    QDate releaseDate;
};

作为 struct,我们当做纯数据类使用。这个类没有额外的构造函数,因此编译器会为我们生成一个默认构造函数。同时,编译器还会生成默认的拷贝构造函数和赋值运算符。这就满足了将其放入容器类存储的条件:

QList<Movie> movs;

Qt 容器类可以直接使用QDataStream进行存取。此时,容器中所存储的类型必须也能够使用QDataStream进行存储。这意味着,我们需要重载operator<<()和operator>>()运算符:

QDataStream &operator<<(QDataStream &out, const Movie &movie)
{
    out << (quint32)movie.id << movie.title
        << movie.releaseDate;
    return out;
}

QDataStream &operator>>(QDataStream &in, Movie &movie)
{
    quint32 id;
    QDate date;

    in >> id >> movie.title >> date;
    movie.id = (int)id;
    movie.releaseDate = date;
    return in;
}

QString和QByteArray提供了一系列函数,用于检测和指定究竟要分配多少内存:

capacity():返回实际已经分配内存的元素数目(对于QHash和QSet,则是散列表中桶的个数)
reserve(size):为指定数目的元素显式地预分配内存。
squeeze():释放那些不需要真实存储数据的内存空间。

如果你知道容器大约有多少数据,那么你可以通过调用reserve()函数来减少内存占用。如果已经将所有数据全部存入容器,则可以调用squeeze()函数,释放所有未使用的预分配空间。

遍历容器

Java 风格的遍历器

Java 风格的遍历器是在 Qt4 首先引入的,是 Qt 应用程序首先推荐使用的形式。这种风格比起 STL 风格的遍历器更方便。方便的代价就是不如后者高效。它们的 API 非常类似于 Java 的遍历器类,故名。

每一种容器都有两种 Java 风格的遍历器:一种提供只读访问,一种提供读写访问:

这里我们只讨论QList和QMap的遍历器。QLinkedList、QVector和QSet的遍历器接口与QList的是一样的;QHash遍历器的接口则同QMap是一样的。

不同于下面我们将要介绍的 STL 风格的遍历器,Java 风格的遍历器指向的是两个元素之间的位置,而不是指向元素本身。因此,它们可能会指向集合第一个元素之前的位置,也可能指向集合的最后一个元素之后的位置,如下图所示:
在这里插入图片描述

QList<QString> list;
list << "A" << "B" << "C" << "D";

QListIterator<QString> i(list);
while (i.hasNext()) {
    qDebug() << i.next();
}

首先,我们使用 list 对象创建一个遍历器。刚刚创建完成时,该遍历器位于第一个元素之前(也就是 A 之前)。我们通过调用hasNext()函数判断遍历器之后的位置上有无元素。如果有,调用next()函数将遍历器跳过其后的元素。next()函数返回刚刚跳过的元素。当然,我们也可以使用hasPrevious()和previous()函数来从尾部开始遍历,详细内容可以参考 API 文档。

QListIterator是只读遍历器,不能插入或者删除数据。如果需要这些操作,我们可以使用QMutableListIterator。来看下面的代码:

QMutableListIterator<int> i(list);
while (i.hasNext()) {
    if (i.next() % 2 != 0) {
        i.remove();
    }
}

这段代码使用QMutableListIterator遍历集合,如果其值是奇数则将其删除。在每次循环中都要调用next()函数。正如前面所说,它会跳过其后的一个元素。remove()函数会删除我们刚刚跳过的元素。调用remove()函数并不会将遍历器置位不可用,因此我们可以连续调用这个函数。向前遍历也是类似的,这里不再赘述。

如果我们需要修改已经存在的元素,使用setValue()函数。例如:

QMutableListIterator<int> i(list);
while (i.hasNext()) {
    if (i.next() > 128) {
        i.setValue(128);
    }
}

如同remove()函数,setValue()也是对刚刚跳过的元素进行操作。实际上,next()函数返回的是集合元素的非 const 引用,因此我们根本不需要调用setValue()函数:

QMutableListIterator<int> i(list);
while (i.hasNext()) {
    i.next() *= 2;
}

QMapItrator也是类似的。例如,使用QMapItrator我们可以将数据从QMap复制到QHash

QMap<int, QWidget *> map;
QHash<int, QWidget *> hash;

QMapIterator<int, QWidget *> i(map);
while (i.hasNext()) {
    i.next();
    hash.insert(i.key(), i.value());
}

STL 风格的遍历器

STL 风格的遍历器从 Qt 2.0 就开始提供。这种遍历器能够兼容 Qt 和 STL 的通用算法,并且为速度进行了优化。同 Java 风格遍历器类似,Qt 也提供了两种 STL 风格的遍历器:一种是只读访问,一种是读写访问。我们推荐尽可能使用只读访问,因为它们要比读写访问的遍历器快一些。

STL 风格的遍历器具有类似数组指针的行为。例如,我们可以使用 ++ 运算符让遍历器移动到下一个元素,使用 * 运算符获取遍历器所指的元素。对于QVector和QStack,虽然它们是在连续内存区存储元素,遍历器类型是typedef T *,const_iterator类型则是typedef const T *。

我们还是以QList和QMap为例,理由如上。下面是有关QList的相关代码:

QList<QString> list;
list << "A" << "B" << "C" << "D";

QList<QString>::iterator i;
for (i = list.begin(); i != list.end(); ++i) {
    *i = (*i).toLower();
}

不同于 Java 风格遍历器,STL 风格遍历器直接指向元素本身。容器的begin()函数返回指向该容器第一个元素的遍历器;end()函数返回指向该容器最后一个元素之后的元素的遍历器。end()实际是一个非法位置,永远不可达。这是为跳出循环做的一个虚元素。如果集合是空的,begin()等于end(),我们就不能执行循环。

下图是 STL 风格遍历器的示意图:
在这里插入图片描述
我们使用const_iterator进行只读访问,例如:

QList<QString>::const_iterator i;
for (i = list.constBegin(); i != list.constEnd(); ++i) {
    qDebug() << *i;
}

QMap和QHash的遍历器,* 运算符返回集合键值对。下面的代码,我们打印出QMap的所有元素:

QMap<int, int> map;

QMap<int, int>::const_iterator i;
for (i = map.constBegin(); i != map.constEnd(); ++i) {
    qDebug() << i.key() << ":" << i.value();
}

由于有隐式数据共享(我们会在后面的章节介绍该部分内容),即使一个函数返回集合中元素的值也不会有很大的代价。Qt API 包含了很多以值的形式返回QList或QStringList的函数(例如QSplitter::sizes())。如果你希望使用 STL 风格的遍历器遍历这样的元素,应该使用遍历器遍历容器的拷贝,例如:

// 正确的方式
const QList<QString> sizes = splitter->sizes();
QList<QString>::const_iterator i;
for (i = sizes.begin(); i != sizes.end(); ++i)
    ...

// 错误的方式
QList<QString>::const_iterator i;
for (i = splitter->sizes().begin();
     i != splitter->sizes().end(); ++i)
    ...

对于那些返回集合的 const 或非 const 引用的函数,就不存在这个问题。

另外,隐式数据共享对 STL 风格遍历器造成的另一个影响是,当一个容器正在被一个遍历器遍历的时候,不能对这个容器进行拷贝。如果你必须对其进行拷贝,那么就得万分小心。例如,

QVector<int> a, b;
a.resize(100000); // 使用 0 填充一个非常大的 vector

QVector<int>::iterator i = a.begin();
// 使用遍历器 i 的错误方式(注意,此时,a 上面已经有一个正在遍历的遍历器):
b = a;
/*
    现在,我们的万分小心遍历器 i。因为它指向了共享的数据。
    如果我们执行语句 *i = 4,我们就会改变了共享的数据实例(两个 vector 都会被改变)。
    这里的行为与 STL 容器不同,因此这种问题仅出现在 Qt 中;使用 STL 标准容器不存在这个问题。
*/

a[0] = 5;
/*
    现在,容器 a 被修改了,其实际数据已经与共享数据不同,
    即使 i 就是从容器 a 创建的遍历器,但是它指向的数据与 a 并不一致,其表现就像是 b 的遍历器。
    这里的情形是:(*i) == 0.
*/

b.clear(); // 现在我们清空 b,此时,遍历器 i 已经不可用了。

int j = *i; // 无定义行为!
/*
    来自 b 的数据(也就是 i 指向的那些数据)已经被销毁了。
    这种行为在 STL 容器中是完全可行的(在 STL 容器中,(*i) == 5),
    但是使用 QVector 则很有可能出现崩溃。
*/

虽然这个例子只演示了QVector,但实际上,这个问题适用于所有隐式数据共享的容器类。
foreach关键字

如果我们仅仅想要遍历集合所有元素,我们可以使用 Qt 的foreach关键字。这个关键字是 Qt 特有的,通过预处理器进行处理。C++ 11 也提供了自己的foreach关键字,不过与此还是有区别的。

foreach的语法是foreach (variable, container)。例如,我们使用foreach对QLinkedList进行遍历

QLinkedList<QString> list;
...
QString str;
foreach (str, list) {
    qDebug() << str;
}

如果类型名中带有逗号,比如QPair<int, int>,我们只能像上面一样,先创建一个对象,然后使用foreach关键字。如果没有逗号,则可以直接在foreach关键字中使用新的对象,例如:

QLinkedList<QString> list;
...
foreach (const QString &str, list) {
    qDebug() << str;
}

Qt 会在foreach循环时自动拷贝容器。这意味着,如果在遍历时修改集合,对于正在进行的遍历是没有影响的。即使不修改容器,拷贝也是会发生的。但是由于存在隐式数据共享,这种拷贝还是非常迅速的。

因为foreach创建了集合的拷贝,使用集合的非 const 引用也不能实际修改原始集合,所修改的只是这个拷贝。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值