Portál AbcLinuxu, 20. května 2025 20:14
Kterak propojit C++ a Web v praktické aplikaci.
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.
Tiskni
Sdílej:
QNetworkAccessManager
a ReportAccessManager
, nebo ještě lépe Report::NetworkAccessManager
.
CamelCase
... jdu blejt...
Díky za zápisek. Taky se chystám pustit do jednoho projektu s Qt(WebKitem), tak mě potěšilo, že tam jsou tyhle věci celkem jednoduché.
Ta VELKÁ písmena a podtržítka v názvech tříd se mi taky nelíbí, ale nevím, jaké jsou konvence v okolním kódu, tak to nechci hodnotit.
Mám ale jednu věcnou připomínku k návrhu: jak funguje třída REQUEST
(„…je naše třída pro přístup do databáze“)? To je nějaký singleton? Kde vezme spojení do databáze nebo jiné zdroje? IMHO by bylo lepší, aby je dostávala jako parametr z toho kontextu, ve kterém je používána, ne že si je někde obstará sama.
...proxy objekt...To mě zajímá. Nevyznám se příliš ve webových technologiích, nevím, co si pod tím představit. Jinak vlastní protokol používám právě proto, že nepředpokládám použití jinde, než v rámci aplikace. Navíc v režimu, kdy předem nevím, jak připojení k databázi vypadá a kdy jednu sestavu může používat více lidí s naprosto odlišným připojením k databázi.
/hello/world
namísto http://example.org/hello/world
či deko:/hello/world
. Pak je můžeš vzít jak jsou a poslat po HTTP do běžného prohlížeče.
No a aby fungovalo připojení k databázi, tak místo deko objektu, který máš teď, tam dáš proxy objekt, což je v Javascriptu implementovaný obyčejný objekt přeposílající volání metod na server a vracející obdržené odpovědi. Na serveru pak bude za nějakým HTTP API schovaný skutečný deko objekt, který udělá všechnu dřinu. Pokud proxy i originál budou mít stejné API, můžeš bezezměn zveřejnit tiskové sestavy na webu i v aplikaci a navíc to bude skoro bez práce (stačí udělat jen ten proxy objekt a aplikaci spustit jako démona bez GUI).
Pointa je v tom, aby jsi si teď nezadělal na problémy v budoucnu, kdy s tím budeš chtít něco dělat a budeš mít spoustu hotových sestav.
QWebView::setUrl("deko:/hello/world/");
by mělo stačit.
QWebView::setHtml( html, QUrl("deko:///hello/world") );
Html se bere z externího editoru
ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.