In evidenza

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ì.

Spendi Meno Quanto Basta Parte 2

Procediamo nella costruzione della applicazione, inserendo il tab impostazioni con il percorso delle immagini.

Nella prima puntata di Spendi Meno Quanto Basta abbiamo creato lo scheletro dell’applicazione e abbiamo visto come salvare e ripristinare la geometria della finestra di dialogo. Ora aggiungiamo il tab delle impostazioni.

La classe per gestire le impostazioni

Da qualche parte dobbiamo poter impostare la cartella in cui si trovano le immagini. Questa informazione va messa nelle impostazioni, e sarà editabile dal tab Impostazioni.

Iniziamo creando una classe SmqbSettings con il comando File – “New File Or Project” poi selezioniamo: C++, C++ Class ed impostiamo il nome come SmqbSettings. Io in genere preferisco usare esattamente lo stesso nome della classe (quindi CamelCase per intenderci) anche per il nome del file di intestazione (.h) e del file di implementazione (.cpp). Il valore di default tutto in caratteri minuscoli, lo trovo impossibile da leggere.

Selezioniamo come classe base QObject e attiviamo anche l’opzione Include QObject.

Completiamo la procedura e la classe è stata creata, con tanto di parent, signals e slots.

Salviamo il percorso delle immagini

Ora aggiungiamo una proprietà privata m_expensesImages in cui mettere il percorso delle immagini. Se avete seguito la prima lezione, saprete che le immagini degli scontrini (le spese o expenses) finiscono in una cartella sincronizzata con Google Drive. E il percorso di questa cartella cambia a seconda del pc in cui vado ad eseguire l’applicazione. Pertanto la prima configurazione che implementiamo è proprio questo percorso.

private:
    QString m_expensesImages;
 

E alla velocità della luce (vi ho già detto “Qt is the future”?) aggiungiamo setter e getter. Basta cliccare su m_expencesImages, premere ALT+INVIO e selezionare “Create Setter e Getter”.

Ora aggiungiamo un signal dataChanged() che useremo per notificare a tutte le finestre di dialogo che le impostazioni sono cambiate. Questo meccanismo semplifica la condivisione delle informazioni e fa in modo che i dati possano essere anche modificati in punti diversi, senza perdere la coerenza.

signals:
    void dataChanged();

Modifichiamo la setExpesesImages() in modo da attivare un dataChanged() solo se i valori sono effettivamente diversi:

 void SmqbSettings::setExpensesImages(const QString &expensesImages)
 {
     if(m_expensesImages == expensesImages)
         return;
     
     m_expensesImages = expensesImages;
     emit dataChanged();
 }

Sospendere gli eventi dataChanged

Quando amplieremo la classe, metteremo altre proprietà ed altri metodi, ma se ogni volta che cambio un valore emetto un dataChanged, potrei rendere il codice molto inefficiente (e non è nello stile Qt) o peggio creare dei loop in cui aggiorno un valore e attivo l’aggiornamento del valore, all’infinito. Per evitare questo si usa la proprietà QObject::blockSignals().

Per rendere il tutto chiaro ed intellegibile, aggiungiamo due metodi:

void beginDataChange();
void endDataChange();     

Dato che SmqbSettings è derivato da QObject, l’implementazione è semplicissima.

 void SmqbSettings::beginDataChange()
 {
     blockSignals(true);
 } 

 void SmqbSettings::endDataChange()
 {
     blockSignals(false);
     emit dataChanged();
 }

L’idea è di bloccare il segnale fino a che non abbiamo finito le modifiche, poi di emetterne uno per tutti.

Salvare le impostazioni

Le impostazioni devono essere permanenti, ovvero chiudo l’applicazione, la riapro e la trovo nella stessa configurazione. Pertanto deve scrivere i settings in un file. Ricollegandoci a quanto fatto nella prima puntata, aggiungiamo il metodo pubblico saveSettings:

     void saveSettigs(const QString &iniFileName);

Ora con il solito ALT+INVIO implementiamo saveSettings selezionando “Add Definition in SmqbSettings.cpp”.

 void SmqbSettings::saveSettigs(const QString &iniFileName)
 {
     QSettings settings(iniFileName, QSettings::IniFormat);
     settings.beginGroup("Settings");
     settings.setValue("expensesImages", m_expensesImages);
     settings.endGroup();
 }

Il percorso del file ci viene passato dalla applicazione principale, poi apriamo un gruppo che chiamiamo “Settings” e andiamo a salvare il percorso in cui ci sono le immagini. Naturalmente non dimentichiamoci di fare ALT+INVIO su QSettings per aggiungere #include <QSettings>.

Recuperare le impostazioni

Aggiungiamo il metodo pubblico loadSettings:

     void loadSettings(const QString &iniFileName);

Procediamo con l’implementazione, leggendo dal file che riceviamo come argomento, usando il gruppo Settings e avendo cura di emettere il signal dataChanged() alla fine della lettura.

 void SmqbSettings::loadSettings(const QString &iniFileName)
 {
     QSettings settings(iniFileName, QSettings::IniFormat);
     settings.beginGroup("Settings");
     m_expensesImages = settings.value("expensesImages")
                            .toString();
     settings.endGroup();

     emit dataChanged();
 }

Implementiamo il tab delle impostazioni

Come facciamo ad implementare una casella di testo in cui impostare il percorso del file? La soluzione più semplice è mettere un controllo direttamente nella prima pagina del QTabWidget, quella che abbiamo chiamato tabSetup. Ma in questo modo, fatto salvo il vantaggio evidente di farlo in 3 secondi, abbiamo due svantaggi. Il primo è che il gruppo di controlli non è riutilizzabile, essendo incorporato nella finestra di dialogo principale. Il secondo è che il codice di maindialog.cpp diventa veramente tanto e difficile da gestire.

Pertanto, io in genere creo un pannello e lo appiccico dentro al tab impostazioni. Questo pannello in realtà è un QWidget, che abbrevieremo con Wgt, e per inserirlo nel tab basta usare “promote”, direttamente da QtCreator. Mettiamoci all’opera!

Sempre con File, New File or Project, selezioniamo Qt, e poi Qt Designer Form Class.

Tra tutti i template selezioniamo Widget, questo passaggio è essenziale, e nella pagina successiva impostiamo il nome: SmqbSettingsWgt. Vi consiglio di usare esattamente il nome della classe anche per SmqbSettingsWgt.h, SmqbSettingsWgt.cpp e SmqbSettingsWgt.ui e poi concludere la procedura.

Ora inserite:

  • una QLabel chiamata lblExpensesPath in cui scriverete: “Cartella con le immagini degli scontrini”
  • un QLineEdit chiamata txtExpensesPath
  • un QPushButton chiamato cmdExpensesPath con scritto “…”

Raggruppate il tutto con un QHBoxLayout e preparate un secondo gruppo formato da:

  • un pulsante Applica che chiameremo cmdApplySettings
  • un pulsante Annulla che chiameremo cmdResetSettings
  • un pulsante Importa che chiameremo cmdImportSettings
  • un pulsante Esporta che chiameremo cmdExportSettings

Nuovamente raggruppiamo il tutto in un QHBoxLayout, mettiamo un QSpacerItem tra i due gruppi e applichiamo un QVBoxLayout. Se ci siamo capiti bene questo è quello che dovreste vedere:

Implementiamo il codice

Premete SHIFT + F4 e saltate nel codice che si comincia! Ora F4 e dichiariamo le proprietà. Questo widget deve editare i dati contenuti in una classe di tipo SmqbSettings, pertanto aggiungiamo subito il riferimento a SmqbSettings.h e già che ci siamo anche alla classe QPointer.

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

Aggiungiamo una proprietà privata m_model che fa riferimento alla classe con i dati.

 QPointer<SmqbSettings> m_model;

QPointer è una classe interessante perchè qualora l’oggetto a cui fa riferimento venisse distrutto, il suo puntatore si annulla. Si usa praticamente come fosse un puntatore, solo che nella dichiarazione del template si mette la classe non il suo puntatore (SmqbSettings non SmqbSettings*). Ora servono la setter e la getter per accedere al valore, pertanto via con un altro ALT+INVIO, Create Setter and Getter.

Aggiornare la GUI

Ora che ho un riferimento ad un oggetto SmqbSettings, per allineare il suo contenuto alla GUI creo due metodi protected:

 protected:
     void modelRead();
     void modelWrite();

Il metodo modelRead accede alla proprietà expensesImage e la scrive nella casella di testo txtExpensesImages solo se il modello non è nullo.

 void SmqbSettingsWgt::modelRead()
 {
     if(!m_model)
         return;
     ui->txtExpensesPath->setText(
         m_model->expensesImages());
 }

Aggiornare il modello

Analogamente modelWrite() salva il dato solo se il modello non è nullo. Poi, c’è da considerare un piccolo problemuccio…

Non possiamo limitarci a scrivere il nuovo valore nel modello, perchè questo provoca l’emissione di un evento dataChanged() che forza l’aggiornamento della nostra GUI e se abbiamo n parametri, ogni volta aggiorneremmo la GUI. Per questo abbiamo creato i metodi beginDataChange() / endDataChange(); è tempo di usarli.

 void SmqbSettingsWgt::modelWrite()
 {
     if(!m_model)
         return;
     
     m_model->beginDataChange();
     m_model->setExpensesImages(
         ui->txtExpensesPath->text());
     m_model->endDataChange();
 }

Abbiamo quasi finito. Dobbiamo solo collegare il pulsante Applica a modelWrite():

 void SmqbSettingsWgt::on_cmdApplySettings_clicked()
 {
     modelWrite();
 }

E il pulsante Annulla a modelRead():

 void SmqbSettingsWgt::on_cmdResetSettings_clicked()
 {
     modelRead();
 }

Standard Pixmap per i pulsanti

Ora per completare l’implementazione dei pulsanti Applica e Annulla, aggiungiamo loro due icone prendendole dalle standard Pixmap presenti in Qt. Queste icone sono accessibili attraverso la classe QStyle e si adeguano in base al sistema operativo, permettendo alla nostra applicazione di avere un look n feel nativo.

Per prima cosa aggiungiamo il riferimento a QStyle

 #include <QStyle>

Poi nel costruttore della classe aggiungiamo:

 ui->cmdApplySettings->setIcon(
         style()->standardPixmap(QStyle::SP_DialogApplyButton));
     ui->cmdResetSettings->setIcon
         (style()->standardPixmap(QStyle::SP_DialogCancelButton));

In questo modo associamo l’icona SP_DialogApplyButton al tasto Applica e l’icona SP_DialogCancelButton al tasto Annulla.

Servirebbe un elenco delle icone gestite, vero? Lo troverete nel mio prossimo libro… prima che il decennio finisca…

Integriamo SmqbSettingsWgt

Tenete duro, siamo quasi arrivati a testare il giocattolo!

Ora spostiamoci nella finestra principale (maindialog.ui), selezioniamo il primo tab (tabSetup) e con un click con il tasto destro attiviamo “Promote to …”

Ora inserite il nome della classe da “Promuovere al posto QWidget”. Questa classe è la nostra SmqbSettings.

Premere Add e poi Promote e il gioco è fatto. Per essere sicuri che tutto sia ok, basta controllare nell’albero degli oggetti. Ora tabSetup non è più di tipo QWidget ma di tipo SmqbSettings.

Forza, possiamo compilare e controllare che il pannello impostazioni sia corretto:

Creare m_settings

Fermi, non funziona ancora! Manca ancora una istanza della classe SmqbSettings.

Apriamo mainwindow.h e aggiungiamo i riferimenti a QPointer e SmqbSettings

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

Creiamo un oggetto m_settings

     QPointer<SmqbSettings> m_settings;

Dai, dai, ci siamo, adesso F4 e saltiamo in mainwindow.cpp e creiamo finalmente una istanza della classe SmqbSettings e colleghiamola al widget SmqbSettingsWgt incorporata nel pannello tabSetup; è molto più semplice a dire che a farsi!

 m_settings = new SmqbSettings;
 ui->tabSetup->setModel(m_settings);

È molto importante che il metodo loadSettings() venga invocato dopo aver creato m_settings e dopo aver collegato m_settings al tabSetup. Giusto per essere chiari, il codice va scritto esattamente in quest’ordine:

     ui->setupUi(this);
     m_settings = new SmqbSettings;
     ui->tabSetup->setModel(m_settings);
     loadSettings();

Distruggere i settings

È bene ricordare che abbiamo creato m_settings con una new, pertanto sarebbe bene cancellarlo con delete. Pertanto completiamo il distruttore della classe principale:

 MainWindow::~MainWindow()
 {
     saveSettigs();
     delete m_settings;
     delete ui;
 }

Anche in questo caso la sequenza del codice è essenziale! Non vorrete salvare i settings dopo aver distrutto m_settings!

Salvare e ripristinare le impostazioni

Per il salvataggio dobbiamo modificare saveSettings() aggiungendo:

     if(m_settings)
         m_settings->saveSettigs(iniFilePath());

Notare il test su m_settings, che essendo un puntatore va prima controllato. In gergo questo si chiama “defensive programming”.

Per leggere le impostazioni modifichiamo loadSettings(), sempre testando che m_settings sia valido:

     if(m_settings)
         m_settings->loadSettings(iniFilePath());

Diamo fuoco alle polveri

Va bene dai compiliamo e testiamo, ma vi avviso, il risultato è alquanto deludente. Provate ad impostare un percorso a caso nel tab Impostazioni, premere Applica, chiudere l’applicazione e riaprirla.

Non succede niente giusto? Bene premete Annulla e compare il testo che avete salvato.

Come mai?

Semplice perché quando il file di configurazione viene caricato, il modello viene aggiornato, viene emesso un evento dataChanged(), ma nessuno è collegato quindi la finestra di dialogo non si aggiorna.

Colleghiamoci a dataChanged()

Apriamo SmqbSettingsWgt.cpp e modifichiamo il metodo setModel() inserendo una lambda expression che si connette all’evento dataChanged.

Per prima cosa controlliamo che effettivamente il modello sia diverso da quello corrente:

 if(m_model == model)
         return;

Poi controlliamo se attualmente abbiamo già un modello collegato, nel qual caso dobbiamo forzare la disconnessione.

  if(m_model)
         disconnect(m_model, nullptr, nullptr, nullptr);

Ora posso assegnare il nuovo modello

  m_model = model;

Per ultimo, se il nuovo modello è valido, collego una lambda expression che forza l’aggiornamento della finestra di dialogo usando il metodo modelRead().

 if(m_model)
         connect(m_model, &SmqbSettings::dataChanged, [=](){
             modelRead();
         });

Se non sapete cosa sono le lambda expression, vi do un indizio… sono delle funzioni senza nome, che usano tutte i tipi di parentesi disponibili, e che permettono di risparmiare un sacco di codice.

Compilate ed eseguite ed adesso il percorso delle immagini viene visualizzato correttamente appena l’applicazione parte.

Il pulsante cmdExpensesPath

Adesso che riusciamo a salvare e rileggere il percorso, è tempo di creare la finestra di dialogo per la ricerca. Impostiamo subito l’icona per il pulsante, aggiungendo al costruttore della classe il codice:

 ui->cmdExpensesPath->setIcon(
         style()->standardPixmap(QStyle::SP_DialogOpenButton));

Poi SHIFT+F4 e passiamo al file SmqbSettings.ui, selezioniamo il pulsante e aggiungiamo lo slot on_cmdExpensesPath_clicked(). Ricordatevi di aggiungere la classe QFileDialog ed impostiamo il codice nel modo seguente:

 void SmqbSettingsWgt::on_cmdExpensesPath_clicked()
 {
     QString path = ui->txtExpensesPath->text();
     path = QFileDialog::getExistingDirectory(this, 
                "Seleziona la cartella con gli scontrini", path);
     if(path.isNull())
         return;
     ui->txtExpensesPath->setText(path);
 }

Finalmente possiamo impostare il percorso della cartella con gli scontrini.

Colleghiamo il pulsante Importa

Il pulsante Importa serve per caricare le impostazioni da un un percorso diverso da quello predefinito. Iniziamo con impostare la sua icona, inserendo nel costruttore della classe un riferimento alla standardPixmap SP_DialogOpenButton:

     ui->cmdImportSettings->setIcon(
         style()->standardPixmap(QStyle::SP_DialogOpenButton));

Quanto alla sua implementazione, basta usare SmqbSettings::loadSettings(), poi il segnale dataChanged() emesso dalla classe, automaticamente aggiornerà la nostra finestra di dialogo.

 void SmqbSettingsWgt::on_cmdImportSettings_clicked()
 {
     if(!m_model)
         return;
 
    QString filter = tr("Config File (*.ini)");
     QString file = QFileDialog::getOpenFileName(this, 
             "Seleziona il file ini da leggere", QString(), filter);
     if(file.isEmpty())
         return;
 
     m_model->loadSettings(file);
 }

Colleghiamo il pulsante Esporta

Questa è proprio l’ultima modifica. Il pulsante esporta serve per salvare il file di configurazione in un percorso diverso da quello standard. In particolare la mia idea è di salvarlo nella cartella del progetto SpendiMenoQuantoBasta, come SmqbSettings.ini in modo da poterlo usare come file di default nei casi in cui il file SpendiMenoQuantoBasta.ini non sia presente. Questo trucco è utile durante lo sviluppo, ad esempio quando si compila in release, oppure con un diverso kit, perché non dobbiamo re-inserire tutte le impostazioni a mano. È chiaro che un parametro non è un problema, ma la nostra applicazione è destinata ad averne 100, pertanto prepariamola al meglio!

Iniziamo con l’icona, che va inserita nel costruttore della classe:

    ui->cmdExportSettings->setIcon(
         style()->standardPixmap(QStyle::SP_DialogSaveButton));

Poi implementiamo lo slot, al solito controllando che m_model sia valido, poi invocando modelWrite() in modo da allineare il modello alle ultime modifiche, e poi chiediamo all’utente dove salvare il file, usando il metodo statico QFileDialog::getFileSaveName().

 void SmqbSettingsWgt::on_cmdExportSettings_clicked()
 {
     if(!m_model)
         return;
     modelWrite();
     QString file = QFileDialog::getSaveFileName(
         this, "Seleziona come salvare il file ini");
     if(file.isEmpty())
         return;
     m_model->saveSettigs(file);
 }

Ecco la nostra finestra di dialogo completa di bellissime icone…

Incorporare il file ini nel progetto

Bene ora usiamo l’applicazione stessa per salvare il file di impostazioni nella cartella del progetto, chiamandolo SmqbSettings.ini

Poi aggiungiamo al progetto un nuovo file usando File -New File or Project, selezionando Qt, Qt Resource File e chiamiamo il file Resources.

Inseriamo il nostro file ini nel contenitore Resources. Io in genere lo metto senza alcun prefisso, pertanto l’albero risultante è il seguente:

In questo modo, ogni volta che compiliamo, includiamo nel progetto anche un file di configurazione di default.

Come facciamo a richiamarlo? Basta modificare MainWindow::loadSettings(), andando prima a testare se il file esiste tramite QFileInfo. Se il file non esiste, usiamo il file incorporato nel codice, accedendovi tramite :/SmqbSettings.ini. Per sapere il percorso con cui accedere ad una risorsa, basta cliccare con il tasto destro sul nodo del file, come nello screenshot seguente:

Prima di compilare, non dimenticatevi di usare iniFile e non iniFilePath() come argomento della classe settings e della chiamata a loadSettings() di m_settings.

Ecco come diventa la funzione

 void MainWindow::loadSettings()
 {
     QString iniFile = iniFilePath();
     if(!QFileInfo::exists(iniFilePath())){
         iniFile = ":/SmqbSettings.ini";
     }
     
     QSettings settings(iniFile, QSettings::IniFormat);
     restoreGeometry(settings.value("Geometry", saveGeometry())
                         .toByteArray());
     ui->tabWidget->setCurrentIndex(
         settings.value("SelectedTab", 0).toInt());
     
     if(m_settings)
         m_settings->loadSettings(iniFile);
 }

Provate a cancellare tutta la cartella con il build del progetto e a ricompilare. Vedrete che il percorso degli scontrini, viene ripristinato, non la posizione e la dimensione della finestra di dialogo.

Conclusione

Questo sprint è stato un po’ lunghetto, circa il triplo della prima puntata, ma ci tenevo a completare la struttura delle impostazioni in modo da non doverci più tornare.

Nella prossima puntata finalmente caricheremo le immagini. Iniziate ad accumularle, altrimenti non avrete niente da testare.

Alla prossima.

Spendi Meno Quanto Basta Parte 1

Posso usare Qt per controllare le mie spese personali?

Da dove iniziamo?

Da inizio anno il saldo delle mie entrate / uscite è negativo. Non che debba chiedere aiuto allo stato, ma alcune spese straordinarie hanno fatto saltare i conti e nel lungo periodo il trend non è sostenibile, pertanto devo fare qualcosa.

Leggendo vari siti di finanza personale e sviluppo personale, il primo suggerimento che ho trovato è di scrivere tutte le spese (tante) e le entrate (poche) e poi identificare quali sono le spese comprimibili. Già la pigrizia di dover registrare la spesa aiuta ad evitare spese superflue; ovviamente sto parlando del caffè al bar, o l’ultimo gadget su Amazon, non dei libri per la scuola dei figli.

Naturalmente preferirei trascrivere il tutto in una applicazione, non su tavolette di argilla, pertanto per prima cosa ho cercato una app.

Seguendo i suggerimenti vari ho fatto delle prove, ma nessuna app mi ha veramente soddisfatto. Non ho provato quelle a pagamento, perché capite bene che se devo spendere di più per risparmiare il progetto parte male.

E se lo facessi con Qt?

Ecco che a questo punto mi sono detto: ma perché non fare una mia applicazione usando Qt, in modo da farmela Divertente e Comoda Quanto Basta?

Analisi del progetto.

Servono 2 flussi, Ingressi e Uscite. Non sono in grado di capire le registrazioni fatte con partita doppia, nonostante il metodo risalga al 1494 e che a descriverlo per bene fu un frate italiano (leggetevi la pagina su Wikipedia). Io voglio sapere alla fine del mese quanto ho incassato e quanto ho speso, cosicché potrò raggruppare le spese e vedere cosa posso ridurre.

Per gli ingressi non serve molto, posso inserirli a mano, in genere è il solo stipendio, per le uscite, vorrei avere un meccanismo di supporto, che potrebbe essere:

  1. Raccolgo le immagini degli scontrini fiscali usando lo smartphone
  2. Faccio arrivare le immagini sul mio PC
  3. Carico le nuove immagini in una applicazione
  4. Salvo l’immagine nel database della applicazione, con data e descrizione della transazione
  5. Registro il movimento e se serve, lo scompongo in più movimenti con diverse categorie.
  6. Classifico la spesa in una categoria e sottocategoria predefinite

Serve un nome

Perché il progetto funzioni, devo avere voglia di alzarmi la mattina e usare la mia applicazione. Quindi anche il nome deve essere interessante! Che ne dite di SMQB? Ovvero Spendi Meno Quanto Basta.

Raccolta degli scontrini

Sarebbe bellissimo fare una app che sullo smartphone mi permette di fare delle foto, mettere dei commenti, salvarle nel cloud e poi accedere al cloud con il mio programma, eccetera eccetera. Ma vorrebbe dire iniziare nel 2030! Perchè non vedere se posso riutilizzare il lavoro di qualcun altro? E mentre ero in attesa dal barbiere (altra spesa che forse a tendere potrei comprimere!), ho avuto una illuminazione!

Posso usare l’app Google Drive, che permette di scattare una foto e salvarla in una cartella in rete. Se la cartella è tra i preferiti, selezionarla è velocissimo. Poi imposto la sincronizzazione della cartella, o di tutto il contenuto di Google Drive, sul mio pc, in modo che ogni volta che scatto una foto, la foto finisce nel mio computer.

Dopo aver creato la cartella: Scontrini sul mio Google Drive, la parte di raccolta delle immagini è risolta.

Ho scelto Google Drive perchè ho visto che è disponibile per macOS, per Windows e con applicazioni di terze parti anche per Linux, e poi perchè è la prima app con cui sono riuscito a fare delle foto senza riempire iCloud di immagini non correlate con i miei ricordi.

Non escludo che anche altre app possano fare lo stesso. Per favore aggiungete queste informazioni nei commenti.

Sprint 01: Creare l’applicazione

Ora prendiamo Qt ed iniziamo ad impostare la nostra applicazione. Si parte creando una Qt Widget Application che chiameremo SpendiMenoQuantoBasta. Poi nella mainwindows andiamo a mettere un tabWidget con 5 tab che chiameremo:

  • tabSetup (ovvero Impostazioni)
  • tabIncome (ovvero Entrate)
  • tabExpense (ovvero Uscite)
  • tabReport (ovvero Report)
  • tabAbout (ovvero Info)

Prima di compilare, impostiamo il parametro windowTitle della finestra principale come: “Spendi Meno Quanto Basta”.

Adattiamo il tabWidget alla finestra di dialogo con un layout verticale e compilando dovreste vedere:

Salvare le impostazioni

Gestiamo subito il salvataggio delle impostazioni della form principale creando 3 metodi protetti in mainwindow.h:

protected:    
    QString iniFilePath(); 
    void loadSettings();
    void saveSettigs();

Il metodo iniFilePath() è semplicissimo, e serve per calcolare il percorso in cui salvare le impostazioni. Io preferisco metterle in un file .ini a fianco dell’eseguibile (o del file .app nel caso macOS). Ecco il codice

 QString MainWindow::iniFilePath()
 {
     QFileInfo FI(qApp->applicationFilePath());
 #if defined Q_OS_MAC
     return FI.absolutePath() + "../../../../" + FI.baseName()
            + ".ini";
 #elif defined Q_OS_WIN
     return FI.absolutePath() + "/" + FI.baseName() + ".ini";
 #elif defined Q_OS_LINUX
     return FI.absolutePath() + "/" + FI.baseName() + ".ini";
 #endif
 }

Non dimenticate di aggiungere #include <QFileInfo> alla lista delle inclusioni. Basta selezionare QFileInfo e premere ALT+INVIO per avere l’opzione come suggerimento del tool di refactoring.

Provate a farlo, è fantastico!

Anche i metodi loadSettings() e saveSettings() si scrivono in poche righe. Per il momento salviamo la geometria della finestra di dialogo e l’indice corrente del controllo con i tab, poi li estenderemo anche alle altre impostazioni.

 void MainWindow::loadSettings()
 {
     QSettings settings(iniFilePath(), QSettings::IniFormat);
     restoreGeometry(settings.value("Geometry", saveGeometry())
                         .toByteArray());
     ui->tabWidget->setCurrentIndex(settings
                         .value("SelectedTab", 0).toInt());
 }
 
 void MainWindow::saveSettigs()
 {
     QSettings settings(iniFilePath(), QSettings::IniFormat);
     settings.setValue("Geometry", saveGeometry());
     settings.setValue("SelectedTab", 
                    ui->tabWidget->currentIndex());
 }

Anche in questo caso, non dimenticate di aggiungere #include <QSettings>, sempre usando ALT+INVIO per velocizzare l’operazione e non perdere il focus.

Ora invochiamo i metodi loadSettings() e saveSettings(), dopo aver costruito la user interface:

MainWindow::MainWindow(QWidget *parent) :
     QMainWindow(parent),
     ui(new Ui::MainWindow)
 {
     ui->setupUi(this);
     loadSettings();
 }

E prima di distruggere il tutto e chiudere il programma

MainWindow::~MainWindow()
 {
     saveSettigs();
     delete ui;
 }

Nella prossima puntata

Per questa puntata è tutto. Abbiamo solo scalfito la superficie, me ne rendo conto, ma abbiamo messo delle basi solide per fare la nostra applicazione Spendi Meno Quanto Basta. Seguitemi e ci divertiremo.

Attendo con ansia i vostri commenti e i vostri suggerimenti, per ora divertitevi a controllare che effettivamente le impostazioni vengano salvate e ripristinate quanto rilanciate il programma.

Nella prossima puntata vedremo come creare la form con le impostazioni.

Il primo database

Che lo crediate o no, i database so qui per restare

Mentre preparavo la presentazione del QtDay 2019, stavo anche leggendo il libro 21 Lezioni per il XXI secolo di Yuval Noah Harari, che è sempre una fortissima fonte di ispirazione.

E pensavo, ma davvero, nel 2019, un talk sui database? Sono cose vecchie, me ne occupavo nel 1990, al tempo si parlava di Modello Entità Relazione e di Tuple, ora sono argomenti vecchi!

Vecchi, ma quanto vecchi?

E qui mi è venuto in mente un tema che avevo trovato in uno dei libri di Yuval Noah Harari, ovvero che l’invenzione della scrittura serviva essenzialmente per registrare il pagamento di tributi e imposte.

La storia in realtà è più complessa, e non credo ci sia stato un giorno in cui la scrittura è stata inventata, ma credo piuttosto ad un processo per fasi, partendo dalle prime incisioni rupestri fino ad arrivare alle tavolette di argilla. Se siete interessati, al solito Wikipedia offre molti spunti: https://it.wikipedia.org/wiki/Storia_della_scrittura. E l’applicazione della scrittura è andata ben oltre la registrazione di entrate e uscite, arrivando a rendere immortali alcune opere dell’ingegno umano.

Ma l’idea che molti dei testi risalenti ai Sumeri, fossero in realtà delle registrazioni di transazioni, mi fece venire in mente questa slide per il QtDay, dove ho rappresentato una tavoletta di pietra risalente al 2500 A.C.

Durante il talk non ho avuto modo di approfondire l’argomento, ma vale la pena di entrare nel dettaglio.

La tavoletta appartiene alla collezione Schoyen, https://www.schoyencollection.com/23-religions/extinct-religions/23-1-sumerian/gift-adab-priestess-ms-3029 e rappresenta un elenco di donazioni fatte al potente dio Adab, in occasione della nomina della sacerdotessa suprema.

Questa immagine mi ha colpito perchè se guardiamo la tavoletta con le righe in verticale, fa venire in mente una tabella, un Excel diremmo oggi.

Se invece la giriamo e mettiamo le righe in orizzontale, come si vede in Wikipedia, mi ricorda gli esempi di database che si facevano con i primi computer, in cui il database aveva dei record a lunghezza fissa e veniva scritto in modo sequenziale su nastro magnetico.

Quindi se i database esistevano già nel 2500 a.C., vecchi o non vecchi, sono destinati a restare.

I database oggi

Se ci pensate oggi tutto funziona con un database, tutti i siti internet hanno un database, tutte le applicazioni hanno un database, anche per allenare l’intelligenza artificiale si usano database. Di fatto il testo che sto scrivendo viene salvato in un database, le banche non sono altro che enormi database.

Anzi si continua a ripetere che i dati saranno il petrolio dei prossimi anni, e chi potrà accedere ai dati potrà fare cose incredibili.

E dove li metti i dati? Non sulle tavolette di pietra, ma nei database.

Per oggi è tutto, vi aspetto la prossima settimana.

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!

Qt è il futuro

Eccoci qua, finalmente ho creato un sito dedicato a Qt 5 Quanto Basta.

Giusto per fare un poco di marketing, vi ricordo che il mio libro (Qt 5 Quanto Basta) è disponibile sia in formato cartaceo che in formato kindle su Amazon.it e su moltissimi altri siti e librerie.

Dopo la prima introduzione doverosa, vorrei iniziare questa avventura con un messaggio chiaro:

Io studio Qt perchè sono convinto che sia il futuro!

Ma cos’è Qt?

Qt è un framework per lo sviluppo di applicazioni… Un fra… che?

Facciamola semplice! Qt è un pacchetto formato da una libreria software che si chiama Qt appunto, e un applicativo che si chiama Qt Creator con il quale posso fare delle applicazioni.

Perchè è diverso da Visual Studio

Perchè con Visual Studio posso creare solo applicazioni per il sistema operativo Windows, mentre con Qt posso creare applicazioni per:

  • Windows
  • Linux
  • macOS
  • Dispositivi mobili iOS
  • Dispositivi Android
  • Dispositivi Embedded quali pannelli operatore, cruscotti di automobili.

E da Java?

Anche Java è pensato per funzionare su diverse piattaforme, usando un approccio diverso. Con Qt compilo per ogni piattaforma e quindi ho un comportamento identico alle altre applicazioni e prestazioni ottimizzate. Con Java, compilo un meta-linguaggio che poi viene interpretato da una macchina virtuale, e con svariate ottimizzazioni che nel tempo sono state implementate raggiunge prestazioni ragguardevoli, ma tendenzialmente usando molta più memoria e molta più CPU, e lasciando spesso l’impressione di non essere una applicazione nativa.

Si sì, ma non lo usa nessuno…

Appunto, questo è il motivo! È così potente che lo useranno tutti, e noi saremo i primi!

Questo era vero nel 2012, quando ho iniziato. Ormai è molto usato. Ad esempio quasi tutti i nuovi cruscotti sono fatti in Qt, a partire da quello di Tesla. O per esempio tutte le auto tedesche usano Qt.

Molti progetti open source usano Qt, come per esempio Video Lan, OBS, GNU Octave, KDE. Valutate voi stessi…

Ci sono anche software commerciali che usano Qt, come per esempio Autodesk Maya

E ci sono sistemi embedded! Un caso esemplare era Tom Tom, ma ormai credo di essere tra i pochi a sapere cosa è. Il decoder di Sky usa Qt.

Quali sono i vantaggi?

Il primo è che per impararlo non devo fare grossi investimenti, se non il tempo necessario a studiarlo. Infatti è disponibile una versione open source che è perfetta per imparare. Certo da qualche anno anche Visual Studio è disponibile come Community Edition, ma non è sempre stato così, e non è detto che lo sarà al prossimo cambio di amministratore delegato.

Il secondo, è che se sviluppo un progetto oggi, posso star sicuro che qualsiasi piattaforma la gente userà tra 10 anni (o anche 20), Qt potrà essere compilato per quella piattaforma, sia essa derivata da Linux, Apple, Microsoft o Android. Conoscete altre piattaforme? Bene state certi che quelli di Qt ci stanno già lavorando.

Concludendo

Fatevi un giro su www.qt.io per farvi una idea di che cosa sia Qt, scaricate il l’anteprima del mio libro da Amazon ed approfittate della calura per chiudervi in casa, condizionatore acceso, birra e… Qt.

Alla prossima settimana!