QtWebApp — пошаговый разжёванный пример с подробными комментариями

в 11:15, , рубрики: c++, edisonsoftware, qt, Блог компании Edison, Программирование, проектирование, разработка, тестирование

QtWebApp — пошаговый разжёванный пример с подробными комментариями - 1

В процессе разработки приложения на Qt, может понадобиться добавить в данное приложение веб-интерфейс, что особенно может быть актуально при разработке встраиваемых систем с использованием Qt. Для решения данной задачи можно либо написать собственное решение, либо воспользоваться готовыми решениями. Например, библиотекой QtWebApp, которая предоставляет необходимый функционал для создания web-интерфейса.

К достоинствам данной библиотеки можно отнести:

  1. формирование страниц с динамическим содержанием по шаблонам;
  2. формирование полностью динамических страниц;
  3. работу с Cookie, что позволит добавить авторизацию на приложении;
  4. работу со статическими файлами, например, style.css или изображения;
  5. реализацию загрузки файлов.

Предлагаю подробно рассмотреть один из вариантов запуска небольшого приложения на Qt, которое будет иметь несколько web-страниц, работающих с применением библиотеки QtWebApp.

На момент написания статьи изначально использовалась библиотека QtWebApp 1.6.3 и Qt 5.6. Проект успешно был запущен с комплектами сборки MSVC2013 и MinGW. В процессе отладки был замечен баг в классе Template библиотеки QtWebApp. После исправления бага и связи с разработчиком версия библиотеки была повышена до 1.6.4. Исходя из этого, можно отметить также плюс библиотеки, что разработчик ответил в течение суток на информацию о баге, и в тот же день версия библиотеки была повышена. Окончательный вариант примера приложения был подготовлен на версии 1.6.4.

В данном проекте предлагается создать приложение, имеющее три страницы, меню для выбора этих страниц, и три статических файл. Один из файлов – это style.css, а два других – это изображения.

Структура проекта

QtWebApp — пошаговый разжёванный пример с подробными комментариями - 2 Проект будет сформирован в виде Subdirs проекта, который будет состоять из основного проекта и проекта библиотеки QtWebApp.
Структура проекта:

QtWebAppExample.pro – основной профайл проекта
common – пользовательский проект web-сервера

  • o common.pro – профайл проекта приложения с веб-сервером
  • o httpsettings.hpp – файл настроек приложения, в котором наследованный от QSettings класс
  • o webconfigurator.h – заголовочный файл класса конфигуратора web-интерфейса, отвечает за формирование базы всех web-страниц приложения
  • o webconfigurator.cpp – файл исходных кодов конфигуратора web-интерфейса
  • o webconfiguratorpage.h – заголовочный файл всех классов web-страниц Qt приложения
  • o webconfiguratorpage.cpp – файл исходных кодов web-страниц
  • o resources.qrc – ресурсный файл, содержащий шаблоны web-страниц и их составляющие части
  • o html-static – папка, содержащая статичные файлы, которые не будут изменяться динамически в процессе работы приложения

QtWebApp – проект библиотеки

  • o QtWebApp.pro – профайл проекта библиотеки
  • o httpserver – подпроект, реализующий работу самого web-сервера
  • o logging – подпроект, реализующий логгирование событий web-сервера
  • o qtservice – подпроект, позволяющий реализвать запуск приложения в качестве службы
  • o templateengine –под проект, реализующий шаблоны страниц, а также подстановку данных в страницы при запросах к серверу.

QtWebAppExample.pro

Общий профайл проекта — шаблон subdirs с подключённым основным проектом и библиотекой QtWebApp. Важна последовательность подключения проектов в файле. Библиотека QtWebApp должна быть прописана первой, иначе при сборке проекта возникнут ошибки:
если на момент сборки основного проекта, который зависит от QtWebApp, собранных файлов библиотеки (.dll или .so) не будет в наличии, проект не соберется.

TEMPLATE = subdirs

SUBDIRS += 
    QtWebApp 
    common

CONFIG += ordered

common.files = common/html-static/*
CONFIG(debug, debug|release) {
    common.path = $$OUT_PWD/../HttpServiceDebug/html-static
} else {
    common.path = $$OUT_PWD/../HttpService/html-static
}

INSTALLS += common 

common.pro

Если профайл библиотеки кардинально в данном примере корректироваться не будет, то настройка профайла основного проекта web-сервера может доставить некоторое неудобство начинающему пользователю. Как видно из ниже следующего скрипта у приложения за ненадобностью отключен модуль, отвечающий за графические библиотеки, но включена сетевая библиотека для обработки запросов к http-серверу.

Помимо этого, необходимо правильно прописать линковку к заголовочным файлам и файлам исходных кодов, а также выходные папки для сборки библиотеки, чтобы собираемый проект смог обратиться по правильному пути к файлам библиотеки.

QT += core network
QT -= gui

TARGET = common
CONFIG += console
CONFIG -= app_bundle
CONFIG += c++11

TEMPLATE = app

SOURCES += main.cpp 
    webconfigurator.cpp 
    webconfiguratorpage.cpp

HEADERS += 
    webconfigurator.h 
    webconfiguratorpage.h 
    httpsettings.hpp

RESOURCES += 
    resources.qrc

CONFIG(debug, debug|release) {
    DESTDIR = $$OUT_PWD/../../HttpServiceDebug
} else {
    DESTDIR = $$OUT_PWD/../../HttpService
}

win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../QtWebApp/release/ -lQtWebApp
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../QtWebApp/debug/ -lQtWebApp
else:unix: LIBS += -L$$OUT_PWD/../QtWebApp/ -lQtWebApp

INCLUDEPATH += $$PWD/../QtWebApp/httpserver
DEPENDPATH += $$PWD/../QtWebApp/httpserver
INCLUDEPATH += $$PWD/../QtWebApp/templateengine
DEPENDPATH += $$PWD/../QtWebApp/templateengine
INCLUDEPATH += $$PWD/../QtWebApp/qtservice
DEPENDPATH += $$PWD/../QtWebApp/qtservice

win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../QtWebApp/release/libQtWebApp.a
else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../QtWebApp/debug/libQtWebApp.a
else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../QtWebApp/release/QtWebApp.lib
else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../QtWebApp/debug/QtWebApp.lib
else:unix: PRE_TARGETDEPS += $$OUT_PWD/../QtWebApp/libQtWebApp.a

DISTFILES += 
    html-static/style.css 
    html-static/favicon-32x32.png 
    html-static/favicon.png 

QtWebApp.pro

Профайл проекта библиотеки по умолчанию показан ниже. Единственным изменением в проекте стало наличие дополнительной настройки сборки в качестве статической библиотеки.

# Build this project to generate a shared library (*.dll or *.so).

TARGET = QtWebApp
TEMPLATE = lib
QT -= gui
CONFIG += staticlib
VERSION = 1.6.4

mac {
   QMAKE_MAC_SDK = macosx10.10
   QMAKE_CXXFLAGS += -std=c++11
   CONFIG += c++11
   QMAKE_LFLAGS_SONAME  = -Wl,-install_name,/usr/local/lib/
}

win32 {
   DEFINES += QTWEBAPPLIB_EXPORT
}

# Windows and Unix get the suffix "d" to indicate a debug version of the library.
# Mac OS gets the suffix "_debug".
CONFIG(debug, debug|release) {
    win32:      TARGET = $$join(TARGET,,,d)
    mac:        TARGET = $$join(TARGET,,,_debug)
    unix:!mac:  TARGET = $$join(TARGET,,,d)
}

DISTFILES += doc/* mainpage.dox Doxyfile
OTHER_FILES += ../readme.txt

include(qtservice/qtservice.pri)
include(logging/logging.pri)
include(httpserver/httpserver.pri)
include(templateengine/templateengine.pri) 

main.cpp

А теперь по порядку пройдёмся по всем файлам проекта common, чтобы разобраться, как можно запустить Qt-приложение с web-интерфейсом. Начнём со стартового файла приложения и с функции main, с которой осуществляется запуск приложения.

Здесь имеется получение пути к файлу настроек, в котором хранятся параметры настройки web-сервера, порт TCP/IP и т.д.
Также создаётся объект класса WebConfigurator, который отвечает за обработку запросов и выдачу по запросам соответствующих страниц web-сервера.

#include <QCoreApplication>
#include <QDir>
#include <webconfigurator.h>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    a.setApplicationName("QtWebAppExample");

    QString configPath = QDir::currentPath() + "/" + QCoreApplication::applicationName() + ".ini";
    new WebConfigurator(configPath);

    return a.exec();
}

HttpSettings.hpp

Содержит фактически вспомогательный класс, не несущий в себе сверх необходимой информации, но в него для удобства вынесена инициализация файла настроек и в случае более глобального проекта данный класс будет удобно расширить. В данном варианте, естественно, больше похоже на дело вкуса.

Все параметры относятся к настройке порта подключения, количеству одновременных сессий, длительности ожидания запроса.

Также в настройках приложения будут содержаться и параметры контроллера статических файлов, в частности путь к папке, в которой будет производиться поиск статических файлов веб-сервера. В данном приложении это папка html-static, которая будет располагаться в той же папке, что и исполняемый файл приложения.

#ifndef HTTPSETTINGS_H
#define HTTPSETTINGS_H

#include <QSettings>

class HttpSettings : public QSettings
{
public:
    explicit HttpSettings(const QString& fileName, QObject* parent = nullptr)
        : QSettings(fileName,QSettings::IniFormat,parent)
    {
        // Настройки веб-сервера
        setValue("port",             value("port", 8080));
        setValue("minThreads",       value("minThreads", 1));
        setValue("maxThreads",       value("maxThreads", 100));
        setValue("cleanupInterval",  value("cleanupInterval", 1000));
        setValue("readTimeout",      value("readTimeout", 60000));
        setValue("maxRequestSize",   value("maxRequestSize", 16000));
        setValue("maxMultiPartSize", value("maxMultiPartSize", 10000000));

        // Настройки для статических файлов
        setValue("html-static/path",                value("html-static/path", "html-static"));
        setValue("html-static/encoding",            value("html-static/encoding", "UTF-8"));
        setValue("html-static/maxAge",              value("html-static/maxAge", 60000));
        setValue("html-static/cacheTime",           value("html-static/cacheTime", 60000));
        setValue("html-static/cacheSize",           value("html-static/cacheSize", 1000000));
        setValue("html-static/maxCachedFileSize",   value("html-static/maxCachedFileSize", 65536));
    }
};

#endif // HTTPSETTINGS_H

WebConfigurator.h

А теперь внимательно посмотрим на содержимое класса WebConfigurator, который отвечает непосредственно за определение страниц, которые подлежат к отправке на запрос извне.

Определение страниц осуществляется с помощью объекта класса QHash, который содержит указатели на все объекты web-страниц и соответствующие им ключевые значения, которые соответствуют URL адресам запросов. Но QHash используется лишь для динамических страниц, а для статических страниц используется объект класса StaticFileController.

#ifndef WEBCONFIGURATOR_H
#define WEBCONFIGURATOR_H

#include <httprequesthandler.h>
#include <httplistener.h>

#include <webconfiguratorpage.h>
#include <httpsettings.hpp>
#include <staticfilecontroller.h>

class WebConfigurator : public HttpRequestHandler
{
    Q_OBJECT
    Q_DISABLE_COPY(WebConfigurator)
public:
    WebConfigurator(QString &configPath);
    virtual ~WebConfigurator();
    virtual void service(HttpRequest& request, HttpResponse& response) override;

private:
    QString                             m_configPath;
    HttpSettings                        m_config;
    HttpListener                        m_httpListener;
    QHash<QString,WebConfiguratorPage*> m_pages;
    StaticFileController                *m_staticFileController;
};

#endif // WEBCONFIGURATOR_H

Webconfigurator.cpp

Конфигуратор отвечает за перенаправление запроса на соответствующие страницы и изображения и является хранилищем данных страниц и изображений. Если страница или изображение не существуют, то возвращается ошибка 404.
#include «webconfigurator.h»

WebConfigurator::WebConfigurator(QString &configPath) :
    m_configPath(configPath),
    m_config(m_configPath),
    m_httpListener(&m_config, this)
{
    /* Помещаем в QHash объекты всех динамических страниц,
     * которые будут использоваться на нашем веб-сервере
     * */
    m_pages.insert("/index.html", new IndexPage());
    m_pages.insert("/second.html", new SecondPage());
    m_pages.insert("/first.html", new FirstPage());

    /* Для работы контроллера статических файлов
     * необходимо обратиться к объекту настроек, перейти к группе
     * параметров настройки контроллера и создать новый контроллер
     * используя состояния объекта настроек, выставленное на группу
     * параметров статического контроллера файлов
     * */
    m_config.beginGroup("html-static");
    m_staticFileController = new StaticFileController(&m_config);
    m_config.endGroup();
}

