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

Il paint system di Qt

Imparare ad utilizzare il paint system fornito dal framework Qt, al fine di disegnare elementi personalizzati all'interno di una GUI con C++.
Imparare ad utilizzare il paint system fornito dal framework Qt, al fine di disegnare elementi personalizzati all'interno di una GUI con C++.
Link copiato negli appunti

Qt dispone di un sotto sistema dedicato al disegno delle widgets specifico per ogni piattaforma, ma omogeneo in termini di funzionalità a API.

Le componenti principali di questo sistema sono le seguenti:

  • La classe QPaintDevice che astrae il concetto di canvas 2D, indipendentemente dal fatto che la superficie di disegno sottostante sia lo schermo del computer, un file PNG o un foglio da stampare.
  • La classe QPainter che fornisce il set di primitive per il disegno di elementi grafici di base come linee e poligoni.
  • La classe QPaintEngine che astrae la piattaforma sottostante e definisce un'interfaccia per il supporto delle funzionalità degli oggetti QPaintDevice e QPainter. Quest'ultimo componente non viene usato mai in maniera esplicita, a meno che non si voglia implementare un motore di rendering alternativo.

Ogni widget in Qt deriva dalla classe QPaintDevice ed eredita da essa una serie di proprietà utili alla definizione del sistema di riferimento, la dimensione della superficie di disegno e la sua risoluzione.

Nel contesto della classe QWidget, queste informazioni sono impiegate per l'implementazione di molte funzionalità, ed in particolare per il metodo virtuale paintEvent. Esso è infatti il metodo designato all'aggiornamento della rappresentazione grafica della widget quando richiesto dal sistema in risposta ad azioni dell'utente come click, mouse hover, eccetera.

Ciò rende possibile implementare widget di qualsiasi tipo per ereditarietà, estendendo il set di widget predefinete del framework, semplicemente fornendo un opportuno sovraccarico del metodo paintEvent.

In questo caso, prenderemo in considerazione un esempio classico come quello di una widget che rappresenta un LED RGB. Il listato seguente mostra lo header file della classe Led, derivata pubblicamente da QWidget:

#ifndef LED_H
#define LED_H
 
#include <QWidget>
#include <QGraphicsDropShadowEffect>
 
class Led : public QWidget
{
    Q_OBJECT
public:
    Led(QWidget *parent = 0);
     
    bool isOn() const;
    QColor getLightColor() const;
     
public slots:
    void setOn(bool value);
    void setLightColor(const QColor& c);
     
protected:
    virtual void paintEvent(QPaintEvent *event) override;
     
    bool on;
    QColor lightColor;
    QGraphicsDropShadowEffect* glowEffect;
};
 
#endif

I membri lightColor e on della classe definiscono lo stato di ogni istanza in termini di colore e alimentazione. Inoltre, il membro glowEffect consente di simulare l'effetto della luce emessa del LED nello spazio circostante.

Il framework Qt predispone la classe base QGraphicsEffect per aggiungere effetti e filtri alla pipeline di rendering della widget, come ad esempio l'ombreggiatura generata dalla classe QGraphicsDropShadowEffect. Tali filtri operano sulla rappresentazione raster della widget.

Il costruttore della classe Led è utilizzato per assegnare dei valori di default ai membri dell'istanza, come mostrato si seguito:

#include "led.h"
#include <QPainter>
 
Led::Led(QWidget *parent) :
    QWidget(parent)
{
    // inizializzazione con valori di default
    on = true;
    lightColor = QColor(Qt::green);
     
    // imposta una dimensione fissa per la widget
    setFixedSize(24, 24);
     
    // inizializzazione dell'effetto di glow
    glowEffect = new QGraphicsDropShadowEffect(this);
    glowEffect->setBlurRadius(48);
    glowEffect->setColor(lightColor.lighter(150));
    glowEffect->setOffset(0, 0);
    glowEffect->setEnabled(false);
    setGraphicsEffect(glowEffect);
}
 
...

In questo caso particolare, alla widget viene inoltre assegnata una dimensione fissa e immutabile pari a 24 pixel, mediante l'invocazione del metodo setFixedSize della classe base.

Alcuni membri di classe sono corredati da metodi pubblici per l'accesso in lettura e scrittura, come riportato qui di seguito. I metodi setter, in particolare, sono etichettati come slot pubblici, al fine di renderli idonei all'integrazione con il sistema degli eventi di Qt.

È importante sottolineare, nel contesto di tali slot, l'uso del metodo update derivato dalla classe base QWidget, la cui invocazione consente di notificare al sistema di disegno la necessità di aggiornare la widget ogni qualvolta una sua proprietà grafica cambi.

 
bool Led::isOn() const
{
    return on;
}
 
void Led::setOn(bool value)
{
    on = value;
    glowEffect->setEnabled(on);
     
    update();
}
 
QColor Led::getLightColor() const
{
    return lightColor;
}
 
void Led::setLightColor(const QColor& c)
{
    lightColor = c;
    glowEffect->setColor(c.lighter(150));
     
    update();
}

