Pull to refresh

QtWebKit-специфика при разработке мобильных HTML5-приложений

Reading time9 min
Views29K
Этот пост участвует в конкурсе „Умные телефоны за умные посты

Введение


Не секрет, что с появлением HTML5, фокус разработки постепенно стал смещаться в сторону Web. Это и простые Web-сайты и динамические приложения и даже мобильные приложения, целиком и полностью написанные с использованием HTML5. Независящие от платформы и среды исполнения и требующие лишь беспрекословного выполнения стандартов. Но тем не менее, как бы ни был хорош весь стек технологий привнесённых новыми стандартами HTML, всё ещё остаются некоторые задачи, для решения которых необходимо использовать нативные средства разработки.

Такими проблемами к примеру, является получение доступа к системной информации, управление и изменение чего-либо в системе. Доступ из HTML5 к контактам, календарю, органайзеру на мобильном устройстве и другие. Опять же, если наше приложение производит какие-то серьёзные вычисления, то их можно перенести с медленного JS на быстрый C++. В данной статье хочу рассмотреть несколько техник взаимодействия Web-приложения и нативного кода на примере модуля QtWebKit, которые могут оказаться полезными.

Оглавление


  • Вызов C++-методов из JavaScript
  • Вызов JavaScript-кода из C++-методов
  • Интеграция QWidget в Web-страницу

Вызов C++-методов из JavaScript


Теоретически, в данном случае нам необходимо добавить на HTML-страницу новый JS-объект, через который мы смогли бы иметь доступ к одному из наших классов. QtWebKit предоставляет возможность внедрять подобные объекты в Web-страничку, но с одним условием — добавление нового объекта необходимо совершать каждый раз, когда мы перезагружаем страничку в нашем компоненте браузера. То есть каждый раз при очищении как DOM-дерева, так и дерева JS-объектов.

На практике, нам необходимо в Qt Creator создать новое «Мобильное приложение Qt», профиль Qt Simulator, остальное по вкусу. После чего на форму бросить компонент QWebView, в .pro файле добавить строчку

QT += webkit

в заголовке mainwindow.h объявить один приватный и один публичный слоты:
private slots:
        void addJSObject();
public slots:
    void webViewQuitClicked ();

А в файле реализации mainwindow.cpp включить заголовок QWebFrame, изменить немного конструктор и описать реализации для добавленных в mainwindow.h слотов:
...
#include <QWebFrame>
#include <QMessageBox>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    connect(ui->webView->page()->mainFrame(), SIGNAL(javaScriptWindowObjectCleared()),
            this, SLOT(addJSObject()));
}

void MainWindow::addJSObject() {
    ui->webView->page()->mainFrame()->addToJavaScriptWindowObject(QString("mainWindow"), this);
}

void MainWindow::webViewQuitClicked ()
{
    qApp->quit();
}
...

А теперь разберёмся с этим кодом:
  • Приватный слот addJSObject() — отвечает за добавление к каждой новой страничке JS-объекта, указывающего на текущую форму. При этом, вызывается он будет каждый раз, когда Web-страница будет перезагружена. За это отвечает сигнал javaScriptWindowObjectCleared(). Как видно из кода, на Web-страничке наш объект будет иметь имя «mainWindow».
  • Публичный слот webViewQuitClicked () является как раз тем слотом, который будет доступен нам из нашего JavaScript-кода, расположенного на Web-странице. Естественно по аналогии можно добавлять другие методы, которые затем вызывать из JS.

Идём далее. Теперь нам необходимо попробовать вызвать данный код из нашей Web-странички. Для этого, создаём HTML-файл примерно следующего содержания:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>Application Example</title>

  <script>

    function quitApplication() {
      mainWindow.webViewQuitClicked ();
    }

  </script>

</head>
  <body>
    <a href="#" onclick='quitApplication();'>Quit Application</a>
  </body>
</html>

И капельку правим конструктор приложения, а именно добавляем в него одну строчку:
ui->webView->load("file:///"+QApplication::applicationDirPath() + "/../../html5caller/example.htm");

Конечно же, загрузить страничку можно и откуда-нибудь из интернета.
Ага, неплохо. А что же делать, если мы хотим передавать некоторый параметр в C++ функцию? Нет ничего проще! Просто описываем в C++-коде новый public-слот с желаемыми параметрами. Например — вывод сообщения пользователю:
mainwindow.h:
public slots:
    void webViewQuitClicked ();
    void webViewShowMessageClicked (QString message, int number);

mainwindow.cpp:
...
void MainWindow::webViewShowMessageClicked (QString message, int number)
{
    QMessageBox::information(this, "From Web", message+QString::number(number));
}
...

И поменяем example.htm:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>Application Example</title>

  <script>

    function quitApplication() {
      mainWindow.webViewQuitClicked ();
    }

    function calculate(val1, val2) {
      result = eval(parseFloat(document.calc.val1.value)+parseFloat(document.calc.val2.value))
      mainWindow.webViewShowMessageClicked ("Result: ", result);
    }
  </script>

</head>
  <body>
    <a href="#" onclick='quitApplication();'>Quit Application</a>

    <FORM name="calc">
      <input name="val1" type="text" value="3" size="4">+
      <input name="val2" type="text" value="4" size="4">
      <input type="button" value="  +  " onclick="calculate(val1, val2)">
    </FORM>
  </body>
</html>

Теперь, мы можем не только закрывать приложение, но и передавать в него какие-либо параметры, причём с нормальным контролем типов данных. И вот что получилось:

Ну и последнее. что можно добавить по этой теме. Допустим. мы отправляем в C++ какие-то данные, там они обрабатываются и мы хотим получить их обратно. Для этого мы просто описываем возвращаемый тип данных в нашем слоте, например:
...
QString readResult(int val1, int val2) { 
...
return QString();
}
...

И из JS-кода вызываем этот метод как и остальные, с той лишь разницей, что мы задаём переменную, куда хотим сложить возвращаемое значение:
var lolo = mainWindow.readResult();

Вот так то… Идём дальше…

Вызов JavaScript-кода из C++-методов


На этом мы долго останавливаться не будем. Для вызова JS-кода, мы должны использовать методы класса QWebFrame. Вот пример: нам необходимо получить url всех картинок на странице:
QVariant listOfImages = webView.page()->mainFrame()->evaluateJavaScript("document.getElementsByTagName(\"img\").length;");
	double numberOfImages = listOfImages.toDouble();

for (double i = 0; i < numberOfImages; ++i)
	{
		srcOfImages = webView.page()->mainFrame()->evaluateJavaScript(QString("document.getElementsByTagName(\"img\")[%1].src;").arg(i));
		imageUrl = srcOfImages.toString();
	}

Сначала мы получаем количество тегов типа img, а затем проходимся по этому списку и вытягиваем так необходимые нам URL. Как видно из примера — мы с лёгкостью можем обращаться к любым объектам на странице, а соответственно и исполнять на ней любые методы.

Интеграция QWidget в Web-страницу


Это вторая большая тема, о которой мне хотелось бы поговорить. Речь пойдёт о несовсем стандартной процедуре… Мы действительно включим в нашу веб-страничку некоторый виджет из нашего приложения.

Это можно сделать при помощи класса QWebPluginFactory. Приступим. Первым делом, в Qt Creator, в дереве проектов слева, кликаем правой кнопкой мыши по нашему проекту. Из контактстного меню выбираем пункт «Добавить новый» -> Класс C++. Имя класса MyWidgetFactory, базовый класс QWebPluginFactory, тип QObject, далее, завершить. Для того чтобы наш плагин стал полноценным, нам необходимо переопределить два метода базового класса:
QObject *create(const QString &mimeType,
                const QUrl &url, const QStringList &argumentNames,
                const QStringList &argumentValues) const;
QList<QWebPluginFactory::Plugin> plugins() const;

Первый — занимается созданием виджетов по требованию Web-страниц. Второй — описывает Mime-типы, на которые должен реагировать плагин и для которых необходимо создавать новые виджеты. Вообще, изначально, я использовал подобный подход пару лет назад, когда писал мессенджер для Vkontakte.ru. Тогда мне нужно было встроить в страничку виджет музыкального проигрывателя, для того, чтобы пользователям проще было делиться музыкой. Посмотреть его полную реализацию можно вот здесь code.google.com/p/vimka/source/browse/#svn%2Ftrunk%2Fchats в файлах vkmediafactory.h и vkmediafactory.cpp

Мы же сейчас сделаем что-нибудь попроще. Итак, давайте для начала изменим получившийся шаблон класса.
mywidgetfactory.h:
#ifndef MYWIDGETFACTORY_H
#define MYWIDGETFACTORY_H

#include <QWebPluginFactory>

class MyWidgetFactory : public QWebPluginFactory
{
    Q_OBJECT
public:
    explicit MyWidgetFactory(QObject *parent = 0);

    QObject *create(const QString &mimeType,
                    const QUrl &url, const QStringList &argumentNames,
                    const QStringList &argumentValues) const;
    QList<QWebPluginFactory::Plugin> plugins() const;

signals:

public slots:

};