WebConfigurator::~WebConfigurator()
{
    foreach(WebConfiguratorPage* page, m_pages) {
        delete page;
    }
    delete m_staticFileController;
}

void WebConfigurator::service(HttpRequest &request, HttpResponse &response)
{
    /* В данном методе осуществляется проверка адреса запроса
     * на соответствие существующим страницам.
     * В данном случае, если страница существует, то мы
     * обращаемся к объекту страницы и передаём запрос на дальнейшую обработку.
     * В противном случаем возвращаем ошибку 404
     * */
    QByteArray path = request.getPath();
    for(auto i = m_pages.begin(); i != m_pages.end(); ++i) {
        if(path.startsWith(i.key().toLatin1())) {
            return i.value()->handleRequest(request,response);
        }
    }
    if(path=="/") {
        response.redirect("/index.html");
        return;
    }
    if(path.startsWith("/style.css") ||
            path.startsWith("/favicon-32x32.png") ||
            path.startsWith("/favicon.png")){
        return m_staticFileController->service(request, response);
    }
    response.setStatus(404,"Not found");
}

WebConfiguratorPage.h

Данный заголовочный файл содержит объявление основного класса, отвечающего за формирование страниц и наследованные от него три класса страниц для проекта: index.html, first.html, second.html.


#ifndef WEBCONFIGURATORPAGE_H
#define WEBCONFIGURATORPAGE_H

