Integrare contenuti web in un’applicazione desktop Qt/C++
Introduzione
Quando ci troviamo ad affrontare lo sviluppo di un’applicazione desktop, spesso incontriamo esigenze particolari che possono presentare sfide significative.
Potremmo trovarci di fronte alla necessità di implementare interfacce sofisticate che vanno oltre le capacità della nostra libreria GUI, o gruppi di funzionalità che richiedono numerose interazioni con un back-end web.
In generale, alcuni tipi di funzionalità possono risultare complesse da gestire in ambito desktop.
Fortunatamente ci vengono in aiuto alcune tecnologie web, che, rispetto a quelle desktop, possono offrire maggiore flessibilità e ridurre notevolmente le difficoltà e i tempi di sviluppo, con un risultato finale comunque indistinguibile dalle interfacce grafiche native desktop.
Caso d’uso
Vogliamo raccontarvi, in questo blog post, un caso di sviluppo recente, che ci ha posto di fronte ad alcuni dei problemi sopra citati.
Abbiamo avuto l’opportunità di creare da zero un’applicazione desktop utilizzando il framework Qt. In particolare, la nostra sfida consisteva nell’accogliere alcune richieste speciali, che andavano oltre l’uso consueto dei Qt Widgets C++ da parte del cliente.
Durante lo sviluppo, ci siamo confrontati con due principali problematiche:
- Creazione di un’interfaccia utente personalizzata, non conforme a quella di sistema: in alcuni casi, i Qt Widgets non soddisfacevano a pieno le nostre esigenze di design.
- Gestione di funzionalità generalmente più adeguate a tecnologie web: alcune parti dell’applicazione richiedevano l’implementazione di un sistema di autenticazione utente e la gestione di numerose richieste verso il server.
Queste avrebbero complicato lo sviluppo, aumentando anche i tempi di lavorazione.
Quindi, abbiamo riflettuto sulle possibili soluzioni e, nel nostro caso, sembrava particolarmente conveniente sviluppare alcune parti con tecnologie web-based. Questa implementazione ibrida poneva, a sua volta, nuove sfide:
- Integrare dei contenuti web dentro un’applicazione Qt Widget desktop
- Far comunicare in modo bidirezionale le due applicazioni (Qt e web)
- Garantire uniformità nell’interfaccia e nell’esperienza
Dopo una fase di sperimentazione, abbiamo potuto integrare le due tecnologie, sfruttando alcuni strumenti del framework Qt.
Qt WebEngine
Le funzionalità del Qt WebEngine facilitano l’integrazione di contenuti web dentro un’applicazione Qt Widgets o Qt Quick.
L’architettura del Qt WebEngine è suddivisa in 3 principali moduli:
- Qt WebEngine Widget, per applicazioni basate su Widgets
- Qt WebEngine, per applicazioni basate su Qt Quick
- Qt WebEngine Core, per interagire con Chromium
Nota: Il modulo Qt WebEngine Core fornisce una API condivisa sia da QtWebEngineWidgets, sia da QtWebEngineQuick.
Per la nostra app desktop Qt Widgets, abbiamo usato il modulo Qt WebEngine Widget, che è organizzato come segue:
Dentro la view è contenuta una web engine page, che gestisce la history dei link navigati e le action. Tutte le page appartengono al web engine profile, che contiene dati condivisi come i setting, gli script e i cookie.
Qt WebEngine Widget
La view è stata il fulcro nei nostri sviluppi ed è fornita da QWebEngineView, ovvero un widget che può essere utilizzato per caricare e mostrare contenuti web.
Vediamo un esempio di come possiamo utilizzare questo widget:
auto webView = new QWebEngineView(this);
webView->page()->setWebChannel(webChannel);
webView->setUrl(QUrl("qrc:/web_pages/library.html"));
Questo codice crea un QWebEngineView (prima linea) e carica un Url a nostro piacere (ultima linea). Nello specifico, qui stiamo usando una pagina html dal Qt Resource System. A questo punto siamo in grado di mostrare una pagina web.
Come abbiamo anticipato, nel nostro progetto è emersa la necessità di far comunicare bidirezionalmente le applicazioni Qt e web. La seconda linea dello snippet sopra suggerisce la soluzione: il Qt web channel.
Qt web channel
La classe QWebChannel colma il gap tra un’applicazione Qt/C++ e una HTML/Javascript. Uno dei maggiori vantaggi è che questa classe ci evita di dover gestire manualmente lo scambio e la serializzazione di messaggi.
La prima cosa da fare, lato Qt, è registrare uno o più QObject nel QWebChannel:
auto bookLending = new BookLending(this);
auto webChannel = new QWebChannel(this);
webChannel->registerObject("bookLending", bookLending);
Qui creiamo un oggetto BookLending e lo registriamo dentro il web channel; quest’ultimo lo associamo alla web view, come abbiamo visto nella sezione precedente “Qt WebEngine Widget”.
Invece, lato HTML/JS, la prima cosa da fare è includere nel codice il file qwebchannel.js, messo a disposizione da Qt:
<script
type="text/javascript"
src="qrc:///qtwebchannel/qwebchannel.js"
></script>
Per ogni QObject registrato, viene automaticamente creato un object JS che ne rispecchia le API.
// Create QWebChannel instance and provide the callback to handle the bookLending object API.
new QWebChannel(qt.webChannelTransport, function (channel) {
output("Connected to WebChannel, ready to send/receive messages!");
printBooks(books);
// "bookLending" is the object registered to the QWebChannel (in the Qt app).
// Here we make the API object accessible globally.
window.bookLending = channel.objects.bookLending;
A questo punto, l’applicazione web è in grado di utilizzare properties, segnali e funzioni/slot pubblici.
Esempio di comunicazione Qt/web
Entriamo nel vivo della comunicazione Qt/web, vedendo più nel dettaglio l’esempio di codice accennato nelle sezioni precedenti.
Questo esempio è simile al caso d’uso originario ed è un’applicazione Qt stand-alone, che simula l’interazione bidirezionale tra un contenuto web HTML/JS (la biblioteca) e l’applicazione desktop Qt (la collezione personale di libri di un certo utente).
Il contenuto web potrebbe interagire con un ipotetico back-end server, per ottenere dati d’interesse, ed è integrato all’interno dell’applicazione desktop.
L’immagine sopra mostra l’applicazione Qt in esecuzione: all’interno del pannello “Library web view” è contenuta la pagina HTML/JS della biblioteca.
Le funzionalità dell’applicazione si possono riassumere in due gruppi:
- Applicazione Qt
- permette di richiedere o restituire un libro alla biblioteca
- mostra la libreria dell’utente, dove vengono aggiunti i libri presi in prestito e rimossi quelli restituiti
- può ricevere e mostrare messaggi in arrivo dalla biblioteca
- Pagina web della biblioteca
- mostra e aggiorna, in base alle richieste lato Qt, il catalogo dei libri appartenenti alla biblioteca
- mostra il log di prestiti e restituzioni
- può anche mandare messaggi all’applicazione Qt, sia automatici, sia personalizzati, attraverso la barra di input in fondo alla pagina web
Si può quindi osservare come le due tecnologie sappiano comunicare e gestire i dati in modo bidirezionale. Inoltre, hanno un’interfaccia grafica uniforme tra loro.
Vediamo il codice che permette questa comunicazione, partendo dalle API Qt, contenute nel nostro QObject di esempio BookLending ed esposte, poi, attraverso il QWebChannel:
public slots:
// This slot can be called by the web app to send a book to the Qt app.
void sendBook(const QString &title, const QString &author) {
Book newBook{title, author};
emit bookReceived(newBook);
}
// This slot can be called by the web app to take back a book from the Qt app.
void takeBackBook(const QString &title, const QString &author) {
Book book{title, author};
emit bookReturned(book);
}
// This slot can be called by the web app to send a text message to the Qt app.
void sendMessage(const QString &message) { emit messageReceived(message); }
signals:
// Signals for web app.
void lendingRequested(const QString &bookData);
void returnRequested(const QString &bookData);
// Signals for Qt app.
void bookReceived(Book newBook);
void bookReturned(Book book);
void messageReceived(const QString &message);
Come usiamo queste API
- Il segnale
lendingRequested()
viene emesso lato Qt per richiedere un libro in prestito alla biblioteca.
// Forward book lending request to the library.
connect(bookshelfWidget, &BookshelfWidget::lendingRequested, bookLending, &BookLending::lendingRequested);
- L’app web si connette al segnale per gestire la richiesta.
bookLending.lendingRequested.connect(function (bookData) {...}
- Lo slot
sendBook()
viene chiamato lato web per “consegnare” un libro all’app Qt.
bookLending.sendBook(found.title, found.author);
- Il segnale
returnRequested()
viene emesso lato Qt per restituire un libro alla biblioteca.
// Forward book return request to the library.
connect(bookshelfWidget, &BookshelfWidget::returnRequested, bookLending, &BookLending::returnRequested);
- L’app web si connette al segnale.
bookLending.returnRequested.connect(function (bookData) {...}
- Lo slot
takeBackBook()
viene chiamato lato web per “riprendere” un libro dall’app Qt.
bookLending.takeBackBook(found.title, found.author);
- Lo slot
sendMessage()
viene chiamato lato web per mandare un messaggio all’app Qt.
bookLending.sendMessage("Couldn't find the requested book!");
- I segnali
bookReceived()
,bookReturned()
emessageReceived()
vengono emessi e gestiti solo ed esclusivamente nell’app Qt.
// When we get a book from the library, we add it to the user book list (i.e.
// the bookshelf).
connect(bookLending, &BookLending::bookReceived, books, &BookListModel::addBook);
// When we return a book to the library, we remove it from the user book list.
connect(bookLending, &BookLending::bookReturned, books, &BookListModel::removeBook);
// Show messages from the library.
connect(bookLending, &BookLending::messageReceived, this, &MainWindow::showMessage);
Altre curiosità
Qt Resource System e Qt WebEngine
L’applicazione HTML/JS ha la possibilità di accedere ai file nel Resource System Qt.
Nel nostro esempio, abbiamo inserito delle icone in formato .svg e le abbiamo potute utilizzare all’interno della nostra biblioteca web
if (typeof qt !== "undefined") {
// We can use icons from Qt app resources.
status_img.src = "qrc:///icons/connected.svg";
status_txt.innerHTML = "Connected";
...
} else {
output("Qt web channel NOT connected");
status_img.src = "qrc:///icons/not-connected.svg";
status_txt.innerHTML = "NOT Connected";
}
QWebEngineView debug
Le web view Qt possono essere ispezionate usando i Developer Tools: questo ci permette di fare debug su qualsiasi QWebEngineView (Qt WebEngine Debugging).
Di seguito, riassumiamo alcuni passi per fare debug usando QtCreator:
- Tab Projects sulla sinistra > sezione Build & Run > Run > Environment
- Aggiungere nuova variabile con chiave:
QTWEBENGINE_CHROMIUM_FLAGS
e valore:--remote-debugging-port=<port_number>
- Fare run dell’app Qt da Qt Creator
- Aprire un browser all’URL dell’app web, esempio:
http://localhost:<port_number>/
- Nella lista di view ispezionabili, selezionare la pagina desiderata
Conclusione
L’integrazione Qt/web si è dimostrata un successo e siamo riusciti a soddisfare tutti i requisiti: gli utenti non percepiscono una sensazione di discontinuità nell’uso dell’applicazione, nonostante alcune parti siano web e alcune desktop.
Grazie all’uso del QWebChannel, abbiamo creato un flusso di comunicazione bidirezionale, che garantisce dati sempre aggiornati in entrambi i contesti.
Nonostante il tempo impiegato nell’integrazione Qt/web, l’uso di una tecnologia web ci ha permesso di ridurre notevolmente i tempi di sviluppo per tutte quelle funzionalità che sarebbero state complesse da implementare solo con tecnologie desktop. Questo, anche grazie al fatto che è stato possibile far lavorare in parallelo due team, Qt e web, come nel caso di un’infrastruttura a microservizi.
Inoltre, con lo sviluppo modulare, siamo in grado di integrare nuove funzionalità con estrema semplicità, sfruttando al massimo le potenzialità offerte dagli strumenti web.
È stata un’esperienza davvero stimolante consentire a team con background tecnologici diversi di collaborare e concepire un’integrazione che non sempre viene presa in considerazione.
Puoi trovare il codice dell’esempio qui: https://github.com/develer-staff/qt_web_example