#endif // MYWIDGETFACTORY_H

mywidgetfactory.cpp:
#include "mywidgetfactory.h"
#include <QTabWidget>
#include <QPushButton>
#include <QProgressBar>
#include <QDebug>
#include <QApplication>
#include <QUrl>

MyWidgetFactory::MyWidgetFactory(QObject *parent) :
    QWebPluginFactory(parent)
{
}

QList<QWebPluginFactory::Plugin> MyWidgetFactory::plugins() const
{
    QWebPluginFactory::MimeType mimeType;
    mimeType.name = "text/qtexample";
    mimeType.description = "Comma-separated values";
    mimeType.fileExtensions = QStringList() << "txt";

    QWebPluginFactory::Plugin plugin;
    plugin.name = "PluginFactory Example";
    plugin.description = "PluginFactory Example";
    plugin.mimeTypes = QList<MimeType>() << mimeType;

    return QList<QWebPluginFactory::Plugin>() << plugin;
}

QObject *MyWidgetFactory::create(const QString &mimeType,
                                const QUrl &url, const QStringList &argumentNames,
                                const QStringList &argumentValues) const
{
    qDebug() << mimeType << url << argumentNames << argumentValues;

    if (mimeType != "text/qtexample")
        return 0;

    QTabWidget *tab = new QTabWidget();

    int max = argumentValues.last().split(",").first().toInt();
    int val = argumentValues.last().split(",").last().toInt();

    QProgressBar *progressBar = new QProgressBar();
    tab->addTab(progressBar, "Progress");
    progressBar->setMaximum(max);
    progressBar->setValue(val);

    QPushButton *pb = new QPushButton("Click me!");
    tab->addTab(pb, "Button");
    connect(pb, SIGNAL(clicked()),QApplication::instance(), SLOT(quit()));

    return tab;
}

Что же мы натворили? В функции plugins() мы описали Mime-тип, на который по идее хотели бы реагировать. А затем описали сам плагин, который будет мониторить все загружаемые странички на наличие данного Mime-типа и при его обнаружении вызывать метод create() а затем получившийся виджет встраивать в саму страницу.

Далее, в методе create() мы принимаем и выводим в консоль данные, проверяем соответствие mime-типа нужному нам (text/qtexample) и возвращаем не пустой виджет, если mime-типы совпадают. Чтобы было чуть более наглядно, вот консольный вывод принимаемых данной функцией данных:
"text/qtexample"  QUrl( "file:///D:/devel/html5caller/100,28" )  ("type", "data", "width", "height", "src") ("text/qtexample", "100,28", "300", "300", "100,28") 

Это что касается фабрики…

Следующим шагом стоит рассказать нашему приложению о наличии нашей собственной фабрики плагинов. Для этого, добавим для начала в файл mainwondow.cpp два новых заголовка:
#include <QWebSettings>
#include "mywidgetfactory.h"

И поменяем конструктор данного класса:
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent), ui(new Ui::MainWindow)
{
    QWebSettings::globalSettings()->setAttribute(
                QWebSettings::PluginsEnabled, true);
    ui->setupUi(this);

    connect(ui->webView->page()->mainFrame(), SIGNAL(javaScriptWindowObjectCleared()),
            this, SLOT(addJSObject()));

    MyWidgetFactory *factory = new MyWidgetFactory(ui->webView);
    ui->webView->page()->setPluginFactory(factory);

    ui->webView->load("file:///"+QApplication::applicationDirPath() + "/../../html5caller/example.htm");
}

Здесь мы перво-наперво в глобальных настройках QtWebKit-а включили поддержку плагинов. После чего создали объект нашего Factory-класса и установили его как Plugin Factory для нашего объекта QWebPage относящегося к нашему webView.

Остаётся последний, заключительный штрих — необходимо как-то встроить наш плагин в web-cnhfybxre/ Для этого откроем example.htm и допишем после описания нашей формы вставим такую строчку:
<object type="text/qtexample" data="100,28" width="300" height="300"></object>

Как несложно догадаться, здесь мы указываем mime-тип для того чтобы созданный плагин на нас отреагировал, задаём размеры будущего виджета и передаём немного данных для инициализации.

Итогом нашей работы будет являться вот такое окошко:

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

Вот в общем то и всё, о чём я хотел бы рассказать на этот раз. Исходники приложения можно взять вот здесь и использовать как пример: code.google.com/p/html5cppmixer/downloads/list

Надеюсь кому-то мой опыт пригодится и окажется полезным.
Tags:
Hubs:
Total votes 22: ↑12 and ↓10+2
Comments21

Articles