Integrare contenuti web in un’applicazione desktop Qt/C++

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:

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:

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: 

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:

Organizzazione Qt WebEngine Widget

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.

Esempio di comunicazione Qt/web

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:

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

// Forward book lending request to the library.
connect(bookshelfWidget, &BookshelfWidget::lendingRequested, bookLending, &BookLending::lendingRequested);
bookLending.lendingRequested.connect(function (bookData) {...}
bookLending.sendBook(found.title, found.author);
// Forward book return request to the library.
connect(bookshelfWidget, &BookshelfWidget::returnRequested, bookLending, &BookLending::returnRequested);
bookLending.returnRequested.connect(function (bookData) {...}
bookLending.takeBackBook(found.title, found.author);
bookLending.sendMessage("Couldn't find the requested book!");
// 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:

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