Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Segnali e slot

Quando si programmano GUI sfruttando Qt, è indispensabile fare uso della programmazione ad eventi: ecco come sfruttarla tramite l'uso di segnali e slot.
Quando si programmano GUI sfruttando Qt, è indispensabile fare uso della programmazione ad eventi: ecco come sfruttarla tramite l'uso di segnali e slot.
Link copiato negli appunti

La gestione di segnali e slot è una delle principali funzionalità offerte della classe QObject. Nel contesto del framework Qt, infatti, è proprio tramite questo meccanismo che si realizza il paradigma della programmazione a eventi.

Da un punto di vista concettuale, un segnale è lo strumento che consente ad un oggetto di notificare un cambiamento di stato. L'emissione di un segnale quindi è un evento che può essere gestito mediante uno slot.

Per introdurre il meccanismo di segnali e slot esamineremo un esempio minimale che include solo due semplici classi che ereditano pubblicamente da QObject. In questo esempio, due istanze di esse svolgeranno rispettivamente il ruolo di generatore e gestore di eventi, consentendo di esaminare gli effetti dell'associazione tra i rispettivi segnali e slot.

Emissione di segnali

Una classe genera un evento mediante l'emissione di un segnale. In Qt un segnale è un particolare costrutto sintattico, molto simile all'invocazione diretta di un metodo, ma che presenta alcune peculiarità.

A parte le consuete sezioni public, protected e private, troviamo infatti anche una estensione sintattica propria di Qt, la sezione signals, all'interno della quale sono enumerati i segnali che una classe può emettere sotto forma di firme di metodi di classe.

I segnali di una classe hanno quindi un nome di funzione ed un elenco di argomenti, se necessario, che possono essere trasmessi all'istanza deputata alla gestione di un particolare evento. Tuttavia si noti che alla dichiarazione di un segnale non corrisponde alcuna implementazione, ed il tipo restituito è solitamente void. Un segnale infatti non è propriamente inteso per essere usato come un ordinario metodo di classe. La sua invocazione avviene mediante l'uso di un'altra parola chiave proria di Qt, emit che, seguita dal nome del segnale completo della sua lista di argomenti tra parentesi tonde, determina la generazione di un evento.

In questo esempio, la classe MyEmitter contiene un solo dato membro di tipo booleano. Quando tale valore viene modificato mediante l'invocazione del metodo changeSomething(), viene anche emesso un segnale per notificare l'avvenuto cambiamento di stato.

#ifndef MYEMITTER_H
#define MYEMITTER_H
#include <QObject>
// La classe MyEmitter emette un segnale
// quando il suo stato cambia.
class MyEmitter : public QObject
{
	Q_OBJECT
public:
	explicit MyEmitter(QObject *parent = nullptr)
		: QObject(parent)
	{
		something = false;
	}
	~MyEmitter() {}
	void changeSomething(bool val)
	{
		if (something != val)
		{
			something = val;
			emit somethingChanged(something); // emissione di un segnale
		}
	}
/* elenco di segnali */
signals:
	void somethingChanged(bool);
protected:
	bool something;
};
#endif // MYEMITTER_H

Ogni istanza della classe MyEmitter genererà quindi un evento che potrà essere gestito da una o più altre entità per le quali l'informazione rappresentata da tale cambiamento di stato è rilevante.

Definizione di uno slot

Uno slot è un metodo di classe deputato alla gestione di un particolare evento. In modo analogo alla dichiarazione di segnali, anche la definizione degli slot è relegata in apposite sezioni slots. Tuttavia a differenza del caso dei segnali, gli slot sono propriamente dei metodi di classe, cui corrisponde una implementazione, ed in quanto tali, essi possono avere un livello di accesso differente. Una classe che comprende la definizione di segnali quindi presenterà una o più sezioni slot qualificate con il modificatore public, protected o private.

In questo esempio, la classe MyHandler presenta uno solo slot pubblico la cui invocazione ha il solo effetto collaterale di stampare a schermo il valore dell'argomento booleano passato come input. Nelle lezioni successive approfondiremo l'uso di qDebug() come canale di output.

#ifndef MYHANDLER_H
#define MYHANDLER_H
#include <QObject>
#include <QDebug>
// La classe MyHandler ha uno slot
// con la stessa firma del segnale emesso dalla
// classe MyEmitter
class MyHandler: public QObject
{
	Q_OBJECT
public:
	explicit MyHandler(QObject *parent = nullptr) : QObject(parent) {}
	~MyHandler() {}
/* elenco di slot pubblici */
public slots:
	void onSomethingChanged(bool val)
	{
		qDebug() << "value changed to: " << val;
	}
};
#endif // MYHANDLER_H

