11.網(wǎng)絡(luò)(Networking)
本章的作者:jryannel
注意:
最新的構(gòu)建時(shí)間:2016/03/21
這章的源代碼能夠在assetts folder找到。
Qt 5 在其 C++ 部分提供了豐富的網(wǎng)絡(luò)接口。例如 http 協(xié)議層上的高級類,例如提供了 QNetworkRequest、QNetworkReply 和 QNetworkAccessManager 等請求回復(fù)方式的上層便利類。但也在 TCP/IP 或 UDP協(xié)議層(如QTcpSocket,QTcpServer和QUdpSocket)上提供了較低級別的類。另外,也存在用于管理代理,網(wǎng)絡(luò)緩存以及系統(tǒng)網(wǎng)絡(luò)配置的其他類。
本章不會講解關(guān)于 C++ 部分的網(wǎng)絡(luò)知識,本章是關(guān)于 Qt Quick 和網(wǎng)絡(luò)的。那么如何將 QML/JS 用戶界面直接連接到網(wǎng)絡(luò)服務(wù),或者如何通過網(wǎng)絡(luò)服務(wù)來為我的用戶界面提供服務(wù)。有很好的書籍和參考資料講解 Qt/C++ 的網(wǎng)絡(luò)部分。那么這只是一個閱讀有關(guān) C++ 集成的章節(jié),以提供一個集成層來將我們的數(shù)據(jù)提供給 Qt Quick 部分。
11.1 通過 HTTP 為用戶界面提供服務(wù)
要通過 HTTP 加載一個簡單的用戶界面,我們需要一個 web 服務(wù)器,它為 UI 文檔提供服務(wù)。我們開始使用我們自己的簡單的 web 服務(wù)器,使用一個 python 單線程。但首先我們需要有我們的演示用戶界面。為此,我們在項(xiàng)目文件夾中創(chuàng)建一個小的 main.qml 文件,并在其中創(chuàng)建一個紅色矩形。
// main.qml
import QtQuick 2.5
Rectangle {
width: 320
height: 320
color: '#ff0000'
}
為了提供這個文件,我們推出了一個小的 python 腳本:
$ cd <PROJECT>
# python -m SimpleHTTPServer 8080
現(xiàn)在我們的文件應(yīng)該通過 http://localhost:8080/main.qml 可以訪問。我們可以通過以下方式測試:
curl http://localhost:8080/main.qml
或者將瀏覽器指向位置。我們的瀏覽器不了解 QML,無法通過文檔進(jìn)行呈現(xiàn)。我們需要為 QML 文檔創(chuàng)建一個能夠解析 QML 的瀏覽器。為了呈現(xiàn)文檔,我們需要指出我們的 qmlscene 的位置。不幸的是,qmlscene 僅限于解析本地文件。我們可以通過編寫我們自己的 qmlscene 替換原有的 qmlscene 來克服這個限制,或者使用 QML 動態(tài)加載它。我們選擇動態(tài)加載,因?yàn)樗ぷ髡?。為此,我們使用一個加載器元素為我們檢索遠(yuǎn)程文檔。
// remote.qml
import QtQuick 2.5
Loader {
id: root
source: 'http://localhost:8080/main2.qml'
onLoaded: {
root.width = item.width
root.height = item.height
}
}
現(xiàn)在我們可以要求 qmlscene 加載本地的 remote.qml 從而實(shí)現(xiàn)加載遠(yuǎn)程文件。還有一個問題 —— 加載程序?qū)⒄{(diào)整到加載項(xiàng)目的大小。而我們的 qmlscene 也需要適應(yīng)這種尺寸。這可以使用 qmlscene 的 --resize-to-root 選項(xiàng)來實(shí)現(xiàn):
$ qmlscene --resize-to-root remote.qml
調(diào)整到根的大小告訴 qml 場景將其窗口的大小調(diào)整為根元素的大小。遠(yuǎn)程目前正在從本地服務(wù)器加載 main.qml,并將其自身調(diào)整為加載的用戶界面。這很優(yōu)雅和簡單。
注意:
如果我們不想運(yùn)行本地服務(wù)器,還可以使用 GitHub 的 gist 服務(wù)。Gist 是像 PasteBin 和其他的在線服務(wù)的剪貼板。它可以在 https://gist.github.com 下找到。 我(原作者)為這個例子創(chuàng)建了 https://gist.github.com/jryannel/7983492 下的一個小小的要點(diǎn)。這將顯示一個綠色矩形。由于主要網(wǎng)址將網(wǎng)站提供為 HTML 代碼,我們需要將 /raw 附加到網(wǎng)址以檢索原始文件而不是 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
}
}
要通過網(wǎng)絡(luò)加載另一個文件,我們只需要引用組件名稱。例如,Button.qml 可以正常訪問,只要它在同一個遠(yuǎn)程文件夾中。
11.1.1 網(wǎng)絡(luò)組件
讓我們創(chuàng)建一個小實(shí)驗(yàn)。我們添加到我們的遠(yuǎn)程端一個小按鈕作為可重復(fù)使用的組件。
- 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()
}
}
再次啟動我們的網(wǎng)絡(luò)服務(wù)器:
$ cd src
# python -m SimpleHTTPServer 8080
我們的遠(yuǎn)程加載程序通過 http 重新加載主要的 QML:
$ qmlscene --resize-to-root remote.qml
我們看到的是一個錯誤:
http://localhost:8080/main2.qml:11:5: Button is not a type
所以 QML 在遠(yuǎn)程加載時(shí)無法解析按鈕組件。如果代碼將在本地 qmlscene src/main.qml 這將是沒有問題的。本地 Qt 可以解析目錄并檢測哪些組件可用,但遠(yuǎn)程地,http 沒有 “l(fā)ist-dir” 功能。我們可以強(qiáng)制 QML 使用 main.qml 中的 import 語句加載元素:
import "http://localhost:8080" as Remote
...
Remote.Button { ... }
當(dāng) qmlscene 再次運(yùn)行時(shí),這將可以正常工作:
$ 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()
}
}
更好的選擇是使用服務(wù)器端的 qmldir 文件來控制導(dǎo)出。
// qmldir
Button 1.0 Button.qml
然后更新 main.qml:
import "http://localhost:8080" 1.0 as Remote
...
Remote.Button { ... }
注意:
當(dāng)使用本地文件系統(tǒng)中的組件時(shí),將立即創(chuàng)建它們,而不會有延遲。當(dāng)通過網(wǎng)絡(luò)加載組件時(shí),它們將異步創(chuàng)建。這具有這樣的問題:創(chuàng)建的時(shí)間是未知的,并且當(dāng)其他元素已經(jīng)加載完成時(shí)有些元素可能尚未被完全加載。在使用通過網(wǎng)絡(luò)加載的組件時(shí)需要考慮到這一點(diǎn)。
11.2 模板
當(dāng)使用 HTML 項(xiàng)目時(shí),通常需要使用模板驅(qū)動開發(fā)。服務(wù)器使用模板機(jī)制生成代碼在服務(wù)器端對一個 HTML 根進(jìn)行擴(kuò)展。例如一個照片列表的列表頭將使用 HTML 編碼,動態(tài)圖片鏈表將會使用模板機(jī)制動態(tài)生成。一般來說,這也可以使用 QML 來完成,但有一些問題。
首先它是沒有必要的。HTML 開發(fā)人員這樣做的原因是克服對 HTML 后端的限制。在 HTML 中沒有組件模型,因此動態(tài)方面必須使用這些機(jī)制來替代,或者在客戶端使用程序化的 JavaScript。許多 JS 框架(jQuery、dojo、backbone、angular、...)都用來解決這個問題,并將更多的邏輯放在客戶端瀏覽器中以與網(wǎng)絡(luò)服務(wù)連接。然后,客戶端將僅使用 Web 服務(wù) API(例如,提供 JSON 或 XML 數(shù)據(jù))來與服務(wù)器進(jìn)行通信。這似乎也是 QML 更好的方法。
第二個問題是 QML 的組件緩存。當(dāng) QML 訪問組件時(shí),它將緩存渲染樹,并加載緩存版本進(jìn)行渲染。在重新啟動客戶端之前,將無法檢測到磁盤或遠(yuǎn)程的修改版本。為了克服這個問題,我們可以使用一個技巧。我們可以使用 URL 片段來加載網(wǎng)址(例如 http://localhost:8080/main.qml#1234),其中 '#1234' 是片段。HTTP 服務(wù)器始終保持相同的文檔,但 QML 將使用完整的 URL(包括片段)存儲此文檔。每次我們訪問此 URL 時(shí),片段都需要更改,并且 QML 緩存不會得到這個信息。片段可以是例如當(dāng)前時(shí)間(毫秒)或隨機(jī)數(shù)。
Loader {
source: 'http://localhost:8080/main.qml#' + new Date().getTime()
}
總而言之,模板是可能的,但不是很推薦的,并沒有發(fā)揮 QML 的優(yōu)勢。更好的方法是使用提供 JSON 或 XML 數(shù)據(jù)的 Web 服務(wù)器。
11.3 HTTP 請求
Qt 中的 http 請求通常使用 QNetworkRequest 和 QNetworkReply 從 C++ 代碼中完成,然后響應(yīng)將使用 Qt/C++ 集成推送數(shù)據(jù)到 QML 代碼中。所以我們試圖把這個信封放在這里,使用 Qt Quick 提供的當(dāng)前工具讓我們與一個網(wǎng)絡(luò)端點(diǎn)進(jìn)行通信。為此,我們使用一個幫助對象來發(fā)出 http 請求,響應(yīng)周期。它以 java 腳本 XMLHttpRequest 對象的形式出現(xiàn)。
XMLHttpRequest 對象允許用戶注冊一個響應(yīng)句柄函數(shù)和一個 url??梢允褂?http 動詞之一(get,post,put,delete,...)發(fā)送請求。當(dāng)響應(yīng)到達(dá)時(shí),調(diào)用 handle 函數(shù)。句柄函數(shù)被調(diào)用多次。每次請求狀態(tài)已更改(例如標(biāo)題已到達(dá)或請求完成)。
這里有一個簡短的例子:
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();
}
對于響應(yīng),我們可以獲取 XML 格式或只是原始文本??梢詫Y(jié)果 XML 進(jìn)行迭代,但更常用的是 JSON 格式響應(yīng)的原始文本。JSON 文檔將用于使用 JSON.parse(text) 將文本轉(zhuǎn)換為 JS 對象。
...
} else if(xhr.readyState === XMLHttpRequest.DONE) {
var object = JSON.parse(xhr.responseText.toString());
print(JSON.stringify(object, null, 2));
}
在響應(yīng)處理程序中,我們訪問原始響應(yīng)文本并將其轉(zhuǎn)換為 JavaScript 對象。這個 JSON 對象現(xiàn)在是一個有效的 JS 對象(在javascript中,對象可以是對象或數(shù)組)。
注意:
似乎優(yōu)先使用 toString() 轉(zhuǎn)換使代碼更加穩(wěn)定。沒有進(jìn)行明確的轉(zhuǎn)換,我有幾次解析器錯誤。不知道是什么原因。
11.3.1 Flickr 調(diào)用
讓我們來看看一個更真實(shí)的世界的例子。一個典型的例子是使用 Flickr 服務(wù)來檢索新上傳圖片的公共 Feed。為此,我們可以使用 http://api.flickr.com/services/feeds/photos_public.gne 網(wǎng)址。不幸的是,它默認(rèn)返回一個 XML 流,這可以很容易地被 qml 中的 XmlListModel 解析。為了實(shí)例,我們想集中注意力在 JSON 數(shù)據(jù)上。為了獲得一個干凈的 JSON 響應(yīng),我們需要為請求附加一些參數(shù):http://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1。這將返回沒有 JSON 回調(diào)的 JSON 響應(yīng)。
注意:
JSON 回調(diào)將 JSON 響應(yīng)包裝到函數(shù)調(diào)用中。這是用于 HTML 編程的快捷方式,其中使用腳本標(biāo)記來生成 JSON 請求。響應(yīng)將觸發(fā)由回調(diào)定義的本地函數(shù)。在 QML 中沒有使用 JSON 回調(diào)的機(jī)制。
讓我們先來看看使用 curl 的回應(yīng):
curl "http://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=munich"
響應(yīng)將是類似下面這樣的:
{
"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 文檔具有定義好的結(jié)構(gòu)。具有標(biāo)題和項(xiàng)目屬性的對象。標(biāo)題是字符串,而項(xiàng)目是一組對象。將此文本轉(zhuǎn)換為 JSON 文檔時(shí),我們可以訪問各個條目,因?yàn)樗怯行У?JS 對象/數(shù)組結(jié)構(gòu)。
// 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 數(shù)組,我們可以使用 obj.items 數(shù)組作為列表視圖的模型。我們將盡力實(shí)現(xiàn)這一點(diǎn)。首先,我們需要檢索響應(yīng)并將其轉(zhuǎn)換為有效的 JS 對象。 然后我們可以將 response.items 屬性設(shè)置為列表視圖的模型。
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();
}
這是完整的源代碼,我們創(chuàng)建請求時(shí),加載組件。然后,請求響應(yīng)用作我們的簡單列表視圖的模型。
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()
}
}
當(dāng)文檔完全加載(Component.onCompleted)時(shí),我們從 Flickr 請求最新的 Feed 內(nèi)容。在到達(dá)時(shí),我們解析 JSON 響應(yīng),并將 items 數(shù)組設(shè)置為我們視圖的模型。列表視圖具有一個代理,它在一行中顯示縮略圖圖標(biāo)和標(biāo)題文本。
另一個選擇是擁有占位符 ListModel 并將每個項(xiàng)目附加到列表模型上。為了支持更大的模型,需要支持分頁(例如第1頁,共10頁)和懶惰內(nèi)容檢索(lazy content retrieval)。
11.4 本地文件
也可以使用 XMLHttpRequest 加載本地(XML / JSON)文件。例如,可以使用以下命令加載名為 “colors.json” 的本地文件:
xhr.open("GET", "colors.json");
我們使用它來讀取顏色表并將其顯示為網(wǎng)格。不能從 Qt Quick 側(cè)修改文件。要將數(shù)據(jù)存儲回源,我們需要一個基于 REST 的小型 HTTP 服務(wù)器或本地 Qt Quick 擴(kuò)展來進(jìn)行文件訪問。
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 文件。