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.