Spendi Meno Quanto Basta Parte 4

In questa puntata diamo una rifinita al visualizzatore degli scontrini. Ora vorrei chiudere la partita e poter vedere lo scontrino ingrandito prima di passare all’inserimento dei dati.

Nella prima parte di Spendi Meno Quanto Basta abbiamo creato lo scheletro dell’applicazione, nella seconda parte abbiamo aggiunto la pagina delle impostazioni, nella terza abbiamo visto come sfogliare gli scontrini fiscali ora sistemiamo alcuni difetti e visualizziamo lo scontrino corrente nel modo corretto, pronti per copiare i dati ed inserirli.

Cosa andremo ad implementare?

In questa puntata vedremo come:

  • Visualizzare correttamente la prima immagine all’avvio dell’applicazione.
  • Visualizzare correttamente le immagini se modifichiamo il percorso nelle impostazioni.
  • Mostrare una immagine di default se la cartella impostata non è corretta o se l’immagine corrente non esiste più, in modo da prepararci per quando archivieremo le immagini già inserite.
  • Intercettiamo il click su una delle immagini ruotate per passare alla visualizzazione dell’immagine ingrandita
  • Click sull’immagine ingrandita per ciclare tra Zoom x 1, Zoom x 2 e Zoom x 4, badando bene di zoommare nel punto del click.
  • Visualizzare un overlay con lo zoom corrente
  • Caricare le immagini in modo asincrono, per fare in modo che l’interfaccia non rimanga bloccata mentre leggo l’immagine da file.

Attaccate le cinture, anche questa puntata si preannuncia densa!

Intercettare il resize

Iniziamo con sistemare la visualizzazione delle immagini all’apertura della finestra di dialogo. Come avrete notato le 4 immagini ruotate non si ridimensionano correttamente all’avvio dell’applicazione, e non riscalano quando cambiamo la dimensione della stessa.

Risolvere questo è rapido, basta reimplementare il metodo resizeEvent(). Pertanto nel costruttore della classe inseriamo:

 protected:
     void resizeEvent(QResizeEvent *event);

ALT+INVIO ed implementiamolo semplicemente chiamando il metodo updateGuiPageRotation(). Mettiamo anche Q_UNUSED() per avvisare il compilatore che volutamente non stiamo usando event.

 void SmqbExpensesImagesWgt::resizeEvent(QResizeEvent *event)
 {
     Q_UNUSED(event)
     updateGuiPageRotation();
 }

Compilate e divertitevi a ridimensionare la finestra di dialogo a piacimento.

Ma questo non è l’unico modo di gestire il resize, dato che è possibile filtrare tutti gli eventi destinati alla form installando un eventFilter, ma ho preferito lasciare eventFilter per il click sulle immagini.

Event Filter

Gran parte delle modifiche di oggi passano attraverso eventi dell’utente. In genere non si riescono a gestire attraverso signals and slot, ma per intercettarli ci sono fondamentalmente 2 modi:

  • Il metodo classico: ovvero faccio una sottoclasse di tutti gli oggetti che mi generano un evento e vado a sovrascrivere il metodo che gestisce l’evento. Sicuramente efficiente, ma terribilmente lungo da fare.
  • Il metodo champenoise, ovvero l’approccio spumeggiante alla Qt: per ogni widget che mi genera un evento posso installare un oggetto in grado di filtrarlo. È più facile a farsi che a dirsi, basta re-implementare il metodo eventFilter della nostra classe SmqbExpensesImagesWgt e poi installare la classe stessa come filtro. Il codice da scrivere è veramente poco e concentrato nella classe principale.

Iniziamo aggiungendo ai metodi protected di SmqbExpensesImagesWgt.h la seguente dichiarazione:

 protected:
     bool eventFilter(QObject *obj, QEvent *event);

ALT+INVIO e creiamo l’implementazione. La funzione va scritta in modo che:

  • se l’evento lo gestisco io, restituisco true
  • se l’evento non è tra quelli che gestisco, restituisco false e passo il controllo al metodo chiamante.