#include <QObject>
#include <httprequesthandler.h>
#include <httplistener.h>
#include <template.h>

class WebConfiguratorPage : public QObject
{
    Q_OBJECT
public:
    WebConfiguratorPage(const QString& title);
    virtual void handleRequest(HttpRequest&, HttpResponse&) {}
    virtual ~WebConfiguratorPage() {}

protected:
    Template commonTemplate() const;

private:
    QString m_title;
};

class IndexPage : public WebConfiguratorPage
{
    Q_OBJECT
public:
    IndexPage() : WebConfiguratorPage("EDISON") {}

    virtual ~IndexPage() {}
public:
    virtual void handleRequest(HttpRequest &request, HttpResponse &response) override;
};

class FirstPage : public WebConfiguratorPage
{
    Q_OBJECT
public:
    FirstPage() : WebConfiguratorPage("First Page") {}

    virtual ~FirstPage() {}
public:
    virtual void handleRequest(HttpRequest &request, HttpResponse &response) override;
};

class SecondPage : public WebConfiguratorPage
{
    Q_OBJECT
public:
    SecondPage() : WebConfiguratorPage("Second Page") {}

    virtual ~SecondPage() {}
public:
    virtual void handleRequest(HttpRequest &request, HttpResponse &response) override;
};

#endif // WEBCONFIGURATORPAGE_H

WebConfiguratorPage.cpp

#include "webconfiguratorpage.h"
#include <QFile>
#include <QDebug>

WebConfiguratorPage::WebConfiguratorPage(const QString &title) :
    m_title(title)
{

}

Template WebConfiguratorPage::commonTemplate() const
{
    /* Для формирования основного шаблона используется файл common.htm.
     * В него устанавилвается название страницы ...
     * */
    QFile file(":/html/common.htm");
    Template common(file, QTextCodec::codecForName("UTF-8"));
    common.setVariable("Title", m_title);

    /* А также формируется меню.
     * Формирование меню сделано с учетом проверки на то,
     * требуется ли данное меню на странице или нет.
     * В данном примере меню будет на всех страницах, поэтому
     * просто обозначим необходимость данного меню.
     * Если вы посмотрите ниже содержимое файла common.htm, то
     * обнаружите там проверку на параметр "Navigation"
     * */
    bool navigation = true;
    common.setCondition("Navigation", navigation);
    if(navigation) {
        /* А само меню будет формироваться с помощью цилического добавления
         * пунктов, что также отражено специальной конструкцией в файле common.htm
         * */
        common.loop("Items", 3);
        common.setVariable("Items0.href", "/index.html");
        common.setVariable("Items0.name", "Main page");

        common.setVariable("Items1.href", "/first.html");
        common.setVariable("Items1.name", "First page");

        common.setVariable("Items2.href", "/second.html");
        common.setVariable("Items2.name", "Second page");
    }
    return common;
}

