美文网首页Qt QML 杂记QML
QML Book 第十一章 网络 1

QML Book 第十一章 网络 1

作者: 赵者也 | 来源:发表于2017-07-14 09:49 被阅读63次

    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 文件。

    相关文章

      网友评论

        本文标题:QML Book 第十一章 网络 1

        本文链接:https://www.haomeiwen.com/subject/ykeacxtx.html