QML Book 第十一章 网络 1

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 <PROJECT>
# 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<obj.items.length; i++) {
    // iterate of the items array entries
    print(obj.items[i].title) // title of picture
    print(obj.items[i].media.m) // url of thumbnail
}

作为有效的 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 文件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,563评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,268评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,982评论 4 60
  • 竹子,用了五年的时间潜伏在地底,第六年借着春风和雨水开始疯长,直至长成参天的模样才开始罢休。 而银杏的寿命极长,到...
    鱼子仙人阅读 1,456评论 2 2
  • 待定
    晓冰阅读 115评论 0 1