11.6.2014
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:
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.
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; } }
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] ); }
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.