Infine, il sovraccarico del metodo paintEvent contiene il blocco di istruzioni responsabili del disegno della widget a schermo, come riportato qui di seguito:

void Led::paintEvent(QPaintEvent* /*event*/)
{
    QPainter painter(this);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::HighQualityAntialiasing);
     
    QColor ledColor = (on) ? lightColor : QColor(Qt::darkGray);
     
    // definizione di un gradiente radiale per renderizzare il
    // colore del led e simulare una riflessione ambientale chiara sulla sua superficie
    int radius = width() / 2.0;
    QRadialGradient radialGrad(QPointF(radius, radius), radius - 1, QPointF(radius, radius / 2.0f));
    radialGrad.setColorAt(0, ledColor.lighter(300));
    radialGrad.setColorAt(0.5, ledColor);
    radialGrad.setColorAt(1, ledColor.darker(200));
     
    // imposta Pen per il contorno e Brush per il filling
    painter.setPen(QPen(Qt::gray, 2));
    painter.setBrush(radialGrad);
     
    // disegna con un'unica direttiva il contorno grigio
    // e riempie l'areaola circolare del led con il gradiente
    painter.drawEllipse(QPointF(radius, radius), radius - 1, radius - 1);
}

In questo caso il LED viene rappresentato mediante una vista dall'alto, per cui è sufficiente basare il disegno su una forma circolare. La prima direttiva consiste nell'inizializzazione di un'istanza della classe QPainter, cui viene associato il puntatore this in qualità di riferimento all'entità QPaintDevice che assume il ruolo di canvas.

L'istanza painter è inoltre configurata in modo da applicare un filtro di antialiasing per migliorare la resa grafica dei contorni tramite invocazione del metodo setRenderHints.

Terminata l'inizializzazione del painter, si procede con la definizione di un gradiente radiale centrato rispetto la widget e avente raggio pari alla metà della sua larghezza/altezza. Si noti in questo caso il ricorso al metodo width, ereditato dalla classe base QPaintDevice, per calcolare il raggio.

Per dare profondità al disegno, il punto focale del gradiente, fornito come ultimo argomento del costruttore, è leggermente eccentrico. Quando il LED è accesso, gli estremi del gradiente sono due tonalità derivate dal colore predefinito della luce, rispettivamente più scura al contorno e più chiara al centro. Quando il LED è spento, sono usati due toni di grigio.

Mediante l'invocazione del metodo setBrush sull'istanza painter il gradiente radiale così definito verrà usato come tessitura per il riempimento, al posto di un colore solido o un altro pattern. In modo analogo, si definisce la modalità di disegno dei contorni assegnando un colore, in questo caso grigio, ed uno spessore (2 pixel) nell'invocazione del metodo setPen.

L'ultima istruzione infine è rappresentata dall'unica direttiva di disegno vera e propria, drawEllipse, per il disegno di una circonferenza avente la tessitura ed il contorno definiti dal painter. Si noti che il centro ed il raggio della circonferenza corrispondono a quelli del gradiente.

Il risultato ottenuto è il seguente:

L'effetto di glow intorno al LED quando esso è acceso è il risultato dell'applicazione dell'effetto di ombreggiatura.

La widget a questo punto è pronta per essere aggiunta ad una finestra, come mostrato nel frammento seguente, in cui l'inizializzazione avviene direttamente nel costruttore:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "led.h"
#include <QGridLayout>
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
     
    // inizializzazione della widget custom
    Led* led = new Led(this);
    led->setLightColor(Qt::green);
     
    QGridLayout *layout = new QGridLayout();
    centralWidget()->setLayout(layout);
    layout->addWidget(led);
     
    // inizializzazione di un timer per far lampeggiare il LED ogni 500 ms.
    QTimer *timer = new QTimer(this);
    connect(timer, &QTimer::timeout, this, [this, led] ()
    {
        // commuta lo stato del LED
        led->setOn(!led->isOn());
        // cambia il colore
        int hue = led->getLightColor().hue();
        led->setLightColor(QColor::fromHsv((hue + 15) % 360, 255, 255));
    });
     
    timer->start(500);
}

In questo esempio è stata aggiunta anche una espressione lambda connessa all'emissione d'evento timeout di un oggetto di tipo QTimer, al fine di far lampeggiare il LED ogni 500 ms, variandone allo stesso tempo la tonalità a passi discreti.

Partendo da questa snippet di codice è possibile creare una widget custom in ogni contesto ed applicare ad essa la business logic desiderata. Tuttavia, in questo esempio manca l'integrazione con Qt Designer, che ci consentirebbe di definire un layout di complessità arbitraria per la finestra in modalità RAD, aggiungendo, ad esempio, ulteriori elementi di controllo per modificare le proprietà della widget in modo interattivo.

Nelle prossime lezioni vedremo meglio come perfezionare la definizione della widget in tal senso e per predisporla alla creazione di plugin o librerie di widget per Qt Designer.

Ti consigliamo anche