《Qt 学习之路 2》原文地址
Qt跨平台策略
- GUI 模拟:任何平台都提供了图形绘制函数,例如画点、画线、画面等。有些工具库利用这些基本函数,在不同绘制出自己的组件,这就是 GUI 模拟。GUI 模拟的工作量无疑是很大的,因为需要使用最基本的绘图函数将所有组件画出来;并且这种绘制很难保证和原生组件一模一样。但是,这一代价带来的优势是,可以很方便的修改组件的外观——只要修改组件绘制函数即可。很多跨平台的 GUI 库都是使用的这种策略,例如 gtk+(这是一个 C 语言的图形界面库。使用 C 语言很优雅地实现了面向对象程序设计。不过,这也同样带来了一个问题——使用大量的类型转换的宏来模拟多态,并且它的函数名一般都比较长,使用下划线分割单词,看上去和 Linux 如出一辙。gtk+ 并不是模拟的原生界面,而有它自己的风格,所以有时候就会和操作系统的界面格格不入。),Swing 以及我们的 Qt。
信号槽
- 信号槽要求信号和槽的参数一致,所谓一致,是参数类型一致。如果不一致,允许的情况是,槽函数的参数可以比信号的少,即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。这是因为,你可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数比信号的少),但是不能说信号根本没有这个数据,你就要在槽函数中使用(就是槽函数的参数比信号的多,这是不允许的)。
- Qt 5 引入了信号槽的新语法:使用函数指针能够获得编译期的类型检查。
- 如果你使用了 Qt5 的新语法,新语法提供了编译期检查(取函数指针),因此取 private 函数的指针是不能通过编译的。
- 在 Qt 5 中,如果你想使用 overloaded 的 signal,有两种方式可供选择:
- 使用 Qt 4 的SIGNAL和SLOT宏,因为这两个宏已经指定了参数信息,所以不存在这个问题;
- 使用函数指针显式指定使用哪一个信号。
- 自动连接(默认)意味着如果接受者所在线程就是当前线程,则使用直接连接;否则将使用队列连接。
动作
- Qt 使用QAction类作为动作。顾名思义,这个类就是代表了窗口的一个“动作”,这个动作可能显示在菜单,作为一个菜单项,当用户点击该菜单项,对用户的点击做出响应;也可能在工具栏,作为一个工具栏按钮,用户点击这个按钮就可以执行相应的操作。有一点值得注意:无论是出现在菜单栏还是工具栏,用户选择之后,所执行的动作应该都是一样的。因此,Qt 并没有专门的菜单项类,只是使用一个QAction类,抽象出公共的动作。当我们把QAction对象添加到菜单,就显示成一个菜单项,添加到工具栏,就显示成一个工具按钮。用户可以通过点击菜单项、点击工具栏按钮、点击快捷键来激活这个动作。
资源文件
- Qt 资源系统是一个跨平台的资源机制,用于将程序运行时所需要的资源以二进制的形式存储于可执行文件内部。如果你的程序需要加载特定的资源(图标、文本翻译等),那么,将其放置在资源文件中,就再也不需要担心这些文件的丢失。也就是说,如果你将资源以资源文件形式存储,它是会编译到可执行文件内部。
对象模型
- Qt 保证的是,任何对象树中的 QObject对象 delete 的时候,如果这个对象有 parent,则自动将其从 parent 的children()列表中删除;如果有孩子,则自动 delete 每一个孩子。Qt 保证没有QObject会被 delete 两次,这是由析构顺序决定的。
- 下面的代码会导致quit析构两次,程序崩溃。
#include "mainwindow.h"
#include
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QPushButton quit("Quit");
MainWindow w;
quit.setParent(&w);
w.show();
return a.exec();
}
事件
- Qt 的事件对象有两个函数:accept()和ignore()。正如它们的名字一样,前者用来告诉 Qt,这个类的事件处理函数想要处理这个事件;后者则告诉 Qt,这个类的事件处理函数不想要处理这个事件。在事件处理函数中,可以使用isAccepted()来查询这个事件是不是已经被接收了。具体来说:如果一个事件处理函数调用了一个事件对象的accept()函数,这个事件就不会被继续传播给其父组件;如果它调用了事件的ignore()函数,Qt 会从其父组件中寻找另外的接受者。
- 事实上,我们很少会使用accept()和ignore()函数,而是像上面的示例一样,如果希望忽略事件(所谓忽略,是指自己不想要这个事件),只要调用父类的响应函数即可。记得我们曾经说过,Qt 中的事件都是 protected 的,因此,重写的函数必定存在着其父类中的响应函数,所以,这个方法是可行的。为什么要这么做,而不是自己去手动调用这两个函数呢?因为我们无法确认父类中的这个处理函数有没有额外的操作。如果我们在子类中直接忽略事件,Qt 会去寻找其他的接收者,该子类的父类的操作会被忽略(因为没有调用父类的同名函数),这可能会有潜在的危险。为了避免自己去调用accept()和ignore()函数,而是尽量调用父类实现,Qt 做了特殊的设计:事件对象默认是 accept 的,而作为所有组件的父类QWidget的默认实现则是调用ignore()。这么一来,如果你自己实现事件处理函数,不调用QWidget的默认实现,你就等于是接受了事件;如果你要忽略事件,只需调用QWidget的默认实现。这一点我们前面已经说明。
- 事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。
- 现在我们可以总结一下 Qt 的事件处理,实际上是有五个层次:
- 重写paintEvent()、mousePressEvent()等事件处理函数。这是最普通、最简单的形式,同时功能也最简单。
- 重写event()函数。event()函数是所有对象的事件入口,QObject和QWidget中的实现,默认是把事件传递给特定的事件处理函数。
- 在特定对象上面安装事件过滤器。该过滤器仅过滤该对象接收到的事件。
- 在QCoreApplication::instance()上面安装事件过滤器。该过滤器将过滤所有对象的所有事件,因此和notify()函数一样强大,但是它更灵活,因为可以安装多个过滤器。全局的事件过滤器可以看到 disabled 组件上面发出的鼠标事件。全局过滤器有一个问题:只能用在主线程。
- 重写QCoreApplication::notify()函数。这是最强大的,和全局事件过滤器一样提供完全控制,并且不受线程的限制。但是全局范围内只能有一个被使用(因为QCoreApplication是单例的)。
model/view 架构
- 总的来说,model/view 架构将传统的 MV 模型分为三部分:模型、视图和委托。每一个组件都由一个抽象类定义,这个抽象类提供了基本的公共接口以及一些默认实现。模型、视图和委托则使用信号槽进行交互:
- 来自模型的信号通知视图,其底层维护的数据发生了改变;
- 来自视图的信号提供了有关用户与界面进行交互的信息;
- 来自委托的信号在用户编辑数据项时使用,用于告知模型和视图编辑器的状态。
- 所有的模型都是QAbstractItemModel的子类。这个类定义了供视图和委托访问数据的接口。模型并不存储数据本身。这意味着,你可以将数据存储在一个数据结构中、另外的类中、文件中、数据库中,或者其他你所能想到的东西中。
- Qt 还提供了一系列预定义好的视图:QListView用于显示列表,QTableView用于显示表格,QTreeView用于显示层次数据。这些类都是QAbstractItemView的子类。这意味着,如果你要创建新的视图类,则可以继承QAbstractItemView。
- QAbstractItemDelegate则是所有委托的抽象基类。自 Qt 4.4 依赖,默认的委托实现是QStyledItemDelegate。但是,QStyledItemDelegate和QItemDelegate都可以作为视图的编辑器,二者的区别在于,QStyledItemDelegate使用当前样式进行绘制。在实现自定义委托时,推荐使用QStyledItemDelegate作为基类,或者结合 Qt style sheets。
- 你觉得 model/view 模型过于复杂,或者有很多功能是用不到的,Qt 还有一系列方便使用的类。这些类都是继承自标准的视图类,并且继承了标准模型。这些类并不是为其他类继承而准备的,只是为了使用方便。它们包括QListWidget、QTreeWidget和QTableWidget。这些类远不如视图类灵活,不能使用另外的模型,因此只适用于简单的情形。
模型
- 模型使用索引来提供给视图和委托有关数据项的位置的信息,这样做的好处是,模型之外的对象无需知道底层的数据存储方式;
- 数据项通过行号、列号以及父项三个坐标进行定位;
- 模型索引由模型在其它组件(视图和委托)请求时才会被创建;
- 如果使用index()函数请求获得一个父项的可用索引,该索引会指向模型中这个父项下面的数据项。这个索引指向该项的一个子项;如果使用index()函数请求获得一个父项的不可用索引,该索引指向模型的最顶级项;
- 角色用于区分数据项的不同类型的数据。
QML 语法
- 每一个属性都可以发出信号,因而都可以关联信号处理函数。这个处理函数将在属性值变化时调用。这种值变化的信号槽命名为 on + 属性名 + Changed,其中属性名要首字母大写。
QML 基本元素
- 最基本的可视元素:Item、Rectangle、Text、Image和MouseArea。
QML 组件
- 在 main.qml 中,我们直接使用了Button这个组件,就像 QML 其它元素一样。由于 Button.qml 与 main.qml 位于同一目录下,所以不需要额外的操作。但是,如果我们将 Button.qml 放在不同目录,比如构成如下的目录结果:
app
|- QML
| |- main.qml
|- components
|- Button.qml
那么,我们就需要在 main.qml 的import部分增加一行import ../components才能够找到Button组件。
定位器
- QML 提供了很多用于定位的元素。这些元素叫做定位器,都包含在 QtQuick 模块。这些定位器主要有 Row、Column、Grid和Flow等。经常结合定位器一起使用的元素:Repeater。
元素布局
- 除了定位器,QML 还提供了另外一种用于布局的机制。我们将这种机制成为锚点(anchor)。锚点允许我们灵活地设置两个元素的相对位置。它使两个元素之间形成一种类似于锚的关系,也就是两个元素之间形成一个固定点。锚点的行为类似于一种链接,它要比单纯地计算坐标改变更强。由于锚点描述的是相对位置,所以在使用锚点时,我们必须指定两个元素,声明其中一个元素相对于另外一个元素。锚点是Item元素的基本属性之一,因而适用于所有 QML 可视元素。
输入元素
- 键盘输入的两个元素:TextInput和TextEdit。当FocusScope接收到焦点时,会将焦点转发给最后一个设置了focus:true的子对象。附加属性Keys类似于键盘事件,允许我们相应特定的按键按下事件。
Qt Quick Controls
- 自 QML 第一次发布已经过去一年多的时间,但在企业应用领域,QML 一直没有能够占据一定地位。很大一部分原因是,QML 缺少一些在企业应用中亟需的组件,比如按钮、菜单等。虽然移动领域,这些组件已经变得可有可无,但在桌面系统中依然不可或缺。为了解决这一问题,Qt 5.1 发布了 Qt Quick 的一个全新模块:Qt Quick Controls。顾名思义,这个模块提供了大量类似 Qt Widgets 模块那样可重用的组件。本章我们将介绍 Qt Quick Controls,你会发现这个模块与 Qt 组件非常类似。
Repeater
动态视图
- Repeater适用于少量的静态数据集。但是在实际应用中,数据模型往往是非常复杂的,并且数量巨大。这种情况下,Repeater并不十分适合。于是,QtQuick 提供了两个专门的视图元素:ListView和GridView。这两个元素都继承自Flickable,因此允许用户在一个很大的数据集中进行移动。同时,ListView和GridView能够复用创建的代理,这意味着,ListView和GridView不需要为每一个数据创建一个单独的代理。这种技术减少了大量代理的创建造成的内存问题。
视图代理
- 每一个代理都可以访问一系列属性和附加属性。这些属性及附加属性中,有些来自于数据模型,有些则来自于视图。前者为代理提供了每一个数据项的数据信息;后者则是有关视图的状态信息。
- 代理中最常用到的是来自于视图的附加属性ListView.isCurrentItem和ListView.view。前者是一个布尔值,用于表示代理所代表的数据项是不是视图所展示的当前数据项;后者则是一个只读属性,表示该代理所属于的视图。通过访问视图的相关数据,我们就可以创建通用的可复用的代理,用于适配视图的大小和展示特性。
模型-视图高级技术
- PathView是 QtQuick 中最强大的视图,同时也是最复杂的。PathView允许创建一种更灵活的视图。在这种视图中,数据项并不是方方正正,而是可以沿着任意路径布局。沿着同一布局路径,数据项的属性可以被更详细的设置,例如缩放、透明度等。
- 模型视图的性能很大程度上取决于创建新的代理所造成的消耗。
- 应该使每个代理中包含的 JavaScript 代码尽可能少。最好能做到在代理之外调用复杂的 JavaScript 代码。这将减少代理创建时编译 JavaScript 所消耗的时间。
Canvas
- 为了使用脚本化的绘图机制,Qt 5 引入的Canvas元素。Canvas元素提供了一种与分辨率无关的位图绘制机制。通过Canvas,你可以使用 JavaScript 代码进行绘制。
- Canvas元素的基本思想是,使用一个 2D 上下文对象渲染路径。这个 2D 上下文对象包含所必须的绘制函数,从而使Canvas元素看起来就像一个画板。这个对象支持画笔、填充、渐变、文本以及其它一系列路径创建函数。
粒子系统
- 粒子系统是一种计算机图形学的技术,用于模拟一些特定的模糊现象,这些现象用传统的渲染技术难以达到一定的真实感。虽然名为“粒子”,但却可以模拟爆炸、烟、水流、落叶、云、雾、流星尾迹或其它发光轨迹这样的抽象视觉效果。粒子系统的特色是“模糊”,其渲染效果并非完全取决于像素,而是使用特定的边界参数描述随机粒子。
QML 存储
- Qt 5.2 起,QML 引入了一个新的类Settings,顾名思义,它就是QSettings的 QML 版本。值得注意的是,直到目前最新的 Qt 5.5.1,Settings依然是试验性质 API,所以,它的 API 可能会在未来版本中有所变化。使用Settings需要添加import Qt.labs.settings 1.0语句。
- Qt Quick 借鉴了 localStorage API,提供了类似的解决方案,名字也被称为 LocalStorage。为了使用该 API,需要添加语句import QtQuick.LocalStorage 2.0。
- LocalStorage 使用 SQLite 数据库保存数据。这个数据库的文件按照给定的数据库名字和版本保存在系统的指定位置,使用唯一 ID 标识。但是,系统并不允许列出或删除已创建的数据库。可以使用 C++ 的QQmlEngine::offlineStoragePath()函数查看数据库文件存储路径。
使用 C++ 扩展 QML
- 扩展 QML 一般有三种方式:
- 上下文属性:setContextProperty()
- 向引擎注册类型:在 main.cpp 调用qmlRegisterType
- QML 扩展插件