Qt with Cascades UI Examples Documentation

Contents

HTTP Example

Files:

Description

The HTTP example demonstrates a simple HTTP client that shows how to fetch files specified by URLs from remote hosts.

The application describes how to create a widget behaving like a dialog in cascades and how to use QNetworkAccessManager to handle HTTP requests.

The UI setup is straight forward and split into three parts, the Downloader, which handles user input and the two dialogs to handle errors and authentication.

    import bb.cascades 1.0

    // This page shows transitions between different screens through toggling Container visibility
    Page {
        // A container is used to gather visual items together.
        Container {
            layout: DockLayout {}

            ImageView {
                horizontalAlignment: HorizontalAlignment.Fill
                verticalAlignment: VerticalAlignment.Fill

                imageSource: "asset:///images/background.png"
            }

            // A custom Container for the download screen
            Downloader {
                horizontalAlignment: HorizontalAlignment.Fill
                verticalAlignment: VerticalAlignment.Fill
            }

            // A custom Container for the authentication dialog
            AuthenticationDialog {
                horizontalAlignment: HorizontalAlignment.Fill
                verticalAlignment: VerticalAlignment.Fill

                visible: _authDialog.visible
            }

            // A custom Container for the message box
            MessageBox {
                horizontalAlignment: HorizontalAlignment.Fill
                verticalAlignment: VerticalAlignment.Fill

                visible: _messageBox.visible
            }
        }
    }

The downloader UI just contains an input field for the URL, a download and a close button and a status label. The download button and the label are linked to the C++ side with a property binding to exported objects. For easier readability the exported objects start with "_":

            // A standard TextField
            TextField {
                id: urlEdit

                verticalAlignment: VerticalAlignment.Center

                layoutProperties: StackLayoutProperties {
                    spaceQuota: 1
                }

                // Default url to use
                text: "http://qt-project.org/"
                //text: "http://www.pagetutor.com/keeper/mystash/secretstuff.html" // For HTTP authentication example (username is jimmy, password is page)
                //text: "https://qt.nokia.com" // For site with SSL errors (untrusted certificate)
                //text: "http://www.example.com" // For site which redirects

                textStyle {
                    base: SystemDefaults.TextStyles.SmallText
                }

                enabled: _downloader.startDownloadPossible
            }
        }

The HttpDownloader

The C++ backend is located in HttpDownloader.h/cpp. It is a QObject with the two properties that our UI is bound to: statusText and startDownloadPossible. Note that the properties have a NOTIFY parameter, this is needed to have a working property binding on the qml side.

    /**
     * The HttpDownloader class contains all the logic to download URLs from
     * the network and store them in a local file.
     */
    class HttpDownloader : public QObject
    {
        Q_OBJECT

        // Makes available to the UI whether the next download can be started
        Q_PROPERTY(bool startDownloadPossible READ startDownloadPossible NOTIFY startDownloadPossibleChanged)

        // Makes the status text available to the UI
        Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged)

To give the button the ability to start the download, we expose the startDownload() method by making it Q_INVOKABLE.

        // This method is executed when the user clicks the 'Download' button in the UI
        Q_INVOKABLE void startDownload(const QString &url);

This method does some preparation before asking the QNetworkAccessManager to download the file. The first step is checking if the given URL contains a filename, if not it adds an index.html to the URL path:

        m_url = url;

        // Extract the file name from the URL ...
        const QFileInfo fileInfo(m_url.path());
        m_fileName = fileInfo.fileName();

        // ... and fall back to 'index.html' if it is empty.
        if (m_fileName.isEmpty())
            m_fileName = "index.html";

Check if the file is already stored on the device and ask the user if the file should be overridden. This is done by using the MessageBoxController.

        // Locate the file in the temp directory
        const QString actualFileName = "tmp/" + m_fileName;

        // If the file already exists, ask whether it should be overwritten
        if (QFile::exists(actualFileName)) {
            const MessageBoxController::Result result = m_messageBoxController.exec(tr("HTTP"),
                                                                                    tr("There already exists a file called %1 in "
                                                                                       "the target directory.\nOverwrite?").arg(m_fileName),
                                                                                    tr("Overwrite"), tr("Cancel"));
            if (result == MessageBoxController::Button2) {
                // If it shouldn't be overwritten, abort here ...
                return;
            }

            // ... otherwise remove the existing file.
            QFile::remove(actualFileName);
        }

Finally we open the file, disable the download button, to avoid a double download and ask the QNetworkAccessManager to download the file.

        // Open the file for writing
        m_file = new QFile(actualFileName);
        if (!m_file->open(QIODevice::WriteOnly)) {
            m_messageBoxController.exec(tr("HTTP"), tr("Unable to save the file %1: %2.").arg(m_fileName).arg(m_file->errorString()), tr("Ok"), QString());
            delete m_file;
            m_file = 0;
            return;
        }

        setStartDownloadPossible(false);

        // Trigger the download
        startRequest();

The QNetworkAccessManager has a very simple api, a network request for the requested URL is created and it returns a QNetworkReply which emits two signals: readyRead() which signals that data can be read from it and finished() which indicates the download was completed. The QNetworkAccessManager can handle more than HTTP of course, so entering an FTP URL for example is fine too.

    void HttpDownloader::startRequest()
    {
        // Start the download ...
        m_reply = m_qnam.get(QNetworkRequest(m_url));

        // ... and create signal/slot connections to get informed about state changes
        connect(m_reply, SIGNAL(finished()), this, SLOT(httpFinished()));
        connect(m_reply, SIGNAL(readyRead()), this, SLOT(httpReadyRead()));
    }

Finishing the download is just closing the file and check if the download succeeded, but there is one case left, the HTTP redirect. The QNetworkAccessManager makes the handling of this case easy. We just ask the QNetworkReply if the RedirectionTargetAttribute was set and start the process again if the user approves the redirect.

    void HttpDownloader::httpFinished()
    {
        // All data have been written to the file, so close it
        m_file->flush();
        m_file->close();
        const QVariant redirectionTarget = m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute);

        if (m_reply->error()) {
            // If there was an error, show an error message in a message box
            m_messageBoxController.exec(tr("HTTP"), tr("Download failed: %1.").arg(m_reply->errorString()), tr("Ok"), QString());

            setStartDownloadPossible(true);
        } else if (!redirectionTarget.isNull()) {
            // If we got a redirect response (3XX), retrieve the redirect URL ...
            const QUrl newUrl = m_url.resolved(redirectionTarget.toUrl());

            // ... and ask the user whether the redirected page should be downloaded instead
            const MessageBoxController::Result result = m_messageBoxController.exec(tr("HTTP"), tr("Redirect to %1 ?").arg(newUrl.toString()), tr("Yes"), tr("No"));

            if (result == MessageBoxController::Button1) {
                // If the redirected page should be downloaded, reset the URL ...
                m_url = newUrl;

                // ... delete the old network reply object ...
                m_reply->deleteLater();

                // ... reset the target file ...
                m_file->open(QIODevice::WriteOnly);
                m_file->resize(0);

                // ... and trigger a new download request with the new URL.
                startRequest();
                return;
            } else {
                setStartDownloadPossible(true);
            }
        } else {
            // If the download was successful, update the status message
            const QFileInfo actualDir(*m_file);
            m_statusText = tr("Downloaded %1 to %2.").arg(m_fileName).arg(actualDir.absolutePath());
            emit statusTextChanged();
            setStartDownloadPossible(true);
        }

        // Delete the network reply object
        m_reply->deleteLater();
        m_reply = 0;

        // Clean up the target file object
        delete m_file;
        m_file = 0;
    }

The MessageBoxController

