Гибридное приложение Qt + Javascript

Данный эпос, как и было обещано в статье Гибридное приложение PyQt + React, будет посвящён разработке гибридного приложения на чистом Qt и чистом же (никаких React!) javascript. Используя эти две статьи вы сможете самостоятельно создавать любые приложения на Qt/PyQt и Js/React (да хоть на Angular!). В качестве бандлера также можно использовать что угодно. В статье про React я использовал parcel, но нет никаких ограничений на использование чего угодно. Мы же переходим к созданию проекта.
Я буду использовать Qt 5.11.2 с компилятором MSVC2017 64 bit. У меня нет цели выяснить что изменилось в Qt 5.12, поэтому я просто буду использовать свою текущую версию. Также сразу скажу, что MinGW не поддерживается! При попытке собрать проект этим компилятором вы получите ошибку Project ERROR: Unknown module(s) in QT: webengine webenginewidgets
Для начала создаём простой проект Qt Widgets. В качестве базового класса будем использовать QWebEngineView
нет, поэтому файл формы необходимо открыть в QtDesigner. В дизайнере нужно выбрать и сделать QWebEngineView
виджет центральным. В результате получится примерно такая разметка:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
<?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> <class>MainWindow</class> <widget class="QMainWindow" name="MainWindow"> <property name="geometry"> <rect> <x>0</x> <y>0</y> <width>950</width> <height>590</height> </rect> </property> <property name="windowTitle"> <string>MainWindow</string> </property> <widget class="QWidget" name="centralWidget"> <layout class="QGridLayout" name="gridLayout"> <item row="0" column="0"> <widget class="QWebEngineView" name="webEngineView" native="true"> <property name="url" stdset="0"> <url> <string>about:blank</string> </url> </property> </widget> </item> </layout> </widget> <widget class="QMenuBar" name="menuBar"> <property name="geometry"> <rect> <x>0</x> <y>0</y> <width>950</width> <height>21</height> </rect> </property> </widget> <widget class="QToolBar" name="mainToolBar"> <attribute name="toolBarArea"> <enum>TopToolBarArea</enum> </attribute> <attribute name="toolBarBreak"> <bool>false</bool> </attribute> </widget> <widget class="QStatusBar" name="statusBar"/> </widget> <layoutdefault spacing="6" margin="11"/> <customwidgets> <customwidget> <class>QWebEngineView</class> <extends>QWidget</extends> <header location="global">QtWebEngineWidgets/QWebEngineView</header> </customwidget> </customwidgets> <resources/> <connections/> </ui> |
Мы больше не будем трогать Ui. В файле .pro
необходимо изменить строчку импортов так: QT += core gui webengine webenginewidgets
. Теперь проект можно собрать и увидеть пустое окно с центральным виджетом QWebEngineView
.
Создаём WebClass
. Как обычно, он должен быть потомком QObject
. В секции инклюдов в файле mainwindow.h
подключаем <QWebChannel>
и webclass.h
. В секции private
пишем:
1 2 |
WebClass *webobj; QWebChannel *channel; |
Дальше в конструктор класса (в файл mainwindow.cpp
после ui->setupUi(this);
) пишем:
1 2 3 |
webobj = new WebClass(); channel = new QWebChannel(this); channel->registerObject("server", webobj); |
Всё, мы соединили WebClass (webobj) с html (с именем «server»)! Время написать сам html.
Сам html-файл будет элементарным до одури. Связано это с тем, что полностью рабочее приложение, которое что-то делает мы уже писали, сегодня же чисто обучающая статья. Итак, index.html
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<!doctype html> <html lang="ru"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> </head> <body> <script type="text/javascript" src="https://getfirebug.com/firebug-lite.js"></script> <!-- Dev only --> <script src="qwebchannel.js"></script> <script src="qtconnector.js"></script> <h1> Привет, Qt!</h1> <fieldset> <legend>Вызов метода Qt из JS</legend> <button onclick="window.server.calledFromJs()">Нажми меня!</button> </fieldset> <hr/> <fieldset> <legend>Вызов метода Qt из JS с аргументом</legend> <label for="arg">Аргумент:</label> <input type="number" id="arg" name="arg" placeholder="10" step="1" /> <button onclick="window.server.calledFromJsWithArg(document.getElementById('arg').value)"> Нажми меня!</button> </fieldset> <hr/> <fieldset> <legend>Получение данных из Qt</legend> <p>Нажмите кнопку «Случайное число» — это QPushButton</p> <div>Случайное значение (генерируется c++): <span id="value"></span></div> </fieldset> <hr/> <fieldset> <legend>Полный круг</legend> <p>Нажатие кнопки отправит данные с аргументом в Qt, который вернёт факториал данного числа, вызвав сигнал factorial_changed. </p> <label for="arg2">Аргумент:</label> <input type="number" id="arg2" name="arg2" placeholder="10" step="1"/> <button onclick="window.server.factorial(document.getElementById('arg2').value)"> Нажми меня!</button> <div>Ответ Qt(факториал числа): <span id="value2"></span></div> </fieldset> </body> |
Пара комментариев:
- Мы снова подключаем firebug
qwebchannel.js
снова подключается самым первым. Читайте Гибридное приложение PyQt + React чтобы узнать где взять этот файл, если у вас его нет- Все наши связи в этот раз будут реализованы в файле
qtconnector.js
- В этот раз я не буду использовать jquery
В остальном же html говорит сам за себя. Мы сразу пишем методы-заглушки типа onclick="window.server.method()"
. Осталось только связать html и Qt. Для начала наши файлы index.html
, qwebchannel.js
и qtconnector.js
нужно положить в папку static
. Саму же папку со всем содержимым нужно поместить в путь %ПУТЬ/ДО/СБОРКИ/debug/%
. Если использовать систему ресурсов Qt, то у меня почему-то ничего не работало. Возможно что-то с моими мозгами, или же это баг. Как бы то ни было, мне приходится таскать папку со статикой ручками, иначе просто ничего не работает. Если у вас всё получится с помощью файлов ресурсов Qt, то вам легче. Теперь нужно уже соединить всё! В файле mainwindow.cpp
в конструктор дописываем
1 2 3 4 |
ui->webEngineView->page()->setWebChannel(channel); ui->webEngineView->page()->load(QUrl(QString("file:///%1/%2") .arg(QApplication::applicationDirPath()) .arg("static/index.html"))); |
Компилируем, запускаем и видим чудовищно медленную (отдельное спасибо firebug!) загрузку страницы! Почему всё работает так медленно — вопрос, но это так. Повторюсь ещё раз, я не делал тестов, но приложение PyQt стартует в разы быстрее (ну ладно, не в разы, но быстрее!), это видно даже не вооружённым глазом. Для меня загадка в чём же причина такой неспешности Qt, но сейчас это не так важно…
Пришло время проверить соединение между Qt и javascript. открываем файл qtconnector.js
и пишем
1 2 3 4 5 6 |
new QWebChannel(qt.webChannelTransport, (channel) => { var server = channel.objects.server; window.server = server; alert("Channel set"); }); |
Компилируем, запускаем и, если нигде не сделали ошибку, видим, что страница нам выдаёт alert с нашим текстом. Теперь пришло время реализовать четыре задачи, о которых мы написали ещё в html.
Содержание
Вызов метода Qt из javascript
Мы пойдём от простого в сложному и начнём с самого элементарного, то есть с простого вызова метода Qt из javascript. Создадим метод calledFromJs()
(да, не забудем подключить QMessageBox
). Важно помнить, что у нас в html уже́ есть заглушка типа onclick="window.server.method()"
1 2 3 4 5 6 7 8 9 10 11 |
// webclass.h ... public slots: void calledFromJs(); ... // webclass.cpp void WebClass::calledFromJs() { QMessageBox::information(nullptr, "calledFromJs","I'm called by js!"); } |
Компилируем, запускаем, нажимаем, получаем результат. Да, сто́ит напомнить, что метод в Qt должен называться также, как и в javascript!
Вызов метода Qt из javascript с аргументом
Не сложнее предыдущего. Опять же помним про то, что методы должны называться одинаково!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// webclass.h ... public slots: void calledFromJs(); // был написан выше! void calledFromJsWithArg(int arg); // для примера возьмём int ... // webclass.cpp void WebClass::calledFromJsWithArg(int arg) { QMessageBox::information(nullptr, "calledFromJsWithArg", QString("Привет, я — метод Qt, вызванный из js, мой аргумент — %1").arg(arg)); } |
Скомпилировав, можно убедиться в работоспособности. Тут нам на помощь придёт сам Qt и если мы не укажем аргумент, то он автоматически выполнит преобразование из ""
в 0
, то есть toInt()
. Дальше будет несколько сложнее.
Получение данных из Qt
Для начала создадим кнопку «Случайное число» и поместим её в toolbar:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// mainwindow.h ... #include <QPushButton> ... private: ... QPushButton *randBtn; ... // mainwindow.cpp MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ... randBtn = new QPushButton("Случайное число", this); ui->mainToolBar->addWidget(randBtn); connect(randBtn, &QPushButton::clicked, [this] () { webobj->value_changed(rand() % 100); }); ... } |
Мы сразу связываем кнопку с сигналом value_changed
в WebClass
. Переходим в webclass.h
и создаём наш сигнал:
1 2 3 4 5 |
... signals: ... int value_changed(int value); ... |
Больше в Qt нам ничего создавать не нужно, теперь по нажатию на кнопку будет вызываться сигнал value_changed
, который нам и нужно связать с javascript. Переходим к файлу qtconnector.js
и после строчки window.server = server;
пишем
1 |
server.value_changed.connect(updateattribute); |
Также нам необходимо реализовать updateattribute
. Сделаем это в самом начале файла:
1 2 3 4 |
const updateattribute = (value) => { document.getElementById("value").textContent = value; }; |
Мы просто находим элемент с id="value"
и вставляем туда случайное значение, полученной из Qt. Компилируем, проверяем. Вместо того, чтобы определять updateattribute
, можно использовать стрелочные функции, что мы сделаем в последнем нашем задании.
Полный круг
Банальная задача просто для того, чтобы увидеть как именно можно реализовать круговорот данных 🙂 В нашем html-файле есть заглушка onclick="window.server.factorial(document.getElementById('arg2').value)"
, которая даже если пользователь ничего не внёс, отработает как надо (несмотря на то, что в консоль вывалится сообщение Could not convert argument QJsonValue(string, "") to target type int .
) — опять же благодаря встроенному в Qt toInt
. Дальше, реализуем метод factorial
и создадим сигнал factorial_changed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// webclass.h ... signals: int value_changed(int value); // был реализован выше! int factorial_changed(int value); public slots: void calledFromJs(); // был написан выше! void calledFromJsWithArg(int arg); // был написан выше void factorial(int value); // не будем ничего возвращать ... // webclass.cpp void WebClass::factorial(int value) { auto f = [] (auto && self, int n) -> int {return n == 0 ? 1 : n * self(self, n - 1); }; emit factorial_changed(f(f, value)); } |
Тут всё относительно просто. Факториал определяется рекурсивно (и да, из-за того, что лямбда возвращает int
он может быть определён только для нескольких первых значений), после чего выполняется вызов сигнала. Осталось реализовать связь сигнала в javascript. И да, в этот раз я использую стрелочную функцию!
1 2 3 4 |
server.value_changed.connect(updateattribute); // было написано выше! server.factorial_changed.connect((value) => { document.getElementById("value2").textContent = value; }); |
Профит!
Прямой вызов javascript
Нам осталось ещё одна задача — прямой вызов javascript из Qt. Для этого сначала создадим ещё одну кнопку и добавим её на тулбар, после чего свяжем её с простым javascript alert.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// mainwindow.h ... #include <QDebug> ... private: ... QPushButton *randBtn, *directJS; // добавляем новую кнопку! ... // mainwindow.cpp MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ... connect(randBtn, &QPushButton::clicked, [this] () { webobj->value_changed(rand() % 100); }); // написали выше directJS = new QPushButton("Прямой вызов JS", this); ui->mainToolBar->addWidget(directJS); connect(directJS, &QPushButton::clicked, [this] () { ui->webEngineView->page()->runJavaScript("alert('Я вызвана напрямую из Qt!');", [](const QVariant &v) { qDebug() << v.toString(); }); }); ... } |
Примерно то же самое мы делали в python.
Таким образом мы написали полностью рабочее (хоть и бесполезное) гибридное приложение Qt и Javascript. Я не буду повторяться про плюсы и минусы таких приложений, скажу лишь, что в данный момент связка Qt/JS проигрывает electron только в одном (на мой сугубо субъективный взгляд) — скорость запуска. В остальном же, qtwebengine — это просто огонь!
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: