11.6.2014

foto Petr Bravenec

Petr Bravenec
Twitter: @BravenecPetr
+420 777 566 384
petr.bravenec@hobrasoft.cz

Ve firmě Hobrasoft vyvíjíme distribuovaný CRM systém Deko the CRM. Různých CRM systémů jsou na světě mraky, takže přijít s něčím novým je obtížné. Za výhodu našeho CRM považujeme nezávislost na připojení k internetu. Databáze CouchDB, na které je aplikace postavená, dovolí uživatelům pracovat offline a přitom nejsou nijak omezení ve sdílení dat. Deko je určené jak pro Windows, tak pro Linux.

Jedním z úkolů, před které nás potřeby aplikace postavily, byly tiskové sestavy. Celá aplikace je napsaná v C++ a Qt, takže se nabízela možnost použít C++ i pro tvorbu sestav. Ale tvořit něco v C++ je nepružné i pro nás a představa, že by si mohl uživatel sám vytvořit sdílenou knihovnu se sestavou je čirá utopie. Jak z takové situace ven?

Můj první nápad byl vlastní tabulkový kalkulátor vestavěný v aplikaci. Uživatelé jsou na tabulky zvyklí, nemuselo by jim to činit potíže. Malý průzkum bojem ale ukázal, že potíže by mohl činit tabulkový kalkulátor nám - naprogramovat něco použitelného je pracné.

Druhý nápad bylo použít HTML. Něco vzdáleně podobného už jsme měli hotové:

http://weko.hobrasoft.cz/timesheet/default/288KFTU

Ale to je napsané v PHP a potřebuje to celý ten veliký cirkus spojený s webovými aplikacemi - http server, php interpreter a napojení na databázi (u každého uživatele jiné).

Naštěstí k provádění nějakého programu ve webovém prohlížeči není potřeba PHP, webové prohlížeče už léta dokáží zpracovávat JavaScript a knihoven pro manipulaci HTML stránek je spousta. Spousta je i programátorů - na rozdíl od C++ dnes HTML a Javascript zvládá velké množství lidí.

Takže stačí už jen napsat a připojit k aplikaci webový prohlížeč. V Qt je situace jednoduchá: stačí přilinkovat webkit.

Sestavy obvykle čerpají své podklady z nějaké databáze. Databázi je proto nutné zpřístupnit i do webového prohlížeče. Prohlížeče získávají data dvojím způsobem: pomocí url, například:

http://hobrasoft.cz/cs/deko

nebo přes JavaScript, každý prohlížeč ve speciálním objektu document zpřístupňuje zobrazovanou html stránku:

var html = document.documentElement.outerHTML;

V aplikaci Deko jsme databázi zpřístupnili podobně. Jednak přes speciální url:

deko:///id-dokumentu-v-databazi

nebo přes objekt JavaScriptu:

var dokument = deko.get('id-dokumentu-v-databazi');

Vestavěný webkit se k tomu dá donutit poměrně snadno.

Speciální URL schéma deko

Třída WebView obsahuje webovou stránku, u níž musíme přepsat třídu QNetworkAccessManager, aby rozuměla i našemu schematu deko, zajistí to pár řádků kódu:

QNetworkAccessManager *om = f_view->page()->networkAccessManager();
REPORT_access_manager *nm = new REPORT_access_manager(om, this);
f_view->page()->setNetworkAccessManager(nm);

Původní QNetworkAccessManager je nahrazený naším vlastním. Nedělá nic jiného, než že ověří url schema a pokud je schéma deko, vytvoří vlastní odpověď, jinak zavolá standardní proceduru:

class REPORT_access_manager : public QNetworkAccessManager {
    Q_OBJECT
  public:
    REPORT_access_manager(QNetworkAccessManager *, QObject *);
    QNetworkReply *createRequest( QNetworkAccessManager::Operation, 
                                  const QNetworkRequest&, 
                                  QIODevice*);
};

REPORT_access_manager::REPORT_access_manager(
                QNetworkAccessManager *manager, 
                QObject *parent) : QNetworkAccessManager(parent) {
    setCache        (manager->cache());
    setCookieJar    (manager->cookieJar());
    setProxy        (manager->proxy());
    setProxyFactory (manager->proxyFactory());
}

QNetworkReply *REPORT_access_manager::createRequest(
        QNetworkAccessManager::Operation operation, const QNetworkRequest &request,
        QIODevice *device) {
    if (request.url().scheme() != "deko") {
        return QNetworkAccessManager::createRequest(operation, request, device);
        }

    if (operation != GetOperation) {
        return QNetworkAccessManager::createRequest(operation, request, device);
        }

    return new REPORT_reply (request.url());
}

Vrácená odpověď je mírně rozšířená třída QNetworkReply

class REPORT_reply : public  QNetworkReply {
    Q_OBJECT
  public:
    REPORT_reply(const QUrl∓);
    void    abort() {} ;
    qint64  bytesAvailable() const;
    bool    isSequential() const { return true; }
  protected:
    qint64 readData(char *data, qint64 maxSize);

  private:
    QByteArray content;
    qint64 offset;
};

REPORT_reply::REPORT_reply(const QUrl& url) {
    offset = 0;
    open(ReadOnly | Unbuffered);

    // REQUEST je naše třída pro přístup do databáze, vrací obvykle JSON řetězec
    // Při vaší vlastní implementaci sem doplňte vlastní přístup do databáze
    REQUEST rq;
    rq.setBinary(true);
    rq.get(url.path().toUtf8());
    content = rq.data();

    setHeader(QNetworkRequest::ContentTypeHeader, rq.contentType().toString());
    setHeader(QNetworkRequest::ContentLengthHeader, QVariant(content.size()));

    QTimer::singleShot(0, this, SIGNAL(metaDataChanged()));
    QTimer::singleShot(0, this, SIGNAL(readyRead()));
    QTimer::singleShot(0, this, SIGNAL(finished()));
}

qint64 REPORT_reply::bytesAvailable() const {
    qint64 bc = content.size() - offset;
    return bc;
}

qint64 REPORT_reply::readData(char *data, qint64 maxSize) {
    if (offset < content.size()) {
        qint64 number = qMin(maxSize, content.size() - offset);
        memcpy(data, content.constData() + offset, number);
        offset += number;
        return number;
    } else {
        return -1;
        }
}

Objekt deko v JavaScriptu

K webové stránce zobrazené ve webkitu lze snadno připojit libovolný QObject:

QWebFrame *frame = f_view->page()->mainFrame();
frame->addToJavaScriptWindowObject("deko", m_report_script);

Pod jménem deko bude objekt m_report_script přístupný ve webové stránce pomocí JavaScriptu.

U tohoto objektu uvedu pouze deklaraci, samotný kód už není tak důležitý:

class REPORT_SCRIPT : public QObject {
    Q_OBJECT
  public:
    REPORT_SCRIPT(QObject *parent);

    Q_INVOKABLE QString     id();
    Q_INVOKABLE QVariant    get(const QString& id);
    Q_INVOKABLE QVariant    document(const QString& id);
    Q_INVOKABLE QVariant    linksToMe(const QString& id);
    Q_INVOKABLE QVariant    linksFromMe(const QString& id);
    Q_INVOKABLE QString     hash(const QString& text);
    Q_INVOKABLE void        begin() { emit jobBegin(); }
    Q_INVOKABLE void        end()   { emit jobEnd(); }
    Q_INVOKABLE QString     userid();

  signals:
    void        jobBegin();
    void        jobEnd();

};

Makrem Q_INVOKABLE deklaruji metodu jako přístupnou z JavaScriptu. Zajímavé je předávání výsledné hodnoty (podobně lze předávat i parametry). Vrací-li metoda QVariant, použije se v JavaScriptu taková hodnota jako objekt. V C++ vypadá vytvoření takového objektu například takto:

QVariantMap data;
data["_id"] = "id-meho-objektu";
data["name"] = "Jmeno objektu";

QVariantList list;
list << "abcd" << "1234";
data["list"] = list;

return data;

V Javascriptu se interpretuje stejně, jako by se interpretoval tento JSON literár:

{ "_id": "id-meho-objektu", "name": "Jmeno objektu", "list": [ "abcd", "1234"] }

V Javascriptu je použití snadné:

var x = deko.metoda();
var id = x._id;
var name = x.name;
for (var i=0; i<x.list.length; i++) {
    // Udělej něco
    neco( deko.list[i] );
    }

Jak může vypadat výsledek

Nakonec několik ukázek: Vestavěná google mapa, hotová sestava a kus zdrojového tvaru sestavy. S webkitem dostanete i luxusní debugger - ten je vidět na posledním obrázku.

Hobrasoft s.r.o. | Kontakt