L'implementazione di uno slot è del tutto simile a quella di un metodo di classe. Uno slot è in effetti invocabile anche direttamente mediante gli operatori -> e . esattamente come ogni altro metodo membro di una classe. Tuttavia, il loro uso tipico prevede che essi siano invocati in risposta alla manifestazione di un particolare evento.

Inoltre ad uno slot possono essere applicati tutti gli specificatori ed i qualificatori previsti dallo standard C++ per particolarizzare il comportamento di tale metodo rispetto all'istanza che lo invoca.

Connessione di segnali e slot

L'associazione di segnali e slot, siano essi definiti nella stessa classe o in classi differenti, avviene identificando in maniera univoca la istanza del mittente e quella del ricevente. Il metodo staticoconnect() è impiegato a tale scopo.

Nel listato seguente, il segnale di un'istanza MyEmitter è connesso allo slot di un'istanza della classe MyHandler.

#include <QCoreApplication>
#include "myemitter.h"
#include "myhandler.h"
int main(int argc, char *argv[])
{
	QCoreApplication a(argc, argv);
	MyEmitter  emitter;
	MyHandler receiver;
	// Associazione tra segnale e slot
	connect(&emitter,  &MyEmitter::somethingChanged,
			&receiver, &MyHandler::onSomethingChanged);
	emitter.changeSomething(true);
	return a.exec();
}

I primi quattro argomenti del metodo connect() sono in ordine:

  1. Il puntatore all'istanza della classe che emette un segnale.
  2. Il puntatore al segnale.
  3. Il puntatore all'istanza della classe il cui slot è deputato alla gestione dell'evento associato al segnale.
  4. Il puntatore al metodo membro della classe, decorato come slot.

La funzione connect() accetta inoltre un quinto parametro opzionale, la cui semantica verrà discussa nel seguito.

Come conseguenza dell'associazione tra segnali e slot, l'invocazione del metodo changeSomething() da parte dell'istanza emitter causerà l'invocazione dello slot onSomethingChanged() da parte dell'istanza receiver per effetto dell'emissione del segnale.

Firma e nomenclatura di segnali e slot

Segnali e slot devono avere firme compatibili, con riferimento alla lista di argomenti. Tipicamente la lista di argomenti di uno slot è identica a quella dei segnali che può gestire, tuttavia è anche ammesso il caso di slot la cui lista di argomenti sia più breve di quella del segnale ad esso associato. In questo caso infatti i parametri in eccesso vengono scartati, anche se rimane il requisito imprescindibile della concordanza di tipo.

Con la sintassi illustrata nell'esempio precedente la compatibilità tra segnali e slot viene verificata già a tempo di esecuzione.

A prescindere dal controllo formale dei tipi, il framework Qt inoltre adotta una convenzione ben precisa per la denominazione di segnali e slot. In particolare, il nome di un segnale è tipicamente un verbo espresso al participio passato (e.g. "clicked()"), mentre il nome di uno slot è composto dalla preposizione "on" ed il nome del segnale correlato (e.g. "onClicked()"). L'adozione a questa convenzione non è obbligatoria, tuttavia è consigliabile perchè aumenta l'uniformità e la leggibilità del codice.

Eventloop e affinità

Fin qui abbiamo analizzato il modo in cui possono essere effettuate connessioni di segnali e slot dal punto di vista del programmatore. In realtà, il funzionamento di segnali e slot è molto più articolato di quanto non appaia in queste poche righe di codice. Ancora una volta, buona parte del lavoro viene svolta per noi da parte di Moc che si occupa di registrare i segnali e gli slot associati, e predispone una serie di strutture dati atte al monitoraggio dell'emissione di segnali.

La gestione di eventi infatti è implementata mediante l'iterazione dello event loop del thread. Un event loop è una struttura di controllo di flusso che processa le coda di eventi generate dalle istanze di QObject che sono di competenza del thread stesso. Quando un segnale viene emesso, gli slot ad esso associati vengono inseriti nella coda di eventi del thread, per essere poi invocati ordinatamente in sequenza.

Di norma, l'esecuzione di uno slot è sincrona rispetto l'emissione del segnale. Ciò significa che all'emissione di un segnale corrisponde un immediato cambio di contesto per l'esecuzione dello slot corrispondente. Terminata l'esecuzione di quest'ultimo l'esecuzione riprende dall'istruzione immediatamente successiva all'emissione del segnale.

