Spendi Meno Quanto Basta Parte 3

Avete raccolto gli scontrini? Oggi procediamo nella costruzione della applicazione, iniziando a lavorare sul tab per sfogliare le immagini degli scontrini.

Nella prima parte di Spendi Meno Quanto Basta abbiamo creato lo scheletro dell’applicazione, nella seconda parte abbiamo aggiunto la pagina delle impostazioni, ora vediamo come sfogliare le immagini.

SmqbExpensesImagesWgt

Creiamo una nuova classe che chiameremo SmqbExpensesImageWgt a cui corrisponde un widget che piazzeremo nel tab spese della nostra applicazione.

Sempre File, New File or Project, Qt, Qt Designer Form Class. Selezioniamo Widget e mettiamo i seguenti nomi:

  • Classe: SmqbExpensesImagesWgt
  • Header file: SmqbExpensesImagesWgt.h
  • Source file: SmqbExpensesImagesWgt.cpp
  • Form file: SmqbExpensesImagesWgt.ui

Questa finestra di dialogo ci servirà per due funzioni: caricare l’immagine dello scontrino, e ruotare lo scontrino nel modo corretto per riuscire a leggerlo comodamente. Pertanto avremo uno stacked widget con due pagine, una per le 4 immagini ruotate (0°, 90°, 180°, 270°) e una con la vista dello scontrino con la rotazione corretta.

Ora aggiungiamo gli elementi alla form. Seguitemi con attenzione e pazienza perchè il layout è abbastanza elaborato.

  • In alto inseriamo un QLabel che chiameremo lblFileName in cui metteremo il nome dell’immagine attuale. Inseriamo il testo NomeFile come segnaposto.
  • Al centro della finestra mettiamo lo stack di widget QStackedWidget
  • La prima pagina dello stack la chiamiamo pageRotation.
  • La seconda pagina dello stack la chiamiamo pageDisplay.
  • All’interno di pageRotation mettiamo 4 QGraphicsView che chiameremo pixmap000, pixmap090, pixmap180, pixmap270, disposti in una matrice 2 x 2 tramite un QGridLayout. Tutta la pagina va ordinata usando un Layout Verticale.

Se ci siamo capiti, dovreste avere questa visualizzazione:

  • Nella seconda pagina (pageDisplay) mettiamo un quinto QGraphicsView che chiameremo pixmapView e applichiamo alla pagina il layout verticale.
  • In basso, mettiamo 3 QPushButton a cui assegneremo poi delle icone prese tra le standard pixmap, e che chiamiamo cmdPrevious (Precedente), cmdNext (Successivo), cmdReload (Ricarica).
  • Tra cmdNext e cmdPrevious mettiamo un distanziatore orizzontale.
  • Raggruppiamo i pulsanti e il distanziatore in un QHBoxLayout.

Poi applichiamo a tutta la nostra form un QVBoxLayout. Ecco quello che dovreste vedere:

Ora compiliamo, in modo che vengano creati if file moc_*, altrimenti non riusciamo ad accedere agli elementi della GUI attraverso l’editor di codice.

Inseriamo le icone dei pulsanti

Con un bel SHIFT+F4 saltiamo in SmqbExpensesImagesWgt.cpp ed inseriamo nel costruttore della classe:

 ui->cmdNext->setIcon(
         style()->standardIcon(QStyle::SP_ArrowRight));
     ui->cmdPrevious->setIcon(
         style()->standardIcon(QStyle::SP_ArrowLeft));
     ui->cmdReload->setIcon(
         style()->standardIcon(QStyle::SP_BrowserReload));

Già che ci siamo, forziamo nel costruttore delle classe, la visualizzazione della prima pagina:

ui->stackedWidget->setCurrentWidget(ui->pageRotation);

Prepriamo il tab Uscite

A differenza di quanto fatto con il tab impostazioni, in questo caso non voglio che SmqbExpensesImagesWgt sostituisca in toto il tab delle uscite, ma voglio che ne occupi metà, mentre l’altra metà la dedicheremo all’inserimento dei parametri nel database. Pertanto apriamo la mainwindow.ui, posizioniamoci sul tab Uscite ed inseriamo 2 widget, uno a destra e uno a sinistra, poi attiviamo il layout verticale.

Il widget di destra lo chiamiamo ctrImages, il widget di sinistra lo chiamiamo ctrData

Poi selezioniamo tabExpense ed andiamo ad azzerare i margini del layout e soprattutto impostiamo layoutStretch 1,1. Questo serve per forzare la distribuzione dello spazio tra i due widget.

Come dite? Non si capisce niente? Verissimo. Per rendere i confini dei 2 widget visibili, impostiamo il loro stylesheet inserendo la stringa:

  border: 1px solid red

Naturalmente se preferite usare green, blue, magenta o quant’altro al posto di red, siete liberi di provare. Il risultato è il seguente:

Se proviamo a compilare otteniamo:

Come si vede ci sono i due widget che si spartiscono lo spazio.

Integriamo SmqbExpensesImagesWgt

Tempo di usare promote e rimpiazzare il contenuto di ctrImages con SmqbExpensesImagesWgt. Al solito, dalla form principale, selezioniamo il tab Uscite, selezioniamo ctrImages e con tasto destro selezioniamo “Promote to …”. Nella finestra che si apre inseriamo il nome della classe da promuovere, ovvero SmqbExpensesImagesWgt, premiamo Add e poi Promote.

Prima di compilare, togliamo il bordo rosso, altrimenti il risultato è indegno di Qt. Pertanto nel costruttore della classe principale MainWindow::MainWindow(), azzeriamo lo stylesheet di ctrImages con:

     ui->ctrImages->setStyleSheet("");

Ora possiamo compilare:

Come mai la ctrData e ctrImages non sono uguali?

Il risultato va un attimo commentato, dato che la distribuzione delle aree non è equa. Questo perché i pulsanti forzano la dimensione minima della zona a sinistra. Se provate ad allargare la finestra a destra, vedrete il rettangolo rosso crescere fino a che non diventa equivalente alla regione di sinistra. A questo punto tutti e due crescono.

Se non avessimo messo layoutStretch = 1,1 ma lo avessimo lasciato 0,0 avremmo avuto una crescita solo del lato sinistro, dato che lo spacer tra i due pulsanti spinge per conquistarsi spazio.

Prepariamoci alle immagini

Tempo di preparare i cinque oggetti QGraphicsView per mostrare le immagini ruotate. Per prima cosa serve una immagine corrente, che chiameremo m_currentImg e che sarà di tipo QPixmap.

Poi salviamoci anche il nome dell’immagine corrente, così lo possiamo usare per l’oggetto lblFileName. Chiameremo questa property m_currentImgFileInfo e sarà di tipo QFileInfo.

Per finire servono una scena per tutti QGraphicsView, e un oggetto QGraphicsPixmapItem. In questo caso devo dire che Qt è fantastico, dato che mi permette di avere una sola pixmap, metterla in una sola scena e di mostrare la scena con rotazioni diverse nella varie viste.

Ecco l’elenco delle property da aggiungere nella sezione private:

 private:
     ...     
     QPixmap m_currentImg;
     QFileInfo m_currentImgFileInfo;
     QGraphicsPixmapItem* m_currentImgPixmapItem{nullptr};
     QGraphicsScene* m_currentImgScene{nullptr};

Notate che m_currentImgPixmapItem (QGraphicsPixmapItem) e m_currentImgScene (QGraphicsScene) sono due puntatori e pertanto vanno inizializzati a nullptr.

Dopo aver aggiunto i riferimenti a QGraphicsScene e a QGraphicsPixmapItem:

 #include <QGraphicsScene>
 #include <QGraphicsPixmapItem>

passiamo a SmqbExpensesImagesWgt.cpp

Ora nel costruttore della classe per prima cosa creiamo l’oggetto m_currentImgScene, poi gli aggiungiamo una pixmap, e il risultato lo usiamo per valorizzare il puntatore m_currentImgPixmapItem. Fatte queste due inizializzazioni non resta che collegare la scena a tutti e 5 i visualizzatori, con relativa rotazione.

Ecco il codice da accodare al costruttore della classe:

m_currentImgScene = new QGraphicsScene(this);
m_currentImgPixmapItem = m_currentImgScene->addPixmap(m_currentImg);
ui->graphicsView->setScene(m_currentImgScene);
ui->pixmap000->setScene(m_currentImgScene);
ui->pixmap090->setScene(m_currentImgScene);
ui->pixmap180->setScene(m_currentImgScene);
ui->pixmap270->setScene(m_currentImgScene);
ui->pixmap090->rotate(90);
ui->pixmap180->rotate(180);
ui->pixmap270->rotate(270);

Proviamo con una immagine vuota

Tempo di vedere almeno una immagine, che dite? Magari un’immagine vuota, che funga da segnaposto qualora la nostra cartella degli scontrini sia vuota. Io ho aggiunto al file di risorse (Resources.qrc che abbiamo creato nella seconda parte), un’immagine che useremo come segnaposto qualora non ci fossero scontrini; ho usato questa:

e grazie a Wikipedia sono sicuro di non violare alcuna licenza, ma ovviamente potete scegliere l’immagine che più vi aggrada.

Dato che il file si chiama Breathe-empty.svg, il suo percorso nel file delle risorse diventa:

:/Breathe-empty.svg

per visualizzarla devo caricarla in m_currentImg prima di creare m_currentImgScene. Attenti a questo particolare o non vedrete niente

  m_currentImg.load("://Breathe-empty.svg");

Bene ora compilando dovreste vedere l’immagine vuota ruotata in 4 posizioni diverse.

Inseriamo la classe SmqbSettings

Prima di vedere le immagini degli scontrini servono alcune implementazioni. Apriamo SmqbExpensesImagesWgt.h ed iniziamo ad aggiungere un riferimento a SmqbSettings, dove potremo andare a leggere il percorso in cui ci sono le immagini, pertanto inseriamo m_settings nella sezione privata e creiamo il metodo pubblico setSettings().

Aggiungiamo anche una lista QFileInfoList, chiamata m_expensesImgList in cui metteremo la lista delle immagini che troviamo nella cartella, e visto che vogliamo sfogliare la lista, serve pure in indice dell’immagine corrente:

 private:
     Ui::SmqbExpencesImages *ui;
     QPointer<SmqbSettings> m_settings; 
     QFileInfoList m_expensesImgList;
     int m_currentImgIndex{0};

Ora aggiungiamo i riferimenti alle tre classi, altrimenti col piffero che compiliamo:

 #include <QFileInfoList>
 #include <QPointer>
 #include "SmqbSettings.h"

Poi prepariamo anche un metodo protected settingsRead() che leggerà il contenuto della cartella delle immagini per metterlo in m_expensesImgList.

 protected:
     void settingsRead();

ALT+INVIO ed implementiamo il metodo, controllando che m_settings sia valido, caricando le immagini con QDir e poi tirando fuori dalla cartella solo i file. Ovviamente mettiamo anche m_currentImgIndex a 0, anche se lo abbiamo inizializzato nella dichiarazione.

 void SmqbExpensesImagesWgt::settingsRead()
 {
     if(!m_settings)
         return;
     
     QDir images(m_settings->expensesImages());
     m_expensesImgList = images.entryInfoList(QDir::Files);
     m_currentImgIndex = 0;
 }

Ricordatevi di aggiungere

 #include <QDir>

CurrentImgRead

Tempo di caricare la prima immagine, almeno per vedere che tutto funzioni, che dite?

Per prima cosa dobbiamo invocare setSettings dalla form principale, in modo da far arrivare i settings direttamente in ctrImages. Pertanto in mainwindow.cpp, nel costruttore della classe, subito dopo il codice che toglie il bordo rosso al widget ctrImages, ma assolutamente PRIMA di invocare loadSettings(), andiamo a mettere:

 ui->ctrImages->setStyleSheet("");
 ui->ctrImages->setSettings(m_settings);
 loadSettings();

Ora torniamo in SmqbExpensesImagesWgt.h e aggiungiamo la funzione CurrentImgRead() come protected:

bool currentImgRead(const QFileInfo &imgFileInfo);

ALT+INVIO ed andiamo ad implementare la funzione che legge le immagini dalla cartella. Dato che riceve il file come argomento, proviamo subito a caricare l’immagine. Se il metodo fallisce usciamo perchè vuol dire che il percorso non è corretto, altrimenti procediamo e salviamoci il file in m_currentImgFileInfo, e poi andiamo ad aggiornare l’immagine rappresentata in m_currenteImgPixmapItem.

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

Modifichiamo setSettings

Tempo di collegare setSettings() con settingsRead(), pertanto modifichiamo il metodo in modo simile a quanto fatto nella form delle impostazioni. Prima controlliamo che il nuovo settings sia diverso da quello attuale. Poi facciamo una disconnect() se il riferimento non era nullo, poi salviamo il nuovo riferimento e ci mettiamo in ascolto dell’evento dataChanged().

Naturalmente se i dati vengono cambiati, come per esempio quando carichiamo i settings per la prima volta, aggiorniamo l’elenco delle immagini e invochiamo currentImgRead() andando a leggere la prima immagine. Usando value possiamo leggere il contenuto di una QList senza il rischio di eccezioni. Se l’immagine non c’è semplicemente restituisce un QFileInfo vuoto, che poi fa fallire currentImgRead().

 void SmqbExpensesImagesWgt::setSettings(
     const QPointer<SmqbSettings> &settings)
 {    
     if(m_settings == settings)
         return;
     
     if(m_settings)
         disconnect(m_settings, nullptr, nullptr, nullptr);
     
     m_settings = settings;
     
     if(m_settings)
         connect(m_settings, &SmqbSettings::dataChanged, [=](){
             settingsRead();
             currentImgRead(m_expensesImgList.value(0));
         });
 }

updateGuiPageRotation

Ora creiamo un metodo protected che serve per aggiornare la GUI quando leggo una nuova immagine. In particolare devo fare tre cose:

  • Forzare la visualizzazione del widget pageRotation
  • Sistemare lblFileName con il nome dell’immagine, l’indice corrente e il numero totale di immagini
  • Ridimensionare le immagini in modo che siano interamente visibili nelle loro QGraphicsView tramite il metodo fitInView().

Ecco il template della funzione, da mettere nella dichiarazione della classe, nella sezione protected:

void updateGuiPageRotation();

E questo è il codice del metodo:

 void SmqbExpensesImagesWgt::updateGuiPageRotation()
 {
     if(m_currentImg.isNull())
         return;
     ui->stackedWidget->setCurrentWidget(ui->pageRotation);
     ui->lblFileName->setText(QString("%1 (%2/%3)")
                                  .arg(m_currentImgFileInfo.fileName())
                                  .arg(m_currentImgIndex + 1)
                                  .arg(m_expensesImgList.count())
                              );
     m_currentImgPixmapItem->setPixmap(m_currentImg);
 
     ui->pixmap000->fitInView(m_currentImgScene->sceneRect(),
                              Qt::KeepAspectRatio);
     ui->pixmap090->fitInView(m_currentImgScene->sceneRect(),
                              Qt::KeepAspectRatio);
     ui->pixmap180->fitInView(m_currentImgScene->sceneRect(),
                              Qt::KeepAspectRatio);
     ui->pixmap270->fitInView(m_currentImgScene->sceneRect(),
                              Qt::KeepAspectRatio);
 }

Attiviamo i pulsanti avanti e indietro

Ora creare i gestori dei tasti “Precedente” e “Successivo” è un gioco da ragazzi. Basta incrementare o decrementare l’indice corrente, controllare se una immagine corrispondente all’indice esiste, se non esistere emettere un beep per avvisare l’utente.

Se invece l’immagine esiste dobbiamo invocare currentImgRead(), aggiornare l’indice corrente, ed invocare updateGuiPageRotation() per sistemare scala e nome immagine.

 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;
     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;
     updateGuiPageRotation();
 }

Compilando il tutto, finalmente possiamo sfogliare gli scontrini fiscali e vederli in una delle quattro orientazioni.

Conclusioni

Per oggi basta, ma la visualizzazione va un attimo sistemata, soprattutto si deve agganciare l’evento resize, fare in modo che gli scontrini siano della dimensione giusta appena apriamo l’applicazione e soprattutto fare in modo che cliccando su una delle immagini si passi alla successiva, con la rotazione corretta.

Nella prossima puntata vedremo come fare, per oggi basta così.

Un archivio di immagini

Nel 1998, per un amico archeologo ho creato un archivio di immagini, a supporto della sua attività. Essendo passati ormai più di 20 anni, mi ero completamente dimenticato del lavoro fatto.

Un giorno andai a trovarlo e con estrema sorpresa, notai che stava usando un database per gestire le immagini. Quando gli chiesi che applicativo fosse, mi disse: “Ma come, non ti ricordi? Me lo hai fatto tu!”

Ebbene sì era il mio vecchio database di immagini, che era passato indenne da Windows 3.11 a Windows 10, e da Access 2 a Access “non lo so a che versione siamo adesso…”

E nel tempo la base di immagini era cresciuta fino a gestire qualcosa vicino alle 100.000 immagini!

Indicizzare i percorsi

Non ci crederete, ma ho iniziato a fare reverse engineering sul mio lavoro. L’idea base era quella di mettere le immagini in cartelle e di salvare nel database solo il percorso dell’immagine, non tutta l’immagine.

Questo ha permesso di passare da fotocamere da 2 MPixel a fotocamere da 20 MPixel senza fare esplodere il database.

Tenete conto che Access memorizza il database in un file solo, pertanto, non sarebbe pensabile un database con 100.000 immagini. Se considerate 10MB a foto, fanno 1TB di file.

Per identificare le foto ho usato semplicemente un contatore, e nuovamente, dato che il filesystem di Windows non gradisce più di 3-5000 file nella stessa cartella, avevo creato un albero facendo cartelle da 1000 foto. Pertanto anche 100.000 foto alla fine si riducono ad una cartella con 100 sottocartelle.

Pre-calcolare le anteprime

Avevo fatto alcuni esperimenti, e avevo notato come fosse insopportabile aspettare che il controllo Immagine di Access ci mettesse dei secondi per visualizzare le immagini di anteprima. Allora pensai di pre-calcolarle, ovvero di creare un albero con tutte le immagini di anteprima già riscalate a 1024×768. Non contento di questo, mi spinsi oltre, pre-calcolando anche dei thumbnail a non ricordo quanto, ma circa 200×150 pixels. In questo modo era possibile incorporare il thumbnail nella finestra di dialogo, e poi con un click vedere l’anteprima a tutto schermo, che al tempo voleva dire 1024×768.

Quindi con 3 alberi indipendenti, uno per in thumbnail, uno per le anteprime, uno per le immagini originali, mi ero assicurato la scalabilità del sistema nel tempo.

L’idea mi era venuta leggendo un articolo sulle olimpiadi di Nagano, del 1998, in cui si spiegava come IBM fosse riuscita a garantire un elevatissimo numero di accessi al sito internet dell’organizzazione, pre-calcolando le pagine HTML al posto di fare una query su database per ogni accesso.

Visto gli strumenti e le conoscenze di cui disponevo al tempo, la procedura di archiviazione di ogni singola immagine era estremamente manuale, ma funziona bene da 20 anni, pertanto, perchè cambiarla?

Se lo rifacessi con Qt?

Per lavoro mi occupo di elaborazione di immagini, pertanto il tema di gestire grandi quantità di immagini mi è rimasto.

Per esigenze di lavoro mi sono trovato a creare un archivio di immagini, dimensionato per gestire 1 Milione di immagini.

Memore dell’esperienza di 20 anni fa, ho deciso di precalcolare le anteprime. Certo adesso le dimensioni sono diverse.

La risoluzione di riferimento di uno schermo è passata da 1024×768 a 1920×1080, quindi è intervenuto quasi un fattore 4, e quanto alla dimensione dei dischi, si è passati da 500 MB a 500 GB, almeno, questa è la dimensione del disco SSD del mio portatile.

Inoltre la velocità di lettura è passata da 5/10MB/s a 500MB/s, ma l’idea delle anteprime precalcolate è ancora valida, perché permette di sfogliare le immagini alla massima velocità possibile.

Il database? SQLite

Ora non uso più Access, ma SQLite, che come Access salva il database in un solo file, e come nel caso di Access è un database: “in proccess” ovvero vive all’interno della mia applicazione, non è un processo esterno. Questo rende la comunicazione estremamente veloce e la latenza bassa.

Se siete curiosi, guardate la mia presentazione al Qt Day 2019:

Archiviare le anteprime nel database

Ecco questo aspetto invece l’ho cambiato. Ora le anteprime le salvo sul database, perchè in questo modo riesco a visualizzarle molto più velocemente, e perché la dimensione del file del database non è più un problema. Anche se ho 1.000.000 di immagini, e per ogni immagine alloco 100kB, si tratta di un file da 100GB, impensabile 20 anni fa quando i dischi erano da 500 MB, accettabile oggi in un NAS da 15 TB (150 volte più grande)

Arrivano i NAS

Ecco, appunto, il problema è il NAS. Mentre 20 anni fa i NAS praticamente non esistevano (a livello personale), oggi un archivio di immagini per forza di cose deve stare su un NAS. Questo permette di avere anche 15 TB di spazio di archiviazione, a prezzi economici, condiviso tra più utenti.

Ma il NAS è collegato alla rete che per quanto sia veloce, introduce una latenza fastidiosa. Questa latenza si applica per ogni volta che accedo al filesystem, pertanto se ogni volta che voglio vedere una immagine, per quanto piccola devo aprire 4 o 5 cartelle, capite bene che il problema non sono i 10kB del thumbnail o i 90kB dell’anteprima, ma sono le 4 o 5 cartelle.

Stiamo parlando di 50/100 ms, che si notano in termini di velocità, e di reattività del sistema.

E se ci mettessimo la VPN? Anche in questo caso la situazione è la stessa, solo che le latenze sono maggiori.

Se invece l’immagine è nel database, il problema non esiste, dato che il file del database è già aperto, da qualche parte c’è una copia in cache, il calcolo della posizione della mia immagine è semplicemente un off-set nel file, la lettura viaggia alla velocità massima permessa dalla mia connessione.

Se rifacessi il lavoro oggi, come lo farei?

L’architettura con le anteprime precalcolate resta molto attuale, ma metterei le anteprime direttamente nel database.

E con Qt renderei automatica la creazione delle anteprime e tutto il processo di archiviazione.

Per non parlare dei dati EXIF e della possibilità di ricostruire la posizione geografica della foto.

Credo che sarebbe divertente farlo… Magari lo propongo al mio amico, che dite?

D’altronde Qt è il futuro!