/* Далее идёт реализация обработчика запроса к каждой из страниц.
 * Фактически они идентичны в данном примере, но в реальном приложении
 * будут скорее всего отличаться по своей логике
 * */

void IndexPage::handleRequest(HttpRequest &request, HttpResponse &response)
{
    if (request.getMethod() == "GET")
    {
        // Получаем родительски щаблон страницы
        Template common = commonTemplate();
        QFile file(":/html/index.htm");
        Template contents(file, QTextCodec::codecForName("UTF-8"));
        /* После чего добавляем собственный контент из шаблона для данной страницы
         * в родительском шаблоне место для добавления информации, равно как и другого шаблона
         * в данном примере обозначено как {Content}
         * */
        common.setVariable("Content", contents);
        response.setHeader("Content-Type", "text/html; charset=ISO-8859-1");
        response.write(common.toUtf8());
        return;
    }
    else
    {
        return;
    }
    return;
}

void FirstPage::handleRequest(HttpRequest &request, HttpResponse &response)
{
    if (request.getMethod() == "GET")
    {
        Template common = commonTemplate();
        QFile file(":/html/first.htm");
        Template contents(file, QTextCodec::codecForName("UTF-8"));
        common.setVariable("Content", contents);
        response.setHeader("Content-Type", "text/html; charset=ISO-8859-1");
        response.write(common.toUtf8());
        return;
    }
    else
    {
        return;
    }
    return;
}

void SecondPage::handleRequest(HttpRequest &request, HttpResponse &response)
{
    if (request.getMethod() == "GET")
    {
        Template common = commonTemplate();
        QFile file(":/html/second.htm");
        Template contents(file, QTextCodec::codecForName("UTF-8"));
        common.setVariable("Content", contents);
        response.setHeader("Content-Type", "text/html; charset=ISO-8859-1");
        response.write(common.toUtf8());
        return;
    }
    else
    {
        return;
    }
    return;
}

Common.htm

Под занавес рассмотрим содержимое шаблонов.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>{Title}</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <link rel="stylesheet" type="text/css" href="style.css">
  <link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32"/>
</head>
<body>
<div class="content">
        <a href="http://edsd.ru"><div class="logo"></div><h1>{Title}</h1></a>
        {if Navigation}
                <ul class="menu">
                        {loop Items}
                                <li class = "menuitem">
                                        <a href={Items.href}>{Items.name}</a>
                                </li>
                        {end Items}
                </ul>
        {end Navigation}
        {Content}
</div>
</body>
</html>

index.htm

<h2>EDISON</h2>
<p>Центр разработки программного обеспечения</p>

Результат

В итоге получим рабочее приложение с веб-сервером, который отлично подойдет для встраиваемых систем.

А данное приложение сформирует следующую веб-страницу.
QtWebApp — пошаговый разжёванный пример с подробными комментариями - 3

Примечание

Проект приложения можно скачать по ссылке: скачать.
При сборке проекта обязательно поставьте этап install, чтобы необходимые статические файлы были установлены в соответствующую папку к исполняемому файлу.

Немного о баге

Добавим пару слов о баге, который сам по себе был больше похож на результат неудачного рефакторинга кода или, скорее, разработчик просто был в определенный момент уставший. Дело в том, что в более ранних версиях QtWebApp, а именно в версии 1.5.10, код был корректным и выглядел следующим образом.

if (data.size()==0 || file.error())

    {

        qCritical("Template: cannot read from %s, 

%s",qPrintable(sourceName),qPrintable(file.errorString()));

    } else {   

        append(textCodec->toUnicode(data));

    }

Тогда как в версии 1.6.3 была пропущена одна единственная строчка.

if (data.size()==0 || file.error())

    {

        qCritical("Template: cannot read from %s, 

%s",qPrintable(sourceName),qPrintable(file.errorString()));

        append(textCodec->toUnicode(data));

    }

В результате данные не добавлялись в шаблон страницы, и пользователь получал пустую страничку. Как сообщил Стефан Фрингс, разработчик QtWebApp, он обычно использует иной, нежели мы, подход к формированию веб-интерфейса, поэтому просто не замечал подобной проблемы.

Автор: Edison

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js