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.

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 *