Integrating a QML Application with Web Resources

  • Tutorial
Hello dear hawk! I want to tell how to integrate the program in the new-fangled language QML with web resources.

In and of itself, QML is a declarative JavaScript-like programming language that is part of the Qt framework. Qt developers are serious and promote it as the main tool for creating interfaces. Moreover, quite a lot of things can be done without resorting to C ++ at all, including the ability to work with web servers.

Web technologies are increasingly penetrating our lives; we often use various web resources. It is not always convenient to launch a browser for this, sometimes a separate client application is much more convenient, which is eloquently indicated, for example, by the number of clients for various social networks, especially on mobile platforms.

Considering that Qt 5.1, the alpha version of which was released last week, includes initial support for Android and iOS, this topic may be especially interesting for those who are looking at Qt or actively mastering it. In this article I will tell you how to organize work with web resources from a QML application using the VK API example.

Just in case, I note that I am considering the latest stable version of Qt 5.0.2. In earlier versions, some features may not be.

What is XMLHttpRequest and why is it needed

Surely, many readers have heard about technology like AJAX (Asynchronous JavaScript And XML). It allows you to send asynchronous requests to the server and update the contents of the page without reloading it. There are various tools for this in modern browsers, XMLHttpRequest is one of them. Since QML is a JavaScript-like language and its JavaScript environment is similar to browser-based, XMLHttpRequest is also present. Later in the text I will also write down its name in abbreviated form - XHR.

Actually, what is it and what does it give us? This is a tool for asynchronous (synchronous browsers are also supported in browsers) HTTP requests. Despite its name, it allows you to transfer data not only in XML format, although it was originally intended for this purpose. The implementation in the QML engine supports the following HTTP requests: GET, POST, HEAD, PUT, and DELETE. Basically, we will use the first two.

A distinctive feature of the implementation of XHR in QML is that requests can be sent to any host, there are no such restrictions as in the browser.

XMLHttpRequest Procedure

The process of working with XHR is as follows.

1. Create an XHR object:

var request = new XMLHttpRequest()


2. We initialize the object, indicating the type of request (aka HTTP method), address and, if necessary, request parameters [1] , which must be transmitted to the server:

request.open('GET', 'http://site.com?param1=value1¶m2=value2')


The first parameter is the type of request, the second is the URL. For a GET request, you need to pass the parameters here, separating them from the address with the '?' Symbol. Parameters are separated by the '&' character.

For a POST request, you must specify the type of content. If we pass data by query parameters, then this is done as follows:

request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')


3. Set the handler to change the status of the request. In most cases, we just need to wait until the request completes and then process the result or errors. At the end of the request, the readyState parameter will be equal to XMLHttpRequest.DONE (for more details on the values, see [2] ).

request.onreadystatechange = function () {
    if (request.readyState === XMLHttpRequest.DONE) {
        if (request.status === 200) {
            console.log(request.responseText)
        } else {
            console.log("HTTP request failed", request.status)
        }
    }
}


Our anonymous function will call readyState on every change. We are interested in completing the request, after which we check whether it was successfully completed. To do this, we check the status code with the success code (200). HTTP is a text protocol and in addition to the numerical values ​​of the codes, a text description is also transmitted, so you can compare the statusText property with a string corresponding to this status, in this case, this is the string “OK”:

if (request.statusText === 'OK')


In case of an error, status and statusText will contain the code and textual description of HTTP status codes (for example, 404 and “Not Found”, respectively).

4. Send a request.

request.send()


In the case of POST, here you need to pass the request parameters:

request.send('param1=value1¶m2=value2')


Not all characters can be transmitted in the query parameters. Therefore, both the parameter and the value should be encoded and, if necessary, decoded accordingly with special functions - encodeURIComponent () and decodeURIComponent (). Usage example:

request.send('%1=%2'.arg(encodeURIComponent(param)).arg(encodeURIComponent(value)))