Questa logica si applica solo a quegli scenari in cui le istanze del mittente e del ricevente risiedono nello stesso thread. Per applicazioni monoprogrammate, in cui esiste un unico thread, l'esecuzione sincrona di segnali e slot è un comportamento ben definito.

Tuttavia, soprattutto nel caso di applicazioni dotate di interfaccia grafica, non è insolito avere la necessità di suddividere il carico di lavoro in più flussi concorrenti, ad esempio per preservare la reattività dell'interfaccia utente anche quando la nostra applicazione sta eseguendo elaborazioni onerose in background.

In Qt, gli oggetti sono pertanto caratterizzati dalla loro affinità con un thread. Ad esempio le widget risiedono nel thread primario o GUI thread, mentre le altre istanze di QObject possono essere assegnate a thread differenti. Quando due istanze connesse mediante segnali e slot hanno una differente affinità, cioè sono assegnate a thread differenti, i loro event loop vengono eseguiti in maniera asincrona e pertanto l'emissione di un segnale non determina necessariamente l'immediata esecuzione dello slot corrispondente.

L'esecuzione asincrona di event loop pone una serie di problematiche tipiche della programmazione multi-threaded. Ad esempio, lo stato del mittente può mutare nuovamente prima che lo slot associato venga eseguito in modi imprevedibili che possono compromettere la consistenza della nostra applicazione. Quando la gestione di flussi concorrenti è lacunosa, la conformità di un programma al comportamento atteso può infatti ridursi ad una mera questione di tempismo, invalidando qualsiasi schema progettuale, per quanto valido.

Il quinto parametro della funzione connect() è la enum Qt::ConnectionType che consente di regolare il comportamento di segnali e slot proprio in casi come questo. Una connessione infatti può essere diretta, differita o bloccante, con riferimento all'affinità tra istanze e thread.

Per una connessione diretta Qt::DirectConnection, l'esecuzione di uno slot segue immediatamente l'emissione del segnale indifferentemente dal fatto che le due istanze risiedano nello stesso thread. In questo caso, il controllo di flusso rimane al thread della istanza mittente che richiama esplicitamente lo slot dell'istanza ricevente, con tutti i rischi del caso qualora essa abbia una affinità differente.

Con una connessione differita Qt::QueuedConnection invece, lo slot viene inserito nella coda di eventi del thread della classe ricevente per essere processato quando esso diviene attivo.

Una connessione bloccante Qt::BlockingConnection è differita e comporta l'interruzione dell'esecuzione del thread dell'istanza segnalante fin tanto che lo slot corrispondente non viene eseguito.

Nel caso di connessioni differite o bloccanti, l'invocazione dello slot è a carico del thread dell'istanza ricevente. Ciò comporta, tra le altre cose, la necessità di copiare e immagazzinare temporaneamente la lista di argomenti del segnale (se presente) fino all'esecuzione dello slot che li riceve.

Il valore di default del tipo di connessione è Qt::AutoConnection, che demanda la modalità di interazione tra segnali e slot a momento dell'emissione vera e propria. Se le istanze hanno la medesima affinità la connessione è diretta, ed in caso contrario è differita.

Usi di deleteLater()

Il distruttore della classe QObject, tra le altre cose, effettua la disconnessione di tutte le associazioni di segnali e slot per l'istanza in questione. Tuttavia, la deallocazione di un QObject ha ripercussioni oltre che sul suo event loop, anche su quello delle istanze connesse. Per prevenire effetti collaterali e potenzialmente critici, la classe QObject include appositamente lo slot deleteLater() che consente di schedularne la distruzione, la quale è differita alla prossima iterazione del suo event loop.

In applicazioni multi-programmate è sempre preferibile l'uso di deleteLater() rispetto l'invocazione diretta del distruttore poichè ciò consente al thread competente, cioè quello affine all'oggetto stesso, di essere informato del fatto tale oggetto sta per essere distrutto e di gestire correttamente la coda di segnali in uscita e quella degli slot pendenti.

Un altro caso in cui deleteLater() può essere utile è quello in cui si rende necessario invocare la distruzione dell'istanza chiamante in un suo slot. In questo caso, l'uso di deleteLater() consente di posticipare la distruzione dell'oggetto al termine dell'esecuzione dello slot in modo sicuro.


Ti consigliamo anche