В процессе разработки приложения на Qt, может понадобиться добавить в данное приложение веб-интерфейс, что особенно может быть актуально при разработке встраиваемых систем с использованием Qt. Для решения данной задачи можно либо написать собственное решение, либо воспользоваться готовыми решениями. Например, библиотекой QtWebApp, которая предоставляет необходимый функционал для создания web-интерфейса.
К достоинствам данной библиотеки можно отнести:
- формирование страниц с динамическим содержанием по шаблонам;
- формирование полностью динамических страниц;
- работу с Cookie, что позволит добавить авторизацию на приложении;
- работу со статическими файлами, например, style.css или изображения;
- реализацию загрузки файлов.
Предлагаю подробно рассмотреть один из вариантов запуска небольшого приложения на 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 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>
Результат
В итоге получим рабочее приложение с веб-сервером, который отлично подойдет для встраиваемых систем.
А данное приложение сформирует следующую веб-страницу.
Примечание
Проект приложения можно скачать по ссылке: скачать.
При сборке проекта обязательно поставьте этап 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, он обычно использует иной, нежели мы, подход к формированию веб-интерфейса, поэтому просто не замечал подобной проблемы.
ссылка на оригинал статьи https://habrahabr.ru/post/280932/
Добавить комментарий