Per il momento, limitiamoci a questa implementazione:

 bool SmqbExpensesImagesWgt::eventFilter(QObject *obj, QEvent *event)
 {
     return false;
 }

Ora, la nostra classe SmqbExpensesImagesWgt può fare da event filter, pertanto colleghiamola a tutti gli oggetti dove andremo a cliccare. In particolare:

  • graphicsView per intercettare il doppio click e attivare lo zoom
  • pixmap000… pixmap270 per passare alla visualizzazione grande quando l’utente seleziona una delle rotazioni.

Ecco il codice da inserire nella costruttore della classe:

     ui->graphicsView->installEventFilter(this);
     ui->pixmap000->installEventFilter(this);
     ui->pixmap090->installEventFilter(this);
     ui->pixmap180->installEventFilter(this);
     ui->pixmap270->installEventFilter(this);

Ora è bene provare a compilare e controllare di poter ancora sfogliare e ridimensionare gli scontrini.

Prepariamoci a vedere pageDisplay

Serve del lavoro preparatorio, in particolare serve aggiungere due metodi protected nella dichiarazione della classe, uno per evidenziare l’immagine selezionata con un bordo verde, l’altro per attivare pageDisplay con l’immagine orientata correttamente.

void updateGuiPageRotationHighlight(QGraphicsView *view);
void updateGuiPageDisplayShowCurrentImg(const QTransform &transform);

Per quanto riguarda updateGuiPageRotationHighlight(), il suo scopo è di attivare un rettangolo verde attorno all’immagine selezionata e la sua implementazione è la seguente:

 void SmqbExpensesImagesWgt::updateGuiPageRotationHighlight(
     QGraphicsView *view)
 {
     // Reset highlight
     QString resetBorder("border: 3px solid rgba(0, 0, 0, 0);");
     ui->pixmap000->setStyleSheet(resetBorder);
     ui->pixmap090->setStyleSheet(resetBorder);
     ui->pixmap180->setStyleSheet(resetBorder);
     ui->pixmap270->setStyleSheet(resetBorder);
     if(!view)
         return;
     view->setStyleSheet("border: 3px solid green;");
 }

In pratica attiviamo il bordo usando lo styleSheet, facendo attenzione a metterne uno trasparente su quelli non selezionati, altrimenti la spaziatura dei 4 widget si sballa.

Per quanto riguarda updateGuiPageDisplayShowCurrentImg(), l’implementazione è la seguente:

 void SmqbExpensesImagesWgt::updateGuiPageDisplayShowCurrentImg(
     const QTransform &transform)
 {
         ui->stackedWidget->setCurrentWidget(ui->pageDisplay);
         ui->graphicsView->setTransform(transform);
         ui->graphicsView->fitInView(m_currentImgScene->sceneRect(),
                                     Qt::KeepAspectRatio);
         m_pageDisplayTransformRotation =
             ui->graphicsView->transform();
 }

In pratica impostiamo pageDisplay come widget corrente, poi applichiamo a graphicsView la trasformazione che riceviamo come argomento, facciamo in modo che tutta la scena (e quindi tutto lo scontrino) sia visibile, e per finire salviamo la trasformazione corrente in una property della classe, che aggiungiamo immediatamente nella definizione:

QTransform m_pageDisplayTransformRotation;

Questa property sarà essenziale per gestire lo zoom, e badate bene che non è la trasformazione che riceviamo in ingresso, ma che applica la rotazione, sommata al risultato di fitInView(). Questo semplice accorgimento, che adesso mi sembra ovvio, in realtà l’ho trovato dopo parecchie prove, ma ne è valsa la pena perché mi ha permesso di risparmiare decine di righe di codice.

Intercettare il click sulle immagini ruotate

Iniziamo ad usare eventFilter() intercettando il caso in cui l’utente clicca su una delle 4 immagini ruotate. L’effetto che voglio è quello di attivare la pagina pageDisplay, dove posso vedere il mio scontrino orientato correttamente.

Inseriamo un metodo protected chiamato:

     bool eventFilter_pixmapRotation(QObject *obj, QEvent *event);

Questo metodo gestirà il caso in cui l’utente clicca su una delle immagini, e la sua implementazione prevede di: controllare che si tratti di un click e non per esempio di un doppio click, controllare che venga da un oggetto QGraphicsView, e poi aggiornare il bordo verde con updateGuiPageRotationHighlight() e mostrare finalmente l’immagine nella pageDisplay con updateGuiPageDisplayShowCurrentImg().

 bool SmqbExpensesImagesWgt::eventFilter_pixmapRotation(QObject *obj,
                                                        QEvent *event)
 {
     if(event->type() == QEvent::MouseButtonPress) {
         QGraphicsView* view = qobject_cast<QGraphicsView*>(obj);
         if(!view)
             return false;
         updateGuiPageRotationHighlight(view);
         updateGuiPageDisplayShowCurrentImg(view->transform());
         return true;
     }
     return false;
 }

Ora basta chiamare il nuovo metodo ogni volta che l’oggetto che ha generato l’evento è una delle quattro immagini e siamo pronti a vedere l’effetto che fa:

 bool SmqbExpensesImagesWgt::eventFilter(QObject *obj, QEvent *event)
 {
     if(obj == ui->pixmap000) {
         return eventFilter_pixmapRotation(obj, event);
     }
     else if(obj == ui->pixmap090) {
         return eventFilter_pixmapRotation(obj, event);
     }
     else if(obj == ui->pixmap180) {
         return eventFilter_pixmapRotation(obj, event);
     }
     else if(obj == ui->pixmap270) {
         return eventFilter_pixmapRotation(obj, event);
     }
     return false;
 }

Compilando dovreste riuscire a vedere l’immagine ingrandita cliccando su una delle 4 rotazioni:

E quando premete il pulsante “Successivo” vedrete il bordo verde attorno all’immagine su cui avete cliccato.

Un lavoro per il pulsante Ricarica

Il pulsante ricarica è servito principalmente a me per testare l’interfaccia, ma già che c’era… Facciamo in modo che premendo il pulsante l’immagine corrente venga ricaricata, si passi alla pagina PageRotation e si possa provare una diversa rotazione.

Implementiamo il gestore dell’evento clicked e inseriamo il codice seguente:

 void SmqbExpencesImages::on_cmdRotation_clicked()
 {
     ui->stackedWidget->setCurrentWidget(ui->pageRotation);
 }

Zoom ciclico

Vorrei uno zoom semplice che funzioni in modo ciclico attraverso questi 3 stati:

  • primo click zoom x2
  • secondo click zoom x 4
  • terzo click torno alla visualizzazione iniziale

Aggiungiamo una property private chiamata m_pageDisplayZoom, che inizializziamo a 1:

     int m_pageDisplayZoom{1};

Poi aggiungiamo un metodo protected chiamato updateGuiPageDisplayApplyZoom(), e con ALT+INVIO lo implementiamo

     void updateGuiPageDisplayApplyZoom(int zoom);

Il metodo riceve il valore di zoom come argomento di ingresso e fa in modo che l’immagine venga riscalata. Per riscalare l’immagine devo calcolare una trasformazione in modo che me la presenti allargata o rimpicciolita. Solo che io sto già usando una trasformazione, quella che mi orienta l’immagine nel modo corretto. Come faccio a combinare le due, ovvero zoom e scala? Semplice, facendo il prodotto tra la trasformazione iniziale (rotazione combinata con zoom to fit), ovvero m_pageDisplayTransformRotation e la trasformazione della scala.

 void SmqbExpensesImagesWgt::updateGuiPageDisplayApplyZoom(int zoom)
 {
     // Apply Zoom
     QTransform newTransform = m_pageDisplayTransformRotation *
                               QTransform::fromScale(zoom,
                                                     zoom);
     ui->graphicsView->setTransform(newTransform);
 }

Ora non resta che gestire il click, aggiungendo il seguente codice a eventFilter:

 else if(obj == ui->graphicsView) {
         if (event->type() == QEvent::MouseButtonPress) {
             m_expenseZoom *= 2;
             if(m_expenseZoom > 4)
                 m_expenseZoom = 1;
             updateGuiPageDisplayApplyZoom(m_expenseZoom);
             return true;
         }
         return false;
 }

Dopo aver calcolato il nuovo valore di zoom in modo ciclico tra 1, 2 e 4, lo applichiamo all’immagine dello scontrino usando updateGuiPageDisplayApplyZoom().

Overlay sul display

Carino lo zoom, solo che non si capisce quanto vale. Che ne dite se ci mettiamo un overlay sull’immagine, in modo che compaia quando cambia e poi scompaia dopo un secondo? Ovviamente farlo non è banale, ma applicando quanto descritto in Qt 5 Quanto Basta, capitolo 7, non è nemmeno troppo complesso.

Ci servono 2 oggetti grafici, una QLabel che chiamiamo m_pageDisplayZoomLabel e un QHBoxLayout che chiamiamo m_pageDisplayZoomLabelLayout.

Li inseriamo come due property nella nostra classe SmqbExpensesImagesWgt:

     QLabel m_pageDisplayZoomLabel;
     QHBoxLayout m_pageDisplayZoomLabelLayout;

Per poterli usare non dimenticate di aggiungere anche:

 #include <QLabel>
 #include <QHBoxLayout>

Bene, ora dobbiamo aggiungere del codice al costruttore della classe in modo da aggiungere il layout a graphicsView. In questo modo il layout copia la dimensione del widget, pertanto gestirà da solo la posizione e il ridimensionamento. Come secondo step inseriamo l’etichetta all’interno del layout. In questo modo l’etichetta sarà al centro di graphicsView.

Ora sistemiamo la label, nascondendola, impostando l’allineamento al centro sia in orizzontale che in verticale e manipolando il font in modo di impostare un bel 40 pixel bold.

     ui->graphicsView->setLayout(&m_pageDisplayZoomLabelLayout);
     m_pageDisplayZoomLabelLayout.addWidget(&m_pageDisplayZoomLabel);
     m_pageDisplayZoomLabel.setVisible(false);
     m_pageDisplayZoomLabel.setAlignment(
         Qt::AlignHCenter | Qt::AlignVCenter);
     QFont font = m_pageDisplayZoomLabel.font();
     font.setPointSize(40);
     font.setBold(true);
     m_pageDisplayZoomLabel.setFont(font);

Per far comparire lo zoom dobbiamo modificare il metodo updateGuiPageDisplayApplyZoom(), mostrando l’etichetta ed impostando il testo. Ecco come diventa il codice del metodo.

 void SmqbExpensesImagesWgt::updateGuiPageDisplayApplyZoom(int zoom)
 {
     // Apply Zoom
     QTransform newTransform = m_pageDisplayTransformRotation *
                               QTransform::fromScale(zoom,
                                                     zoom);
     ui->graphicsView->setTransform(newTransform);
     m_pageDisplayZoomLabel.setVisible(true);
     m_pageDisplayZoomLabel.setText(QString("ZOOM %1X").arg(zoom));
 }

Ecco che cosa esce:

Come faccio a far sparire la scritta? Semplice, attivo un timer one shot che dopo un secondo disattiva la visualizzazione.

     QTimer::singleShot(1000, [&]() {
         m_pageDisplayZoomLabel.setVisible(false); } );

Il timer si attiva una sola volta, dopo 1 secondo, ed esegue il codice della lambda expression, disattivando la visualizzazione dell’etichetta con il valore di zoom. Non dimenticate di aggiungere:

 #include <QTimer>

Avete provato a compilare? Vi piace il risultato? A me non piace molto! La scritta compare e sparisce in modo netto. Servirebbe una transizione più dolce sia in ingresso che in uscita, ma al momento mi accontento di questa.

Immagine vuota

Cosa succede se non ci sono immagini, per esempio perché ho impostato un percorso non valido? Vi ricordate l’immagine segnaposto usata nella puntata precedente? Potremmo provare a visualizzare quella.

Modifichiamo currentImgRead() per fare in modo che se non riesce a leggere un’immagine valida, la sostituisca con l’immagine segnaposto ed azzeri il riferimento al file. Ecco il codice completo del metodo:

 bool SmqbExpensesImagesWgt::currentImgRead(
     const QFileInfo &imgFileInfo)
 {
     if(!m_currentImg.load(imgFileInfo.absoluteFilePath())) {
         m_currentImg.load("://Breathe-empty.svg");
         m_currentImgFileInfo = QFileInfo();
         m_currentImgPixmapItem->setPixmap(m_currentImg);
         return false;
     }
     m_currentImgFileInfo = imgFileInfo;
     m_currentImgPixmapItem->setPixmap(m_currentImg);
     return true;
 }

Ma non pensate che sia tutto quì! Se provate a compilare, dopo aver inserito un percorso sbagliato per le immagini, vi ritrovate con:

In pratica l’immagine segnaposto è ridotta a pochi pixel!

Questo succede per una caratteristica peculiare dell’oggetto QGraphicsScene, ovvero la scena automaticamente cresce, mentre la si deve rimpicciolire manualmente. Pertanto dato che le immagini degli scontrini sono dei giganti rispetto all’immagine segnaposto, ecco spiegato il risultato. Per adattare la scena al suo contenuto, basta aggiornare il metodo updateGuiPageRotation(), forzando la dimensione della scena alla dimensione degli oggetti in essa contenuti, dopo aver caricato le immagini.

 m_currentImgPixmapItem->setPixmap(m_currentImg);
 m_currentImgScene->setSceneRect(
         m_currentImgScene->itemsBoundingRect());

Ora sistemiamo il nome del file, sempre in updateGuiPageRotation(), in particolare, testiamo se il file esiste, altrimenti svuotiamo il contenuto dell’etichetta lblFileName. Ecco il codice:

  if(m_currentImgFileInfo.exists()) {
         ui->lblFileName->setText(
             QString("%1 (%2/%3)").arg(m_currentImgFileInfo.fileName())
                 .arg(m_currentImgIndex + 1)
                 .arg(m_expensesImgList.count())
             );
     }
     else {
         ui->lblFileName->clear();
     }

Resta ancora un piccolo passaggio, ovvero dobbiamo fare in modo di forzare un updateGuiPageRotation() quando SmqbSettings scatena un evento dataChanged:

         connect(m_settings, &SmqbSettings::dataChanged, [=](){
             settingsRead();
             currentImgRead(m_expensesImgList.value(0));
             updateGuiPageRotation();
         });

Ecco adesso potete compilare e vedere come, se cambio il percorso delle immagini con gli scontrini e punto una cartella non esistente, viene visualizzata l’immagine segnaposto, e il nome del file sparisce.

Ci sono ancora due imperfezioni…

Se sono nella pagina pageDisplay e ridimensiono la finestra, il gestore dell’evento resize (resizeEvent()) si attiva e chiama updateGuiPageRotation() che forza la visualizzazione della prima pagina. Chiaramente questo non è il comportamento desiderato, pertanto dobbiamo fare una piccola rilavorazione e commentare una riga di codice.

 p, li { white-space: pre-wrap; } 
 void SmqbExpensesImagesWgt::updateGuiPageRotation()
 {
     if(m_currentImg.isNull())
         return;
     // ui->stackedWidget->setCurrentWidget(ui->pageRotation);

Il codice che ho commentato, va ripetuto 3 volte, una per ogni pulsante della form. Ecco come diventano i 3 gestori dei pulsanti:

 void SmqbExpensesImagesWgt::on_cmdPrevious_clicked()
 {
     int idx = m_currentImgIndex-1;
     QFileInfo FI = m_expensesImgList.value(idx);
     if(!FI.exists()) {
         QApplication::beep();
         return;
     }
     if(!currentImgRead(FI))
         return;
     m_currentImgIndex = idx;
     ui->stackedWidget->setCurrentWidget(ui->pageRotation);
     updateGuiPageRotation();
 }
 
 void SmqbExpensesImagesWgt::on_cmdNext_clicked()
 {
     int idx = m_currentImgIndex + 1;
     QFileInfo FI = m_expensesImgList.value(idx);
     if(!FI.exists()) {
         QApplication::beep();
         return;
     }
     if(!currentImgRead(FI))
         return;
     m_currentImgIndex = idx;
     ui->stackedWidget->setCurrentWidget(ui->pageRotation);
     updateGuiPageRotation();
 }
 
 void SmqbExpensesImagesWgt::on_cmdReload_clicked()
 {
     currentImgRead(m_currentImgFileInfo);
     ui->stackedWidget->setCurrentWidget(ui->pageRotation);
     updateGuiPageRotation();
 }

Il resize della pageDisplay

La seconda imprecisione riguarda la pagina con lo scontrino ingrandito. Ora che ridimensionando la finestra di dialogo non passo più su pageRotation, mi sono accorto che l’immagine non si ridimensiona automaticamente. Per sistemare dobbiamo commentare la selezione della pagina che viene fatta in: updateGuiPageDisplayShowCurrentImg()

     // ui->stackedWidget->setCurrentWidget(ui->pageDisplay);

e la dobbiamo spostare nel metodo eventFilter_pixmapRotation(). Questo è come diventa il codice del metodo:

 bool SmqbExpensesImagesWgt::eventFilter_pixmapRotation(QObject *obj,
                                                        QEvent *event)
 {
     if(event->type() == QEvent::MouseButtonPress) {
         QGraphicsView* view = qobject_cast<QGraphicsView*>(obj);
         if(!view)
             return false;
         updateGuiPageRotationHighlight(view);
         ui->stackedWidget->setCurrentWidget(ui->pageDisplay);        
         updateGuiPageDisplayShowCurrentImg(view->transform());
         return true;
     }
     return false;
 }

Ora per concludere basta invocare updateGuiPageDisplayShowCurrentImg() e updateGuiPageDisplayApplyZoom() nel gestore dell’evento resize. Ecco l’implementazione finale:

 void SmqbExpensesImagesWgt::resizeEvent(QResizeEvent *event)
 {
     Q_UNUSED(event)
     updateGuiPageRotation();
     updateGuiPageDisplayShowCurrentImg(
         m_pageDisplayTransformRotation);
     updateGuiPageDisplayApplyZoom(m_pageDisplayZoom);
 }

Zoom sul punto

Questa in realtà è una finezza, ma dato che ormai ho imparato come farla, ci tengo ad applicarla sempre nei miei applicativi. Il problema è il seguente:

  • Sto analizzando uno scontrino fiscale e non riesco a leggere il testo scritto, o la spesa.
  • Clicco sulla immagine per ingrandirla
  • Vorrei che lo zoom tenesse fermo il punto in cui ho cliccato, in modo da mostrare ingrandito il particolare che mi interessa e non dover aggiustare la posizione.

Dato che al momento, lo zoom preserva il centro dell’immagine, l’idea è di calcolare il punto di zoom prima, calcolare la posizione dopo lo zoom e applicare una traslazione per compensare. Le modifiche vanno fatte nel metodo updateGuiPageDisplayApplyZoom().

Per prima cosa serve una funzione di supporto in grado convertire delle coordinate globali in un punto sulla nostra immagine. Creiamo un prototipo protected chiamato: globalToCurrentImgPixmap()

 QPointF globalToCurrentImgPixmap(QPoint globalPoint);

L’implementazione prevede di convertire il punto globale in un punto dell controllo graphicsView, poi di mappare il punto sulla scena, e per finire mappare il punto su m_currentImgPixmapItem.

 QPointF SmqbExpencesImages::globalToCurrentImgPixmap(
     QPoint globalPoint)
 {
     QPoint viewPoint = ui->graphicsView->mapFromGlobal(
         globalPoint);
     QPointF scenePoint = ui->graphicsView->mapToScene(
         viewPoint);
     QPointF pixmapPoint = m_currentImgPixmapItem->mapFromScene(
         scenePoint);
     return pixmapPoint;
 }

Ora possiamo iniziare a modificare updateGuiPageDisplayApplyZoom(). Partiamo prendendo il punto del click in coordinate globali, le trasformiamo in coordinate per l’immagine, e controlliamo se il click è avvenuto all’interno dell’immagine, altrimenti usiamo il centro dell’oggetto graphicsView, dato che evidentemente l’utente non si aspetta di zoommare un punto fuori dall’immagine.

     QPoint centerZoom = QCursor::pos();
     QPointF centerZoom2Pixmap = globalToCurrentImgPixmap(centerZoom);
     if(!m_currentImgPixmapItem->contains(centerZoom2Pixmap)) {
         QPoint centerViewport = ui->graphicsView->viewport()
                                     ->rect().center();
         centerZoom = ui->graphicsView->mapToGlobal(centerViewport);
         centerZoom2Pixmap = globalToCurrentImgPixmap(centerZoom);
     }

Poi applico lo zoom e la rotazione, dopo di che ricalcolo dove è andato a finire il mio punto. Per questo motivo mi sono salvato centerZoom.

Ora ho la coordinata del click, espressa nello spazio dell’immagine, e la nuova coordinata del click, dopo la trasformazione. Se applico una traslazione, posso fare in modo che l’immagine si riposizioni sotto al mio cursore. Il metodo moveBy fa tutto il lavoro.

     QPointF newCenterZoom2Pixmap = globalToCurrentImgPixmap(
         centerZoom);
     QPointF delta = newCenterZoom2Pixmap - centerZoom2Pixmap;
     m_currentImgPixmapItem->moveBy(delta.rx(), delta.ry());
     m_currentImgScene->setSceneRect(
         m_currentImgScene->itemsBoundingRect());

Non dimenticate di ricalcolare il bouding rect della scena, dopo aver applicato lo zoom.

Caricamento asincrono

Il caricamento asincrono serve per sfogliare velocemente le immagini e non bloccare la GUI in attesa delle immagini stesse. Il concetto base è questo: io voglio poter scorrere le immagini velocemente ad esempio per saltare all’ultima, vedendo una immagine ogni tanto, ma soprattutto vedendo l’ultima immagine che ho selezionato.

Questo meccanismo va implementato in questo modo:

  • Carico la prima immagine
  • L’utente chiede la prossima, ma sto ancora cercando la prima, pertanto la salto, ma mi salvo il file.
  • L’utente chiede un’altra immagine, ma sto ancora caricando la prima, mi limito ad aggiornare il percorso della prossima immagine.
  • Finisco di caricare la prima, e avvio il caricamento del prossimo file.
  • L’utente smette di scorrere, si trova con l’ultimo file selezionato, che inevitabilmente, alla fine viene caricato.

Partiamo includendo il modulo concurrent nel file di progetto.

 QT       += core gui concurrent

Poi modifichiamo nella classe SmqbExpensesImagesWgt inseriamo il metodo statico che caricherà l’immagine:

protected:     
     static QPixmap doCurrentImgRead(const QFileInfo& imgFileInfo);

La sua implementazione è semplice, deve solo leggere la pixmap da disco.

 QPixmap SmqbExpensesImagesWgt::doCurrentImgRead(
     const QFileInfo &imgFileInfo)
 {
     return QPixmap(imgFileInfo.absoluteFilePath());
 }

Ora, sempre nel file .h inseriamo il riferimento a QFutureWatcher, che è l’oggetto con cui controlliamo lo stato del thread che legge l’immagine da disco:

 #include <QFutureWatcher>

E poi inseriamo due property, una di tipo QFutureWatcher e un QFileInfo dove metteremo il file dell’ultima immagine da caricare.

 QFutureWatcher<QPixmap> m_currentImgReadWatcher;
 QFileInfo m_nextImageToRead;

Ora dobbiamo rivedere il metodo currentImgRead() in quanto non deve leggere l’immagine, ma far partire doCurrentImgRead() in un thread separato. Se il file non esiste restituisce false, e questo serve per emettere il beep quando arrivo alla fine della lista. Poi controllo che non ci sia già un caricamento in corso, nel qual caso mi limito a mettere via il nome della prossima immagine. Per finire, tramite il metodo setFuture() mi metto in attesa dei risultati di QtConcurrent::run(), a cui passo il metodo doCurrentImgRead() e il nome del file da usare.

 bool SmqbExpensesImagesWgt::currentImgRead(
     const QFileInfo &imgFileInfo)
 {
     if(!imgFileInfo.exists())
         return false;
 
     if(m_currentImgReadWatcher.isRunning()) {
         m_nextImageToRead = imgFileInfo;
         return true;
     }
     m_currentImgReadWatcher.setFuture(QtConcurrent::run(
            doCurrentImgRead, imgFileInfo));
     m_currentImgFileInfo = imgFileInfo;
     return true;
 }

Ora che abbiamo stravolto currentImgRead, creiamo uno slot che dovrà gestire l’arrivo delle immagini:

 protected slots:
     void currentImgUpdate();

La sua implementazione ricalca in parte quello che era currentImgRead() dato che va a pescare l’immagine letta in m_currentImgReadWatcher, controlla che sia valida, altrimenti carica il segnaposto, poi salva l’immagine nel pixmapItem, e chiama updateGuiPageRotation() in modo da aggiornare l’immagine e il suo nome file.

Poi attiva il caricamento della immagine successiva.

 void SmqbExpensesImagesWgt::currentImgUpdate()
 {
     m_currentImg = m_currentImgReadWatcher.result();
     if(m_currentImg.isNull()) {
         m_currentImg.load("://Breathe-empty.svg");
         m_currentImgFileInfo = QFileInfo();
     }
     m_currentImgPixmapItem->setPixmap(m_currentImg);
     updateGuiPageRotation();
     currentImgRead(m_nextImageToRead);
     m_nextImageToRead = QFileInfo();
     return;
 }

Chi chiama lo slot? Serve una connect, pertanto nel costruttore della classe aggiungiamo:

 connect(&m_currentImgReadWatcher, SIGNAL(finished()),
             this, SLOT(currentImgUpdate()));

Abbiamo finito? Quasi! Dato che abbiamo inserito updateGuiPageRotation() in currentImgUpdate(), lo dobbiamo commentare in

  • on_cmdPrevious_clicked()
  • on_cmdNext_clicked()
  • on_cmdReload_clicked()

Compilate e provate. A seconda della velocità del computer dovreste riuscire a percepire che le immagini arrivano leggermente in ritardo, e che se cliccate in fretta non si accodano, ma dopo un attimo che vi siete fermati arriva l’ultima.

Conclusioni

Basta per oggi, la prossima volta inizieremo la form per inserire i dati nel database.

Autore: Gianbattista

Appassionato di tecnologia, sono l'autore di Qt5 Quanto Basta. Per lavoro mi occupo di elaborazione delle immagini per applicazioni industriali, ma nel tempo libero adoro creare applicazioni con Qt (www.qt.io)

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *