11.网络(Networking)
本章的作者:jryannel
** 注意: **
最新的构建时间:2016/03/21
这章的源代码能够在assetts folder找到。
Qt 5 在其 C++ 部分提供了丰富的网络接口。例如 http 协议层上的高级类,例如提供了 QNetworkRequest、QNetworkReply 和 QNetworkAccessManager 等请求回复方式的上层便利类。但也在 TCP/IP 或 UDP协议层(如QTcpSocket,QTcpServer和QUdpSocket)上提供了较低级别的类。另外,也存在用于管理代理,网络缓存以及系统网络配置的其他类。
本章不会讲解关于 C++ 部分的网络知识,本章是关于 Qt Quick 和网络的。那么如何将 QML/JS 用户界面直接连接到网络服务,或者如何通过网络服务来为我的用户界面提供服务。有很好的书籍和参考资料讲解 Qt/C++ 的网络部分。那么这只是一个阅读有关 C++ 集成的章节,以提供一个集成层来将我们的数据提供给 Qt Quick 部分。
11.1 通过 HTTP 为用户界面提供服务
要通过 HTTP 加载一个简单的用户界面,我们需要一个 web 服务器,它为 UI 文档提供服务。我们开始使用我们自己的简单的 web 服务器,使用一个 python 单线程。但首先我们需要有我们的演示用户界面。为此,我们在项目文件夹中创建一个小的 main.qml 文件,并在其中创建一个红色矩形。
// main.qml
import QtQuick 2.5
Rectangle {
width: 320
height: 320
color: '#ff0000'
}
为了提供这个文件,我们推出了一个小的 python 脚本:
$ cd
# python -m SimpleHTTPServer 8080
现在我们的文件应该通过 http://localhost:8080/main.qml 可以访问。我们可以通过以下方式测试:
curl http://localhost:8080/main.qml
或者将浏览器指向位置。我们的浏览器不了解 QML,无法通过文档进行呈现。我们需要为 QML 文档创建一个能够解析 QML 的浏览器。为了呈现文档,我们需要指出我们的 qmlscene 的位置。不幸的是,qmlscene 仅限于解析本地文件。我们可以通过编写我们自己的 qmlscene 替换原有的 qmlscene 来克服这个限制,或者使用 QML 动态加载它。我们选择动态加载,因为它工作正常。为此,我们使用一个加载器元素为我们检索远程文档。
// remote.qml
import QtQuick 2.5
Loader {
id: root
source: 'http://localhost:8080/main2.qml'
onLoaded: {
root.width = item.width
root.height = item.height
}
}
现在我们可以要求 qmlscene 加载本地的 remote.qml 从而实现加载远程文件。还有一个问题 —— 加载程序将调整到加载项目的大小。而我们的 qmlscene 也需要适应这种尺寸。这可以使用 qmlscene 的 --resize-to-root 选项来实现:
$ qmlscene --resize-to-root remote.qml
调整到根的大小告诉 qml 场景将其窗口的大小调整为根元素的大小。远程目前正在从本地服务器加载 main.qml,并将其自身调整为加载的用户界面。这很优雅和简单。
** 注意: **
如果我们不想运行本地服务器,还可以使用 GitHub 的 gist 服务。Gist 是像 PasteBin 和其他的在线服务的剪贴板。它可以在 https://gist.github.com 下找到。 我(原作者)为这个例子创建了 https://gist.github.com/jryannel/7983492 下的一个小小的要点。这将显示一个绿色矩形。由于主要网址将网站提供为 HTML 代码,我们需要将 /raw 附加到网址以检索原始文件而不是 HTML 代码。
// remote.qml
import QtQuick 2.5
Loader {
id: root
source: 'https://gist.github.com/jryannel/7983492/raw'
onLoaded: {
root.width = item.width
root.height = item.height
}
}
要通过网络加载另一个文件,我们只需要引用组件名称。例如,Button.qml 可以正常访问,只要它在同一个远程文件夹中。
11.1.1 网络组件
让我们创建一个小实验。我们添加到我们的远程端一个小按钮作为可重复使用的组件。
- src/main.qml
- src/Button.qml
我们修改我们的 main.qml 来使用该按钮并保存为 main2.qml:
import QtQuick 2.5
Rectangle {
width: 320
height: 320
color: '#ff0000'
Button {
anchors.centerIn: parent
text: 'Click Me'
onClicked: Qt.quit()
}
}
再次启动我们的网络服务器:
$ cd src
# python -m SimpleHTTPServer 8080
我们的远程加载程序通过 http 重新加载主要的 QML:
$ qmlscene --resize-to-root remote.qml
我们看到的是一个错误:
http://localhost:8080/main2.qml:11:5: Button is not a type
所以 QML 在远程加载时无法解析按钮组件。如果代码将在本地 qmlscene src/main.qml 这将是没有问题的。本地 Qt 可以解析目录并检测哪些组件可用,但远程地,http 没有 “list-dir” 功能。我们可以强制 QML 使用 main.qml 中的 import 语句加载元素:
import "http://localhost:8080" as Remote
...
Remote.Button { ... }
当 qmlscene 再次运行时,这将可以正常工作:
$ qmlscene --resize-to-root remote.qml
这里完整的代码:
// main2.qml
import QtQuick 2.5
import "http://localhost:8080" 1.0 as Remote
Rectangle {
width: 320
height: 320
color: '#ff0000'
Remote.Button {
anchors.centerIn: parent
text: 'Click Me'
onClicked: Qt.quit()
}
}
更好的选择是使用服务器端的 qmldir 文件来控制导出。
// qmldir
Button 1.0 Button.qml
然后更新 main.qml:
import "http://localhost:8080" 1.0 as Remote
...
Remote.Button { ... }
** 注意: **
当使用本地文件系统中的组件时,将立即创建它们,而不会有延迟。当通过网络加载组件时,它们将异步创建。这具有这样的问题:创建的时间是未知的,并且当其他元素已经加载完成时有些元素可能尚未被完全加载。在使用通过网络加载的组件时需要考虑到这一点。
11.2 模板
当使用 HTML 项目时,通常需要使用模板驱动开发。服务器使用模板机制生成代码在服务器端对一个 HTML 根进行扩展。例如一个照片列表的列表头将使用 HTML 编码,动态图片链表将会使用模板机制动态生成。一般来说,这也可以使用 QML 来完成,但有一些问题。
首先它是没有必要的。HTML 开发人员这样做的原因是克服对 HTML 后端的限制。在 HTML 中没有组件模型,因此动态方面必须使用这些机制来替代,或者在客户端使用程序化的 JavaScript。许多 JS 框架(jQuery、dojo、backbone、angular、...)都用来解决这个问题,并将更多的逻辑放在客户端浏览器中以与网络服务连接。然后,客户端将仅使用 Web 服务 API(例如,提供 JSON 或 XML 数据)来与服务器进行通信。这似乎也是 QML 更好的方法。
第二个问题是 QML 的组件缓存。当 QML 访问组件时,它将缓存渲染树,并加载缓存版本进行渲染。在重新启动客户端之前,将无法检测到磁盘或远程的修改版本。为了克服这个问题,我们可以使用一个技巧。我们可以使用 URL 片段来加载网址(例如 http://localhost:8080/main.qml#1234),其中 '#1234' 是片段。HTTP 服务器始终保持相同的文档,但 QML 将使用完整的 URL(包括片段)存储此文档。每次我们访问此 URL 时,片段都需要更改,并且 QML 缓存不会得到这个信息。片段可以是例如当前时间(毫秒)或随机数。
Loader {
source: 'http://localhost:8080/main.qml#' + new Date().getTime()
}
总而言之,模板是可能的,但不是很推荐的,并没有发挥 QML 的优势。更好的方法是使用提供 JSON 或 XML 数据的 Web 服务器。
11.3 HTTP 请求
Qt 中的 http 请求通常使用 QNetworkRequest 和 QNetworkReply 从 C++ 代码中完成,然后响应将使用 Qt/C++ 集成推送数据到 QML 代码中。所以我们试图把这个信封放在这里,使用 Qt Quick 提供的当前工具让我们与一个网络端点进行通信。为此,我们使用一个帮助对象来发出 http 请求,响应周期。它以 java 脚本 XMLHttpRequest 对象的形式出现。
XMLHttpRequest 对象允许用户注册一个响应句柄函数和一个 url。可以使用 http 动词之一(get,post,put,delete,...)发送请求。当响应到达时,调用 handle 函数。句柄函数被调用多次。每次请求状态已更改(例如标题已到达或请求完成)。
这里有一个简短的例子:
function request() {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
print('HEADERS_RECEIVED');
} else if(xhr.readyState === XMLHttpRequest.DONE) {
print('DONE');
}
}
xhr.open("GET", "http://example.com");
xhr.send();
}
对于响应,我们可以获取 XML 格式或只是原始文本。可以对结果 XML 进行迭代,但更常用的是 JSON 格式响应的原始文本。JSON 文档将用于使用 JSON.parse(text) 将文本转换为 JS 对象。
...
} else if(xhr.readyState === XMLHttpRequest.DONE) {
var object = JSON.parse(xhr.responseText.toString());
print(JSON.stringify(object, null, 2));
}
在响应处理程序中,我们访问原始响应文本并将其转换为 JavaScript 对象。这个 JSON 对象现在是一个有效的 JS 对象(在javascript中,对象可以是对象或数组)。
** 注意: **
似乎优先使用 toString() 转换使代码更加稳定。没有进行明确的转换,我有几次解析器错误。不知道是什么原因。
11.3.1 Flickr 调用
让我们来看看一个更真实的世界的例子。一个典型的例子是使用 Flickr 服务来检索新上传图片的公共 Feed。为此,我们可以使用 http://api.flickr.com/services/feeds/photos_public.gne 网址。不幸的是,它默认返回一个 XML 流,这可以很容易地被 qml 中的 XmlListModel 解析。为了实例,我们想集中注意力在 JSON 数据上。为了获得一个干净的 JSON 响应,我们需要为请求附加一些参数:http://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1。这将返回没有 JSON 回调的 JSON 响应。
** 注意: **
JSON 回调将 JSON 响应包装到函数调用中。这是用于 HTML 编程的快捷方式,其中使用脚本标记来生成 JSON 请求。响应将触发由回调定义的本地函数。在 QML 中没有使用 JSON 回调的机制。
让我们先来看看使用 curl 的回应:
curl "http://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=munich"
响应将是类似下面这样的:
{
"title": "Recent Uploads tagged munich",
...
"items": [
{
"title": "Candle lit dinner in Munich",
"media": {"m":"http://farm8.staticflickr.com/7313/11444882743_2f5f87169f_m.jpg"},
...
},{
"title": "Munich after sunset: a train full of \"must haves\" =",
"media": {"m":"http://farm8.staticflickr.com/7394/11443414206_a462c80e83_m.jpg"},
...
}
]
...
}
返回的 JSON 文档具有定义好的结构。具有标题和项目属性的对象。标题是字符串,而项目是一组对象。将此文本转换为 JSON 文档时,我们可以访问各个条目,因为它是有效的 JS 对象/数组结构。
// JS code
obj = JSON.parse(response);
print(obj.title) // => "Recent Uploads tagged munich"
for(var i=0; i
作为有效的 JS 数组,我们可以使用 obj.items 数组作为列表视图的模型。我们将尽力实现这一点。首先,我们需要检索响应并将其转换为有效的 JS 对象。 然后我们可以将 response.items 属性设置为列表视图的模型。
function request() {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(...) {
...
} else if(xhr.readyState === XMLHttpRequest.DONE) {
var response = JSON.parse(xhr.responseText.toString());
// set JS object as model for listview
view.model = response.items;
}
}
xhr.open("GET", "http://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=munich");
xhr.send();
}
这是完整的源代码,我们创建请求时,加载组件。然后,请求响应用作我们的简单列表视图的模型。
import QtQuick 2.5
Rectangle {
width: 320
height: 480
ListView {
id: view
anchors.fill: parent
delegate: Thumbnail {
width: view.width
text: modelData.title
iconSource: modelData.media.m
}
}
function request() {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
print('HEADERS_RECEIVED')
} else if(xhr.readyState === XMLHttpRequest.DONE) {
print('DONE')
var json = JSON.parse(xhr.responseText.toString())
view.model = json.items
}
}
xhr.open("GET", "http://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=munich");
xhr.send();
}
Component.onCompleted: {
request()
}
}
当文档完全加载(Component.onCompleted)时,我们从 Flickr 请求最新的 Feed 内容。在到达时,我们解析 JSON 响应,并将 items 数组设置为我们视图的模型。列表视图具有一个代理,它在一行中显示缩略图图标和标题文本。
另一个选择是拥有占位符 ListModel 并将每个项目附加到列表模型上。为了支持更大的模型,需要支持分页(例如第1页,共10页)和懒惰内容检索(lazy content retrieval)。
11.4 本地文件
也可以使用 XMLHttpRequest 加载本地(XML / JSON)文件。例如,可以使用以下命令加载名为 “colors.json” 的本地文件:
xhr.open("GET", "colors.json");
我们使用它来读取颜色表并将其显示为网格。不能从 Qt Quick 侧修改文件。要将数据存储回源,我们需要一个基于 REST 的小型 HTTP 服务器或本地 Qt Quick 扩展来进行文件访问。
import QtQuick 2.5
Rectangle {
width: 360
height: 360
color: '#000'
GridView {
id: view
anchors.fill: parent
cellWidth: width/4
cellHeight: cellWidth
delegate: Rectangle {
width: view.cellWidth
height: view.cellHeight
color: modelData.value
}
}
function request() {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
print('HEADERS_RECEIVED')
} else if(xhr.readyState === XMLHttpRequest.DONE) {
print('DONE');
var obj = JSON.parse(xhr.responseText.toString());
view.model = obj.colors
}
}
xhr.open("GET", "colors.json");
xhr.send();
}
Component.onCompleted: {
request()
}
}
不使用 XMLHttpRequest 也可以使用 XmlListModel 来访问本地文件的。
import QtQuick.XmlListModel 2.0
XmlListModel {
source: "http://localhost:8080/colors.xml"
query: "/colors"
XmlRole { name: 'color'; query: 'name/string()' }
XmlRole { name: 'value'; query: 'value/string()' }
}
使用 XmlListModel,只能读取 XML 文件而不是 JSON 文件。