QtWebApp — покроковий розжований приклад з докладними коментарями



У процесі розробки додатка на 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, а два інших – це зображення.

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



Проект буде сформований у вигляді 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 Unix and 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("порту", value("порту", 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>

Результат

В результаті отримаємо робочий додаток з веб-сервером, який відмінно підійде для вбудованих систем.

А ця програма сформує наступну веб-сторінку.

Примітка

Проект програми можна завантажити за посиланням: завантажити.
При складанні проекту обов'язково поставте етап 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, він зазвичай використовує інший, ніж ми, підхід до формування веб-інтерфейсу, тому просто не помічав подібної проблеми.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.