The messagebox UI only shows a label and two buttons all logic is handled by accessing the exported _messageBox object. The dialog logic of the MessageBoxController is located in the DialogController, which acts like a modal dialog in qml. Its implementation is very simple, it just makes the dialog visible and starts a local event loop on exec which catches all events. If the dialog is closed the UI is hidden again and the local event loop gives control back to the parent event loop.

    /**
     * The DialogController is a helper class that is used to implement
     * dialog-like behavior with QML.
     * It blocks the execution flow when opening a dialog by starting a
     * nested event loop. The event loop will handle all user input while
     * the dialog is open and will be quit when the dialog is closed.
     */
    class DialogController : public QObject
    {
        Q_OBJECT

        // This property describes whether the dialog is currently visible
        Q_PROPERTY(bool visible READ visible NOTIFY visibleChanged)

    public:
        explicit DialogController(QObject *parent = 0);

        // This method is called to show the dialog
        void exec();

        // The accessor method for the visible property
        bool visible() const;

    Q_SIGNALS:
        // The change notification signal of the visible property
        void visibleChanged();

    protected:
        // This method is called to close the dialog.
        void close();

    private:
        // The flag that defines whether the dialog is visible or not
        bool m_visible;

        // The nested event loop
        QEventLoop m_localLoop;
    };

    void DialogController::exec()
    {
        // Mark the dialog as visible
        m_visible = true;
        emit visibleChanged();

        /**
         * Start the local event loop.
         *
         * Note: the local execution flow is blocked now until m_localLoop.quit() is called.
         */
        m_localLoop.exec();

        // Mark the dialog as hidden
        m_visible = false;
        emit visibleChanged();
    }

The MessageBoxController only adds an overloaded version of exec. That version provides the ability to set the names of the dialogs and returns which button was pressed by the user.

        /**
         * Describes the two buttons of the dialog.
         */
        enum Result
        {
            Button1,
            Button2
        };

        // Shows the message box with the given title, text and the two buttons
        Result exec(const QString &title, const QString &text, const QString &button1Text, const QString &button2Text);

The AuthenticationDialog

The AuthenticationDialogController works very similar to the MessageBoxController, with the only difference that the AuthenticationDialogController asks for username and password and stores them.

    /**
     * The AuthenticationDialogController encapsulates the logic of
     * a password dialog. When executed it will block the execution flow
     * until the user has entered a user name and password.
     */
    class AuthenticationDialogController : public DialogController
    {
        Q_OBJECT

        // Makes the site, the credentials are requested for, available to the UI
        Q_PROPERTY(QString site READ site NOTIFY siteChanged)

        // Makes the user name of the credentials available to the UI
        Q_PROPERTY(QString user READ user NOTIFY userChanged)

        // Makes the password of the credentials available to the UI
        Q_PROPERTY(QString password READ password NOTIFY passwordChanged)

    public:
        // The accessor methods for the properties
        QString site() const;
        QString user() const;
        QString password() const;

        // This method is executed to show the dialog
        bool exec(const QString &site, const QString &user, const QString &password);

        // This method is called from the UI when the user has entered a user name and password
        Q_INVOKABLE void continueAuthentication(const QString &user, const QString &password);

        // This method is called from the UI when the user wants to cancel authentication
        Q_INVOKABLE void cancel();

    Q_SIGNALS:
        // The change notification signals of the properties
        void siteChanged();
        void userChanged();
        void passwordChanged();

    private:
        // The property values
        QString m_site;
        QString m_user;
        QString m_password;

        // A flag that stores whether dialog has been canceled
        bool m_result;
    };

Integration

In the end we put it all together starting with the qml document setup:

        QmlDocument *qml = QmlDocument::create("asset:///main.qml");

The next needed step is to export our C++ backend classes, the Downloader and the two dialogs:

        qml->setContextProperty("_downloader", &downloader);
        qml->setContextProperty("_messageBox", downloader.messageBoxController());
        qml->setContextProperty("_authDialog", downloader.authenticationDialogController());

Setup the application object and execute it.

        AbstractPane *appPage = qml->createRootObject<AbstractPane>();
        Application::instance()->setScene(appPage);

        // Quit the application whenever 'Qt.quit()' is called inside the QML document
        QObject::connect(qml->documentContext()->engine(), SIGNAL(quit()), &app, SLOT(quit()));