Skip to main content

Raymii.org Raymii.org Logo

Quis custodiet ipsos custodes?
Home | About | All pages | Cluster Status | RSS Feed | Gopher

HTTP GET requests with Qt and in Qml (async)

Published: 29-04-2022 | Author: Remy van Elst | Text only version of this article


Table of Contents


With Qt it's very easy to work with (async) HTTP requests. This guide shows you how to do it with Qt core and in Qml. The two examples print the output of a HTTP GET request on screen after pressing a button. The Qml method uses JavaScript, so that's cheating a bit, the other method uses plain C++ with Qt's libraries for networking (QNetworkAccessManager) and signals and slots for the async part.

I'm developing a desktop monitoring app, Leaf Node Monitoring, open source, but paid. For Windows, Linux & Android, go check it out.

Consider sponsoring me on Github. It means the world to me if you show your appreciation and you'll help pay the server costs.

You can also sponsor me by getting a Digital Ocean VPS. With this referral link you'll get $100 credit for 60 days.

This guide is written mainly because I find myself doing this often and keep looking in other projects where I've already did this to copy over the code. Even my fellow colleagues over at work peek at my GitHub for this specific thing, I was told recently, so better put it up online.

screenshot

Screenshot of the demo app layout

Without using Qt, I'd probably handle network requests using curl or something like cpp-httplib, a header-only http client/server. I've done plain old C++ network http requests before and written about it here, on parsing HackerNews and Lobste.rs API's.

The full code for this guide can be found on my github.

Basic setup

Using Qt Creator, do a File, New Project. Select an empty Qt Quick (QML) application and finish the wizard. I'm using Qt 5.15, but the example also works with Qt 6.3.

This is the main.qml file layout, 2 rows with a button and a textfield:

Column {
    spacing: 5
    anchors.fill: parent
    anchors.margins: 5

    Row {
        spacing: 5
        Button {
            text: "Qml HTTP GET"
        }

        TextField {
            id: qmlResult
        }
    }

    Row {
        spacing: 5
        Button {
            text: "C++ HTTP GET "
        }

        TextField {
            id: cppResult
        }
    }
}

C++ HTTP GET Request

The plain old C++ HTTP Get uses a few Qt provides classes, namely QNetworkAccessManager, QNetworkRequest and QNetworkReply, including a few signals and slots to handle the request async.

We'll start by doing some busywork, creating the class derived from QObject and registering it for the QML Engine. If you've done any Qt before, you know that you'll do this many times and as I do, consider it busywork. Whichever form of qRegister/qmlRegister you need depends on the shape of the moon, but Qt 6 has made improvements on that spectrum, now using cmake and only 1 place to register objects.

Create classes and Qml registration

Make a new class named NetworkExample based off QObject, either by creating the files yourself or by using the Qt Creator Add New wizard, in that case select a new C++ class and give it QObject as base:

add new class wizard

NetworkExample.h

#ifndef NETWORKEXAMPLE_H
#define NETWORKEXAMPLE_H

#include <QObject>

class NetworkExample : public QObject
{
    Q_OBJECT
public:
    explicit NetworkExample(QObject *parent = nullptr);

signals:

};

#endif // NETWORKEXAMPLE_H

NetworkExample.cpp

#include "NetworkExample.h"

NetworkExample::NetworkExample(QObject *parent)
    : QObject{parent}
{

}

The file does not do anything yet. In main.cpp, create an instance and register it to the Qml engine so we can import it in Qml:

#include "NetworkExample.h"
[...] // below the QGuiApplication line
NetworkExample* networkExample = new NetworkExample();
qmlRegisterSingletonInstance<NetworkExample>("org.raymii.NetworkExample", 1, 0, "NetworkExample", networkExample);

At the bottom of the file, change the return app.exec() line so we save that value but also destroy our object before quitting:

auto result = app.exec();
networkExample->deleteLater();
return result;

Even though this is a simple example, I'm hoping to teach you a bit of hygiene by explicitly adding this part.

In main.qml, below the other import lines:

import org.raymii.NetworkExample 1.0

Network request

Finally, time to do the actual request. Add the <QNetworkAccessManager> header to your includes and add a QNetworkAccessManager* _manager = nullptr; in the private: section of your header. Inside the constructor, new it:

_manager = new QNetworkAccessManager(this);

Since we're providing a parent object, new is fine. Once the parent QObject is destroyed, this one will also be destroyed.

Add a method to do the actual request. In your header, declare and mark it as Q_INVOKABLE so Qml can call it:

Q_INVOKABLE void doGetRequest(const QString& url);

The function definition:

void NetworkExample::doGetRequest(const QString& url)
{
    setResponse("");
    auto _request = QScopedPointer<QNetworkRequest>(new QNetworkRequest());
    _request->setUrl(url);
    _request->setTransferTimeout(5000);
    _request->setRawHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/99.0");

    QNetworkReply *reply = _manager->get(*_request);
    QObject::connect(reply, &QNetworkReply::finished, this, &NetworkExample::slotFinished);
}

Don't forget to include the <QNetworkReply> header.

First part is a Qt style smart pointer, so we don't have to delete that QNetworkRequest ourselves. Once it goes out of scope, it is destroyed. The very first line clears out any previous response data in our Q_PROPERTY, we will define that later.

Next up we set a few parameters, most important one being the URL, and as a bonus I've included setting a user agent header and request timeout of 5 seconds.

Using our QNetworkAccessManager we send the request off, then connecting up the finished signal to out reply. To keep this guide simple, I'm not connecting up the errorOccured or readyRead signals, but you probably should read the docs regarding the signals QNetworkReply can emit.

Add a new slot (regular method, below the line public slots:) for our slotFinished method:

public slots:
    void slotFinished();

Contents:

void NetworkExample::slotFinished()
{
    QNetworkReply *reply = dynamic_cast<QNetworkReply*>(sender());
    if(reply != nullptr) {
        setResponse(reply->readAll());
        reply->deleteLater();
    }
}

Every signal/slot connection has method that returns a pointer to the object that sent the signal, QObject::sender(). I'm using it with a dynamic_cast to make sure it's not a nullptr and the correct type. Using QNetworkReply::readAll(), the entire reply is available. If slotFinished () is called directly (not via a signal/slot), the reply object will be a nullptr. There are a few more considerations to keep in mind with QObject::sender() like if the origin object is destroyed and DirectConnection, but for our example this will work just fine.

The documentation mentions explicitly to call deleteLater() on the networkReply, so we do that instead of regular delete.

The last part of our method is a new Q_PROPERTY named response. Add it in the header just below the line Q_OBJECT:

Q_PROPERTY(QString response READ response WRITE setResponse NOTIFY responseChanged)

In recent versions of Qt Creator you can right-click the Q_PROPERTY part and select Refactor, Generate Missing Q_PROPERTY Members. Do that, nothing special about this property otherwise. If your Qt Creator version does not show that handy option, add the signal/slot and member variable yourself manually.

In Qml, bind this property to the TextField text property:

TextField {
    id: cppResult
    text: NetworkExample.response
}

Make the Button call the function we've just defined:

Button {
    text: "C++ HTTP GET "
    onClicked: NetworkExample.doGetRequest("http://httpbin.org/ip")
}

This URL will send back a JSON response containing the sending IP.

Press the big green Play (run) button and test it out:

cpp get request

That was easy right? No messing around with a CURL* or curl_easy_setopt() and async by default. The QML / JavaScript part is even easier, so easy it feels like type-unsafe cheating.

QML HTTP GET request

The QML part is just plain old JavaScript with a property binding. In the main.qml file, define a property var which will hold the response data, inside the Window{}, just above our Column:

property var response: undefined

Right below the new property, add a function that will do the request:

function doGetRequest(url) {
    var xmlhttp = new XMLHttpRequest()
    xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState === XMLHttpRequest.DONE
                && xmlhttp.status == 200) {
            response = xmlhttp.responseText
        }
    }
    xmlhttp.open("GET", url, true)
    xmlhttp.send()
}

The method, when called, does a XMLHttpRequest, with a callback function that checks the status code, if the request was successful it updates the response property. Bind the response property to our TextField:

TextField {
    id: qmlResult
    text: response
}

Add the new function to the Button's onClicked:

Button {
    text: "Qml HTTP GET"
    onClicked: {
        response = ""
        doGetRequest("http://httpbin.org/ip")
    }
}

Go forth, press the big green Play button and test it out:

qml get

You could of course, in the case of JSON, add a JSON.parse(xmlhttp.responseText), then you can access the JSON right inside QML, (text: response.origin), or add more error handling.

As you can see, because it's just JavaScript, this is even easier than the already very simple C++ part.

If you want to test the async-ness, specifically, not blocking the GUI thread, use the url https://httpbin.org/delay/4, which will wait 4 seconds before responding. You should still be able to click the buttons and see stuff happening.

Please send me your thoughts regarding what you like best, C++ or Qml for this purpose.

Tags: c++ , cpp , networking , qml , qt , qt5 , qt6 , seeed , tutorials