It is recommended to further process the encoded string and replace the sequence "% 20" (ie, the encoded space) with the '+' character. Before decoding, respectively, do the opposite.

Typically, query parameters pass values ​​of simple types. You can also pass an array, but the syntax is somewhat muddy. For example, sending an array of params of two values ​​would look like this:

request.send('params[]=value1¶ms[]=value2')


If you get the hell out of it, you can even transfer objects (!) As values, but this may not be completely reliable, in the sense that on the receiving side it can turn into an array :)

Using POST requests we can transfer data not only with request parameters but also in the body of the request. For example, you can send data in JSON format. To do this, set the correct Content-Type and content size (Content-Length). An example of sending such a request:

request.setRequestHeader('Content-Type', 'application/json')
var params = {
    param1: value1,
    param2: value2
}
var data = JSON.stringify(params)
request.setRequestHeader('Content-Length', data.length)
request.send(data)


Here JSON is a global object available in QML that provides tools for working with this format [3] .

In fact, the format in which we can transfer data is determined by the server. If it accepts JSON - fine, the JSON helmet. Expects that the data will come by request parameters - so it should be sent.

Now that we have studied the necessary theoretical information, we will begin to practice and work with VKontakte.

Getting and displaying a list of friends

To begin, consider a simple example with methods that do not require authorization and other unnecessary gestures. Getting a friend list falls into this category. We’ll write a simple program that sends XHR to get a list of friends at startup and displays user names and their avatars after receiving it.

Most of the code is the display interface and there is no point in describing it specifically. I only note that if a JavaScript object or array is used as the model, then modelData is used instead of model to get model data.

The most interesting part here is working with the server. To access the VK API, there is a special address: api.vk.com/method . We add the name of the method to the received address (a list of methods can be found in [4]), in our case, this is the friends.get method. To this address you need to send a POST or GET request with the necessary parameters. The answer will come in JSON format. We need to pass the user ID in the uid parameter. We will also pass photo_medium in the fields parameter to get the photo and so that it is not the smallest size.

Below is the actual source code of the program. The userId in main is the user ID.

import QtQuick 2.0
Rectangle {
    id: main
    property int userId: XXX
    property var friends
    width: 320
    height: 640
    color: 'skyblue'
    function getFriends() {
        var request = new XMLHttpRequest()
        request.open('POST', 'https://api.vk.com/method/friends.get')
        request.onreadystatechange = function() {
            if (request.readyState === XMLHttpRequest.DONE) {
                if (request.status && request.status === 200) {
                    console.log("response", request.responseText)
                    var result = JSON.parse(request.responseText)
                    main.friends = result.response
                } else {
                    console.log("HTTP:", request.status, request.statusText)
                }
            }
        }
        request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
        request.send('fields=photo_medium&uid=%1'.arg(main.userId))
    }
    ListView {
        id: view
        anchors.margins: 10
        anchors.fill: parent
        model: friends
        spacing: 10
        delegate: Rectangle {
            width: view.width
            height: 100
            anchors.horizontalCenter: parent.horizontalCenter
            color: 'white'
            border {
                color: 'lightgray'
                width: 2
            }
            radius: 10
            Row {
                anchors.margins: 10
                anchors.fill: parent
                spacing: 10
                Image {
                    id: image
                    height: parent.height
                    fillMode: Image.PreserveAspectFit
                    source: modelData['photo_medium']
                }
                Text {
                    width: parent.width - image.width - parent.spacing
                    anchors.verticalCenter: parent.verticalCenter
                    elide: Text.ElideRight
                    renderType: Text.NativeRendering
                    text: "%1 %2".arg(modelData['first_name']).arg(modelData['last_name'])
                }
            }
        }
    }
    Component.onCompleted: {
        getFriends()
    }
}


I made a conclusion to the console of what will come in the answer, this is convenient if there is a desire to play around with this example.

By running the program, if a valid ID was specified, we get something like this:



The biggest difficulty here is precisely in working with XHR. Let's try to figure this out and simplify the code a bit.

Simplified XMLHttpRequest

There are two challenges to working with XHR.

1. When transmitting data by request parameters, this request needs to be compiled. If these parameters can change, then most likely there will be many operations in the code that glue the request parameters from pieces. In addition, we must not forget that it would be nice to encode the keys and values ​​using encodeURIComponent when compiling, as I wrote above. In total, the code that forms these parameters can turn out to be cumbersome and not very clear. It would be much more convenient to use an object in which the corresponding fields are set as parameters.

I wrote a small JavaScript library that converts an object into query parameters, everything encodes, in general, produces a finished string that can be sent right away. There is also a function that decodes query parameters and creates an object from them (but it only supports simple types, an array or an object in the parameters will not parse, however, it is unlikely to be needed). You can take it here: github.com/krnekit/qml-utils/blob/master/qml/URLQuery.js .

2. Depending on the type of request, the data needs to be sent in different ways, and it may also be necessary to set additional headers. I wrote a library that makes sending XHR easier by providing a single interface. It can send data in any format, for this you can pass a content type parameter, by default it is still considered the same “application / x-www-form-urlencoded”, but remember that data of a different type cannot be transmitted using a GET request , in this case, you will need to use POST. Content-Length will also be automatically calculated and set. It accepts the request type, URL, the callback function (optional), which will be called when the request is completed, and the data type (optional). The function returns the request object itself or null in case of an error. You can take it here:github.com/krnekit/qml-utils/blob/master/qml/XHR.js

Using the two library data, I simplified the previous example. I won’t give all the code here, we’ll only consider what has changed.
At the beginning of the file, we connect the libraries (in this example, the library files are in the same directory as the qml file):

import 'URLQuery.js' as URLQuery
import 'XHR.js' as XHR


We import libraries and set namespaces for them through which we will access functions from libraries.

The function sending the XHR now looks like this:

function getFriends() {
    var params = {
        fields: 'photo_medium',
        uid: main.userId
    }
    function callback(request) {
        if (request.status && request.status === 200) {
            console.log("response", request.responseText)
            var result = JSON.parse(request.responseText)
            main.friends = result.response
        } else {
            console.log("HTTP:", request.status, request.statusText)
        }
    }
    XHR.sendXHR('POST', 'https://api.vk.com/method/friends.get', callback, URLQuery.serializeParams(params))
}


First, we define an object with query parameters. Then the callback function, which is called when the request completes. The function receives the request itself as a parameter. And then we send the request itself, converting the object with the parameters using the serializeParams function.
As a result, the size of the code, one might say, has not changed, but it has become much more structured and understandable.

I will use these functions in the future to make the code simpler. If they are useful to someone, you can take and use, a MIT license.

VK authorization from QML

Not all methods work without authorization, so most likely we will need to log in. As a result, we should get the so-called Authorization Token, which we will then transfer in requests to VKontakte. So that we can log in, you need to create an application on VKontakte. You can do this here: vk.com/editapp?act=create . Select the type of Standalone application. Then we will transfer its ID with one of the request parameters.

1. Authorization methods

Since we are making a standalone application, that is, two methods of authorization, both have their own problems, so you need to choose the least evil :)

1. Direct authorization. An HTTP request is sent with login information to a specific address. The response will be data in JSON format containing a token or error description.

Benefits:
  • Simplicity.

Disadvantages:
  • It is necessary to transfer the secret code of the application (it may even have to be sewn into the program), accordingly there is a risk of its leak.
  • This method will only work for trusted applications. When you create a new application, it will be unavailable and you need to write support to enable it.


2. OAuth authorization. It is implemented as follows. A browser must be built into the program in which the user is shown a special login page. After authorization, a redirect to another page will take place and the current URL will contain a token or error description. VKontakte positions this as the main one.

Benefits:
  • The main and very significant advantage is that it works for all applications and for applications that are not allowed direct authorization, this is generally the only way.
  • No need to pass the secret key.
  • OAuth is a standard and you can also log in to Facebook, for example.


The disadvantages, however, are also significant.

  • You need to open the VK page, which means either try to embed it in the program window or open it in a separate window.
  • Since we open the page, we also need a browser. Accordingly, you will have to drag QtWebkit and everything that it pulls along with itself, why the program will put on weight.
  • You will need to intercept the events of changing the URL of the built-in browser, parse this URL and select parameters from it, which is somewhat more complicated than XHR.


2. Direct authorization

Of course, I requested that they enable direct authorization, but VKontakte support at first leisurely asked me what I needed and then completely clamped on access :( So we will consider it theoretically. It will look something like this:

function login() {
    var params = {
        grant_type: 'password',
        client_id: 123456,
        client_secret: 'XXX',
        username: 'XXX',
        password: 'XXX',
        scope: 'audio'
    }
    function callback(request) {
        if (request.status && request.status === 200) {
            console.log("response", request.responseText)
            var result = JSON.parse(request.responseText)
            if (result.error) {
                console.log("Error:", result.error, result.error_description)
            } else {
                main.authToken = result.auth_token
                // Now do requests with this token
            }
        } else {
            console.log("HTTP:", request.status, request.statusText)
        }
    }
    XHR.sendXHR('POST', 'https://oauth.vk.com/token', callback, URLQuery.serializeParams(params))
}


At the beginning, we form the parameters, in them I indicated for example that access to the user's audio records is required (the scope parameter). Then, the callback function, which in case of an error writes to the console, and if successful, saves a token and API requests can go on.

Just in case, I’ll leave a link to the documentation: vk.com/dev/auth_direct .

3. Authorization through OAuth.

For this type of authorization, we need to show the user the login web page. QtQuick has a WebView component that allows you to embed a WebKit engine into a QML application. After the user logs in, the URL in the browser will change and, in case of successful authorization, will contain a token in the request parameters or a description of the error in the anchor [5] .

In order not to be fooled by parsing this URL, we use the parseParams function from URLQuery. You can pass the whole URL to it at once, at the output we get an object with parameters.

The component that implements this functionality is described below.

LoginWindow.qml:
import QtQuick 2.0
import QtQuick.Window 2.0
import QtWebKit 3.0
import "URLQuery.js" as URLQuery
Window {
    id: loginWindow
    property string applicationId
    property string permissions
    property var finishRegExp: /^https:\/\/oauth.vk.com\/blank.html/
    signal succeeded(string token)
    signal failed(string error)
    function login() {
        var params = {
            client_id: applicationId,
            display: 'popup',
            response_type: 'token',
            redirect_uri: 'http://oauth.vk.com/blank.html'
        }
        if (permissions) {
            params['scope'] = permissions
        }
        webView.url = "https://oauth.vk.com/authorize?%1".arg(URLQuery.serializeParams(params))
    }
    width: 1024
    height: 768
    WebView {
        id: webView
        anchors.fill: parent
        onLoadingChanged: {
            console.log(loadRequest.url.toString())
            if (loadRequest.status === WebView.LoadFailedStatus) {
                loginWindow.failed("Loading error:", loadRequest.errorDomain, loadRequest.errorCode, loadRequest.errorString)
                return
            } else if (loadRequest.status === WebView.LoadStartedStatus) {
                return
            }
            if (!finishRegExp.test(loadRequest.url.toString())) {
                return
            }
            var result = URLQuery.parseParams(loadRequest.url.toString())
            if (!result) {
                loginWindow.failed("Wrong responce from server", loadRequest.url.toString())
                return
            }
            if (result.error) {
                loginWindow.failed("Error", result.error, result.error_description)
                return
            }
            if (!result.access_token) {
                loginWindow.failed("Access token absent", loadRequest.url.toString())
                return
            }
            succeeded(result.access_token)
            return
        }
    }
}


We display this component in a separate window. After calling the login () method, the login page will be loaded.



After authorization, you will be redirected to a URL in which oauth.vk.com/blank.html will be used as the address , and then through '?' or '#' the result will go. With the permissions parameter we set the access rights that we need. If we indicate something there, then when logging in through our widget, the user will see a dialog for granting access rights to the application.

In order to understand when we got to the right address, we set the onLoadingChanged handler. It takes a loadRequest object, from which we get all the information we need. It is called several times and we are interested in the situation either when an error occurred, in which case we send the appropriate signal, or when the desired page is loaded. In this case, we check if a token has come to us and, if so, send a signal about successful authorization, otherwise an error signal.

Well, now consider the program itself that uses this widget. In case of successful authorization, the program sets the user status to “test”. The user ID is set by the userId property in main.

import QtQuick 2.0
import 'URLQuery.js' as URLQuery
import 'XHR.js' as XHR
Rectangle {
    id: main
    property int userId: XXX
    property var authToken
    width: 640
    height: 320
    function processLoginSuccess(token) {
        loginWindow.visible = false
        authToken = token
        setStatus()
    }
    function setStatus() {
        var params = {
            access_token: main.authToken,
            text: 'test'
        }
        function callback(request) {
            if (request.status == 200) {
                console.log('result', request.responseText)
                var result = JSON.parse(request.responseText)
                if (result.error) {
                    console.log('Error:', result.error.error_code,result.error.error_msg)
                } else {
                    console.log('Success')
                }
            } else {
                console.log('HTTP:', request.status, request.statusText)
            }
            Qt.quit()
        }
        XHR.sendXHR('POST', 'https://api.vk.com/method/status.set', callback, URLQuery.serializeParams(params))
    }
    LoginWindow {
        id: loginWindow
        applicationId: XXX
        permissions: 'status'
        visible: false
        onSucceeded: processLoginSuccess(token)
        onFailed: {
            console.log('Login failed', error)
            Qt.quit()
        }
    }
    Component.onCompleted: {
        loginWindow.visible = true
        loginWindow.login()
    }
}


After loading, we will see the login window. After the login, it is hidden and a request is sent to the server to change the user status. After that, the program writes the result to the console and terminates.

After we logged in, we do not need to request a token anymore, if we did not need some additional access rights or its lifetime did not expire (we will be returned with the token, in case of successful authorization).

What else can you use XMLHttpRequest for

I’ll tell you a short story from my experience, not related to VKontakte, but related to XHR.

Somehow my colleague had a task to receive and process XML data in QML.

QtQuick has a special type of XmlListModel that can pull out, parse, and render an XML file as a model. He needs to set up a query of type XPath, according to which the model will be filled. The problem was that the XML file contained not only the elements that needed to be selected and placed in the model, but also some additional information that also needed to be obtained.

There are several solution methods. You can use two XmlListModel objects, but this is an unambiguous crutch, besides, I did not want the XML file to be downloaded twice (and it will be checked). You can implement this functionality using Qt, which contains as many as several parser options, but there was a desire to solve the problem in pure QML.

Since XMLHttpRequest was originally intended to work with XML, it has tools for working with XML. Accordingly, you can get and parse XML with its tools and select the necessary information. Then the same XML can be passed to the XmlListModel (there you can pass not only the URI, but also the contents of the XML file).

So, despite the fact that now XMLHttpRequest is used for anything, you should not forget why it was created and that there are also tools for working with XML.

A small summary

QML contains many tools available for JavaScript in the browser. XMLHttpRequest allows you to send HTTP requests and thereby ensure the integration of QML applications with web resources. Using XHR allows in many cases to do without using C ++ to exchange data with the server and thereby simplify development.

Also popular now: