Nelle lezioni precedenti abbiamo affrontato alcuni aspetti della definizione di interfacce grafiche con Qt widgets, concentrandoci prevalentemente sulle modalità di layouting all'interno di una singola finestra o form.
Solitamente, l'interfaccia grafica di un'applicazione ricorre all'uso di finestre o dialog secondari per l'espletamento di alcune funzioni specifiche. In questa lezione vedremo come inizializzare e mostrare a schermo finestre di questo tipo, e come la loro presenza interagisce con le funzionalità della finestra principale.
Finestre modali e non modali
Ogni istanza di QWidget
può essere mostrata a schermo invocando semplicemente il metodo show()
. In questo caso, il window manager del sistema operativo prende in carico la richiesta di disegno aggiungendo il bordo ed i pulsanti della finestra prima di mostrarla a schermo come finestra non modale.
L'uso di una finestra non modale consente di preservare la reattività della finestra principale dell'applicazione, permettendo all'utente di interagire allo stesso tempo con entrambe. In altre parole, l'invocazione del metodo show()
non è bloccante per l'event loop della finestra principale.
A volte, però, questo comportamento deve essere modificato per forzare l'utente ad interagire momentaneamente soltanto con una finestra secondaria per motivi specifici.
Per questa eventualità, il framework Qt predispone la classe QDialog
che estende QWidget
con il metodo exec()
. Invocare exec()
su una istanza di tipo QDialog
o di una sua derivata consente di mostrarla come finestre modali.
Per mostrare la differenza tra le due modalità di interazione analizzeremo una applicazione composta da una finestra principale che contiene soltanto due pulsanti, mostrata nella figura seguente:
Finestra principale con due pulsanti per la apertura di finestre modali e non modali (click per ingrandire)
Cliccando sul pulsante a sinistra, denominato modalButton, si apre una finestra vuota modale, mentre cliccando su quello a destra, denominato modelessButton, si apre una finestra vuota non modale. Le istruzioni necessarie per ottenere questo effetto sono incluse in due slot definiti nella classe MainWindow
, collegati rispettivamente ai segnali clicked()
dei due pulsanti.
In questo caso è stata usata la funzionalità propria del designer per aggiungere slot ad una form, come mostrato in figura.
Cliccando con il tasto destro su una widget e selezionando Goto slot..., è infatti possibile scegliere il segnale al quale agganciare lo slot, tra quelli disponibili per quella particolare widget. Lo slot verrà aggiunto in maniera automatica alla classe della finestra con un'implementazione vuota.
La connessione tra segnale e slot, in questo caso, non è esplicitamente definita mediante l'uso della funzione connect()
. Essa viene infatti effettuata da moc, il quale connette automaticamente segnali e slot il cui nome segue la convenzione "on_<nome-istanza>_<nome-segnale>". Gli slot definiti mediante la funzionalità Goto slot... seguono tale nomenclatura.
Il file header della classe MainWindow
, dopo l'aggiunta degli opportuni slot, conterrà il codice il seguente:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_modalButton_clicked();
void on_modelessButton_clicked();
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
Il file mainwindow.cpp contiene la definizione dei metodi e slot della classe:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDialog>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_modalButton_clicked()
{
QDialog modalWindow;
modalWindow.setWindowTitle("Modal dialog");
modalWindow.setMinimumSize(QSize(300, 150));
modalWindow.setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::MinimumExpanding);
modalWindow.exec();
}
void MainWindow::on_modelessButton_clicked()
{
QWidget* modelessWindow = new QWidget();
modelessWindow->setWindowTitle(tr("Modeless Window"));
modelessWindow->setMinimumSize(QSize(300, 150));
modelessWindow->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::MinimumExpanding);
modelessWindow->setAttribute(Qt::WA_DeleteOnClose); // previene memory leak
modelessWindow->show();
}
A parte il tipo differente, QDialog
e QWidget
, l'implementazione dei due slot si differenza per un dettaglio fondamentale.
Nel primo caso, quello di una finestra modale, l'istanza di nome modalWindow viene allocata a stack. Poiché l'invocazione del metodo exec()
è bloccante per l'event loop della finestra principale, la distruzione dell'istanza modalWindow è ritardata fino all'uscita dal metodo on_modalButton_clicked()
che ne costituisce l'ambito di visibilità.
Quando l'utente chiude la finestra, il metodo exec()
termina, restituendo un valore numerico che analizzeremo nel seguito, ed il controllo di flusso viene restituito all'event loop della finestra principale.
Fin tanto che ciò non accade, l'istanza rimane valida e l'utente può interagire liberamente con essa. L'uso delle altre finestre dell'applicazione, ad esempio la finestra principale, è invece inibito.
Nel caso della finestra non modale, invece, si usa una widget allocata dinamicamente. L'invocazione del metodo show()
infatti non è bloccante, pertanto dopo che la finestra è stata mostrata a schermo l'esecuzione del metodo on_modelessButton_clicked()
prosegue, in questo caso con l'uscita dal metodo stesso. Se l'istanza modelessWindow non fosse allocata dinamicamente, il suo ambito di visibilità sarebbe vincolato a quello del metodo in cui è definita, pertanto essa sarebbe immediatamente distrutta, senza dare all'utente possibilità di interazione.
Per evitare però un memory leak dovuto alla mancata deallocazione della finestra dopo la sua chiusura da parte dell'utente, ad essa viene aggiunto un particolare attributo che ne forza la distruzione alla chiusura mediante l'invocazione del metodo setAttribute()
con argomento Qt::WA_DeleteOnClose
.
Questo artificio è necessario perché in questo caso non vi sono altri riferimenti alla finestra non modale se non quello definito all'interno dello slot stesso.
L'effetto ottenuto cliccando sui pulsanti è illustrato nelle immagini seguenti. La finestra modale previene l'interazione con la finestra principale fino alla sua chiusura.
La finestra non modale consente di interagire con la finestra principale che, ad esempio, può essere portata in primo piano.
Valore di ritorno per finestre modali
Il metodo exec()
restituisce i valori 1 e 0 rispettivamente per segnalare il fatto che l'utente abbia completato l'operazione espletata dalla finestra o la abbia interrotta precocemente, ad esempio cliccando sul pulsante Annulla. Nell'esempio che abbiamo riportato in questa lezione, la finestra modale è vuota e non implementa nessuna logica particolare. Il valore restituito da exec()
quindi non viene preso in considerazione.
Nel caso reale di una finestra dotata di controlli e funzionalità, è possibile definire il valore di ritorno invocando opportunamente i metodi accept()
e reject()
della classe QDialog
.
Entrambi hanno l'effetto di chiudere la finestra, ma consentono di discriminare il fatto che l'operazione sia da considerarsi conclusa o meno, come mostrato nel frammento seguente:
MyDialog myDialog;
if (myDialog.exec() == QDialog::accepted)
{
// operazione completata con successo,
// ad esempio cliccando su Ok.
}
else
{
// operazione annullata o interrotta,
// ad esempio cliccando su Annulla.
}
Finestre modali del framework
Il modulo Qt Widget include un set eterogeneo di classi derivate da QDialog
per espletare alcune delle funzioni più comuni. Queste classi forniscono un set di API piuttosto esaustivo che consente, ad esempio, di mostrare un messaggio informativo o di errore (QMessageBox
), selezionare cartelle e file (QFileDialog
), selezionare un colore (QColorDialog
), immettere un input testuale o numerico (QInputDialog
), eccetra. Si rimanda alla documentazione ufficiale della classe QDialog
che enumera tutte le classi derivate per una descrizione completa della loro funzionalità.