Sur cette page

Exemple de tablette

Cet exemple montre comment utiliser une tablette Wacom dans les applications Qt.

Application affichant une zone de dessin

Lorsque vous utilisez une tablette avec des applications Qt, QTabletEvents est généré. Vous devez réimplémenter le gestionnaire d'événements tabletEvent() si vous souhaitez gérer les événements liés à la tablette. Des événements sont générés lorsque l'outil (stylet) utilisé pour dessiner entre et sort de la proximité de la tablette (c'est-à-dire lorsqu'elle est fermée mais non enfoncée), lorsque l'outil est enfoncé et relâché, lorsque l'outil est déplacé sur la tablette et lorsque l'un des boutons de l'outil est enfoncé ou relâché.

Les informations disponibles sur QTabletEvent dépendent de l'appareil utilisé. Cet exemple peut gérer une tablette dotée de trois outils de dessin différents : un stylet, un aérographe et un stylo. Pour chacun de ces outils, l'événement contiendra la position de l'outil, la pression sur la tablette, l'état du bouton, l'inclinaison verticale et l'inclinaison horizontale (c'est-à-dire l'angle entre le dispositif et la perpendiculaire de la tablette, si le matériel de la tablette peut le fournir). L'aérographe est doté d'une molette ; la position de celle-ci est également disponible dans l'événement de la tablette. Le stylo artistique permet une rotation autour de l'axe perpendiculaire à la surface de la tablette, de sorte qu'il peut être utilisé pour la calligraphie.

Dans cet exemple, nous mettons en œuvre un programme de dessin. Vous pouvez utiliser le stylet pour dessiner sur la tablette comme vous le feriez avec un crayon sur du papier. Lorsque vous dessinez avec l'aérographe, vous obtenez un jet de peinture virtuelle ; la molette permet de modifier la densité du jet. Lorsque vous dessinez avec le stylo artistique, vous obtenez une ligne dont la largeur et l'angle du point d'arrivée dépendent de la rotation du stylo. La pression et l'inclinaison peuvent également être affectées à la modification des valeurs alpha et de saturation de la couleur et de la largeur du trait.

L'exemple se compose des éléments suivants :

  • La classe MainWindow hérite de QMainWindow, crée les menus et connecte leurs emplacements et leurs signaux.
  • La classe TabletCanvas hérite de QWidget et reçoit les événements de la tablette. Elle utilise ces événements pour peindre sur une pixmap hors écran, puis effectue le rendu.
  • La classe TabletApplication hérite de QApplication. Cette classe gère les événements de proximité de la tablette.
  • La fonction main() crée une fenêtre MainWindow et l'affiche comme fenêtre de premier niveau.

Définition de la classe MainWindow

La fonction MainWindow crée une fenêtre TabletCanvas et la place comme widget central.

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(TabletCanvas *canvas);

private slots:
    void setBrushColor();
    void setAlphaValuator(QAction *action);
    void setLineWidthValuator(QAction *action);
    void setSaturationValuator(QAction *action);
    void setEventCompression(bool compress);
    bool save();
    void load();
    void clear();
    void about();

private:
    void createMenus();

    TabletCanvas *m_canvas;
    QColorDialog *m_colorDialog = nullptr;
};

createMenus() met en place les menus avec les actions. Nous avons un QActionGroup pour les actions qui modifient respectivement le canal alpha, la saturation de la couleur et l'épaisseur de la ligne. Les groupes d'actions sont connectés aux emplacements setAlphaValuator(), setSaturationValuator() et setLineWidthValuator(), qui appellent des fonctions dans TabletCanvas.

Mise en œuvre de la classe MainWindow

Nous commençons par examiner le constructeur MainWindow():

MainWindow::MainWindow(TabletCanvas *canvas)
    : m_canvas(canvas)
{
    createMenus();
    setWindowTitle(tr("Tablet Example"));
    setCentralWidget(m_canvas);
    QCoreApplication::setAttribute(Qt::AA_CompressHighFrequencyEvents);
}

Dans le constructeur, nous appelons createMenus() pour créer toutes les actions et tous les menus, et nous définissons le canevas comme widget central.

void MainWindow::createMenus()
{
    QMenu *fileMenu = menuBar()->addMenu(tr("&File"));
    fileMenu->addAction(tr("&Open..."), QKeySequence::Open, this, &MainWindow::load);
    fileMenu->addAction(tr("&Save As..."), QKeySequence::SaveAs, this, &MainWindow::save);
    fileMenu->addAction(tr("&New"), QKeySequence::New, this, &MainWindow::clear);
    fileMenu->addAction(tr("E&xit"), QKeySequence::Quit, this, &MainWindow::close);

    QMenu *brushMenu = menuBar()->addMenu(tr("&Brush"));
    brushMenu->addAction(tr("&Brush Color..."), tr("Ctrl+B"), this, &MainWindow::setBrushColor);

Au début de createMenus(), nous remplissons le menu File. Nous utilisons une surcharge de addAction(), introduite dans Qt 5.6, pour créer un élément de menu avec un raccourci (et optionnellement une icône), l'ajouter à son menu, et le connecter à un slot, le tout avec une seule ligne de code. Nous utilisons QKeySequence pour obtenir les raccourcis clavier standard spécifiques à la plate-forme pour ces éléments de menu courants.

Nous remplissons également le menu Pinceau. La commande permettant de changer de pinceau n'a normalement pas de raccourci standard, c'est pourquoi nous utilisons tr() pour permettre la traduction du raccourci en même temps que la traduction de la langue de l'application.

Nous allons maintenant étudier la création d'un groupe d'actions s'excluant mutuellement dans un sous-menu du menu Tablette, afin de sélectionner la propriété de chaque site QTabletEvent qui sera utilisée pour modifier la translucidité (canal alpha) de la ligne en cours de dessin ou de la couleur en cours d'aérographie.

    QMenu *alphaChannelMenu = tabletMenu->addMenu(tr("&Alpha Channel"));
    QAction *alphaChannelPressureAction = alphaChannelMenu->addAction(tr("&Pressure"));
    alphaChannelPressureAction->setData(TabletCanvas::PressureValuator);
    alphaChannelPressureAction->setCheckable(true);

    QAction *alphaChannelTangentialPressureAction = alphaChannelMenu->addAction(tr("T&angential Pressure"));
    alphaChannelTangentialPressureAction->setData(TabletCanvas::TangentialPressureValuator);
    alphaChannelTangentialPressureAction->setCheckable(true);
    alphaChannelTangentialPressureAction->setChecked(true);

    QAction *alphaChannelTiltAction = alphaChannelMenu->addAction(tr("&Tilt"));
    alphaChannelTiltAction->setData(TabletCanvas::TiltValuator);
    alphaChannelTiltAction->setCheckable(true);

    QAction *noAlphaChannelAction = alphaChannelMenu->addAction(tr("No Alpha Channel"));
    noAlphaChannelAction->setData(TabletCanvas::NoValuator);
    noAlphaChannelAction->setCheckable(true);

    QActionGroup *alphaChannelGroup = new QActionGroup(this);
    alphaChannelGroup->addAction(alphaChannelPressureAction);
    alphaChannelGroup->addAction(alphaChannelTangentialPressureAction);
    alphaChannelGroup->addAction(alphaChannelTiltAction);
    alphaChannelGroup->addAction(noAlphaChannelAction);
    connect(alphaChannelGroup, &QActionGroup::triggered,
            this, &MainWindow::setAlphaValuator);

Nous voulons que l'utilisateur puisse choisir si la composante alpha de la couleur du dessin doit être modulée par la pression ou l'inclinaison de la tablette, ou par la position de la molette de l'outil aérographe. Nous avons une action pour chaque choix, et une action supplémentaire pour choisir de ne pas modifier l'alpha, c'est-à-dire de garder la couleur opaque. Nous rendons les actions vérifiables ; le site alphaChannelGroup veillera alors à ce qu'une seule des actions soit vérifiée à tout moment. Le signal triggered() est émis par le groupe lorsqu'une action est vérifiée, nous le connectons donc à MainWindow::setAlphaValuator(). Il devra savoir à quelle propriété (valuator) de QTabletEvent il doit désormais prêter attention, nous utilisons donc la propriété QAction::data pour transmettre cette information. (Pour que cela soit possible, l'enum Valuator doit être un métatype enregistré, de sorte qu'il puisse être inséré dans un QVariant, ce qui est réalisé par la déclaration Q_ENUM dans tabletcanvas.h).

Voici l'implémentation de setAlphaValuator():

void MainWindow::setAlphaValuator(QAction *action)
{
    m_canvas->setAlphaChannelValuator(qvariant_cast<TabletCanvas::Valuator>(action->data()));
}

Il suffit de récupérer l'enum Valuator à partir de QAction::data() et de le transmettre à TabletCanvas::setAlphaChannelValuator(). Si nous n'utilisions pas la propriété data, nous devrions comparer le pointeur QAction lui-même, par exemple dans une instruction de commutation. Mais cela nécessiterait de conserver des pointeurs vers chaque QAction dans des variables de classe, à des fins de comparaison.

Voici l'implémentation de setBrushColor():

void MainWindow::setBrushColor()
{
    if (!m_colorDialog) {
        m_colorDialog = new QColorDialog(this);
        m_colorDialog->setModal(false);
        m_colorDialog->setCurrentColor(m_canvas->color());
        connect(m_colorDialog, &QColorDialog::colorSelected, m_canvas, &TabletCanvas::setColor);
    }
    m_colorDialog->setVisible(true);
}

Nous initialisons paresseusement une QColorDialog la première fois que l'utilisateur choisit Brush color... dans le menu ou via le raccourci d'action. Tant que la boîte de dialogue est ouverte, chaque fois que l'utilisateur choisit une couleur différente, TabletCanvas::setColor() est appelé pour modifier la couleur du dessin. Comme il s'agit d'une boîte de dialogue non modale, l'utilisateur est libre de laisser la boîte de dialogue ouverte, afin de pouvoir changer de couleur facilement et fréquemment, ou de la fermer et de la rouvrir plus tard.

Voici l'implémentation de save():

bool MainWindow::save()
{
    QString path = QDir::currentPath() + "/untitled.png";
    QString fileName = QFileDialog::getSaveFileName(this, tr("Save Picture"),
                             path);
    bool success = m_canvas->saveImage(fileName);
    if (!success)
        QMessageBox::information(this, "Error Saving Picture",
                                 "Could not save the image");
    return success;
}

Nous utilisons QFileDialog pour permettre à l'utilisateur de sélectionner un fichier pour enregistrer le dessin, puis nous appelons TabletCanvas::saveImage() pour l'écrire dans le fichier.

Voici l'implémentation de load():

void MainWindow::load()
{
    QString fileName = QFileDialog::getOpenFileName(this, tr("Open Picture"),
                                                    QDir::currentPath());

    if (!m_canvas->loadImage(fileName))
        QMessageBox::information(this, "Error Opening Picture",
                                 "Could not open picture");
}

Nous laissons l'utilisateur sélectionner le fichier image à ouvrir à l'aide de QFileDialog; nous demandons ensuite au canevas de charger l'image à l'aide de loadImage().

Voici l'implémentation de about():

void MainWindow::about()
{
    QMessageBox::about(this, tr("About Tablet Example"),
                       tr("This example shows how to use a graphics drawing tablet in Qt."));
}

Nous affichons une boîte de message avec une brève description de l'exemple.

Définition de la classe TabletCanvas

La classe TabletCanvas fournit une surface sur laquelle l'utilisateur peut dessiner avec une tablette.

class TabletCanvas : public QWidget
{
    Q_OBJECT

public:
    enum Valuator { PressureValuator, TangentialPressureValuator,
                    TiltValuator, VTiltValuator, HTiltValuator, NoValuator };
    Q_ENUM(Valuator)

    TabletCanvas();

    bool saveImage(const QString &file);
    bool loadImage(const QString &file);
    void clear();
    void setAlphaChannelValuator(Valuator type)
        { m_alphaChannelValuator = type; }
    void setColorSaturationValuator(Valuator type)
        { m_colorSaturationValuator = type; }
    void setLineWidthType(Valuator type)
        { m_lineWidthValuator = type; }
    void setColor(const QColor &c)
        { if (c.isValid()) m_color = c; }
    QColor color() const
        { return m_color; }
    void setTabletDevice(QTabletEvent *event)
        { updateCursor(event); }

protected:
    void tabletEvent(QTabletEvent *event) override;
    void paintEvent(QPaintEvent *event) override;
    void resizeEvent(QResizeEvent *event) override;

private:
    void initPixmap();
    void paintPixmap(QPainter &painter, QTabletEvent *event);
    Qt::BrushStyle brushPattern(qreal value);
    static qreal pressureToWidth(qreal pressure);
    void updateBrush(const QTabletEvent *event);
    void updateCursor(const QTabletEvent *event);

    Valuator m_alphaChannelValuator = TangentialPressureValuator;
    Valuator m_colorSaturationValuator = NoValuator;
    Valuator m_lineWidthValuator = PressureValuator;
    QColor m_color = Qt::red;
    QPixmap m_pixmap;
    QBrush m_brush;
    QPen m_pen;
    bool m_deviceDown = false;

    struct Point {
        QPointF pos;
        qreal pressure = 0;
        qreal rotation = 0;
    } lastPoint;
};

Le canevas peut modifier le canal alpha, la saturation de la couleur et la largeur du trait. Nous disposons d'un enum listant les propriétés de QTabletEvent avec lesquelles il est possible de les moduler. Nous conservons une variable privée pour chacune d'entre elles : m_alphaChannelValuator m_colorSaturationValuator et m_lineWidthValuator, et nous leur fournissons des fonctions d'accès.

Nous dessinons sur un QPixmap avec m_pen et sur un m_brush avec m_color. Chaque fois qu'un QTabletEvent est reçu, le trait est dessiné à partir de lastPoint jusqu'au point indiqué dans le QTabletEvent actuel, puis la position et la rotation sont sauvegardées dans lastPoint pour la prochaine fois. Les fonctions saveImage() et loadImage() sauvegardent et chargent le fichier QPixmap sur le disque. La pixmap est dessinée sur le widget dans paintEvent().

L'interprétation des événements provenant de la tablette est effectuée dans tabletEvent(), et paintPixmap(), updateBrush(), et updateCursor() sont des fonctions d'aide utilisées par tabletEvent().

Mise en œuvre de la classe TabletCanvas

Nous commençons par examiner le constructeur :

TabletCanvas::TabletCanvas()
    : QWidget(nullptr), m_brush(m_color)
    , m_pen(m_brush, 1.0, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)
{
    resize(500, 500);
    setAutoFillBackground(true);
    setAttribute(Qt::WA_TabletTracking);
}

Dans le constructeur, nous initialisons la plupart des variables de notre classe.

Voici l'implémentation de saveImage():

bool TabletCanvas::saveImage(const QString &file)
{
    return m_pixmap.save(file);
}

QPixmap La classe TabletCanvas met en œuvre une fonctionnalité permettant de s'enregistrer sur le disque, nous appelons donc simplement save().

Voici l'implémentation de loadImage():

bool TabletCanvas::loadImage(const QString &file)
{
    bool success = m_pixmap.load(file);

    if (success) {
        update();
        return true;
    }
    return false;
}

Nous appelons simplement load(), qui charge l'image à partir de file.

Voici l'implémentation de tabletEvent():

void TabletCanvas::tabletEvent(QTabletEvent *event)
{
    switch (event->type()) {
        case QEvent::TabletPress:
            if (!m_deviceDown) {
                m_deviceDown = true;
                lastPoint.pos = event->position();
                lastPoint.pressure = event->pressure();
                lastPoint.rotation = event->rotation();
            }
            break;
        case QEvent::TabletMove:
#ifndef Q_OS_IOS
            if (event->pointingDevice() && event->pointingDevice()->capabilities().testFlag(QPointingDevice::Capability::Rotation))
                updateCursor(event);
#endif
            if (m_deviceDown) {
                updateBrush(event);
                QPainter painter(&m_pixmap);
                paintPixmap(painter, event);
                lastPoint.pos = event->position();
                lastPoint.pressure = event->pressure();
                lastPoint.rotation = event->rotation();
            }
            break;
        case QEvent::TabletRelease:
            if (m_deviceDown && event->buttons() == Qt::NoButton)
                m_deviceDown = false;
            update();
            break;
        default:
            break;
    }
    event->accept();
}

Cette fonction reçoit trois types d'événements : TabletPress, TabletRelease, et TabletMove, qui sont générés lorsqu'un outil de dessin est appuyé, soulevé ou déplacé sur la tablette. Nous réglons m_deviceDown sur true lorsqu'un dispositif est appuyé sur la tablette ; nous savons alors que nous devons dessiner lorsque nous recevons des événements de déplacement. Nous avons implémenté updateBrush() pour mettre à jour m_brush et m_pen en fonction des propriétés de l'événement tablette auxquelles l'utilisateur a choisi de prêter attention. La fonction updateCursor() sélectionne un curseur pour représenter l'outil de dessin utilisé, de sorte que lorsque vous survolez la tablette avec l'outil, vous pouvez voir le type de trait que vous êtes sur le point de réaliser.

void TabletCanvas::updateCursor(const QTabletEvent *event)
{
    QCursor cursor;
    if (event->type() != QEvent::TabletLeaveProximity) {
        if (event->pointerType() == QPointingDevice::PointerType::Eraser) {
            cursor = QCursor(QPixmap(":/images/cursor-eraser.png"), 3, 28);
        } else {
            switch (event->deviceType()) {
            case QInputDevice::DeviceType::Stylus:
                if (event->pointingDevice()->capabilities().testFlag(QPointingDevice::Capability::Rotation)) {
                    QImage origImg(QLatin1String(":/images/cursor-felt-marker.png"));
                    QImage img(32, 32, QImage::Format_ARGB32);
                    QColor solid = m_color;
                    solid.setAlpha(255);
                    img.fill(solid);
                    QPainter painter(&img);
                    QTransform transform = painter.transform();
                    transform.translate(16, 16);
                    transform.rotate(event->rotation());
                    painter.setTransform(transform);
                    painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
                    painter.drawImage(-24, -24, origImg);
                    painter.setCompositionMode(QPainter::CompositionMode_HardLight);
                    painter.drawImage(-24, -24, origImg);
                    painter.end();
                    cursor = QCursor(QPixmap::fromImage(img), 16, 16);
                } else {
                    cursor = QCursor(QPixmap(":/images/cursor-pencil.png"), 0, 0);
                }
                break;
            case QInputDevice::DeviceType::Airbrush:
                cursor = QCursor(QPixmap(":/images/cursor-airbrush.png"), 3, 4);
                break;
            default:
                break;
            }
        }
    }
    setCursor(cursor);
}

Si un stylo artistique (RotationStylus) est utilisé, la fonction updateCursor() est également appelée pour chaque événement TabletMove et affiche un curseur pivoté afin que vous puissiez voir l'angle de la pointe du stylo.

Voici l'implémentation de paintEvent():

void TabletCanvas::initPixmap()
{
    qreal dpr = devicePixelRatio();
    QPixmap newPixmap = QPixmap(qRound(width() * dpr), qRound(height() * dpr));
    newPixmap.setDevicePixelRatio(dpr);
    newPixmap.fill(Qt::white);
    QPainter painter(&newPixmap);
    if (!m_pixmap.isNull())
        painter.drawPixmap(0, 0, m_pixmap);
    painter.end();
    m_pixmap = newPixmap;
}

void TabletCanvas::paintEvent(QPaintEvent *event)
{
    if (m_pixmap.isNull())
        initPixmap();
    QPainter painter(this);
    QRect pixmapPortion = QRect(event->rect().topLeft() * devicePixelRatio(),
                                event->rect().size() * devicePixelRatio());
    painter.drawPixmap(event->rect().topLeft(), m_pixmap, pixmapPortion);
}

La première fois que Qt XML appelle paintEvent(), m_pixmap est construit par défaut, donc QPixmap::isNull() renvoie true. Maintenant que nous savons sur quel écran nous allons effectuer le rendu, nous pouvons créer une pixmap avec la résolution appropriée. La taille de la pixmap avec laquelle nous remplissons la fenêtre dépend de la résolution de l'écran, car l'exemple ne prend pas en charge le zoom ; et il se peut qu'un écran ait une résolution élevée et un autre non. Nous devons également dessiner l'arrière-plan, qui est gris par défaut.

Après cela, nous dessinons simplement la pixmap en haut à gauche du widget.

Voici l'implémentation de paintPixmap():

void TabletCanvas::paintPixmap(QPainter&painter, QTabletEvent *event) { static qreal maxPenRadius = pressureToWidth(1.0) ; painter.setRenderHint(QPainter::Antialiasing) ; switch (event->deviceType()) { case QInputDevice::DeviceType::Airbrush : { painter.setPen(Qt::NoPen) ; QRadialGradient grad(lastPoint.pos, m_pen.widthF() * 10.0) ; QColor color = m_brush.color() ; color.setAlphaF(color.alphaF() * 0.25) ; grad.setColorAt(0, m_brush.color()) ; grad.setColorAt(0.5, Qt::transparent) ; painter.setBrush(grad) ; qreal radius = grad.radius() ; painter.drawEllipse(event->position(), radius, radius) ; update(QRect(event->position().toPoint() - QPoint(rayon, rayon), QSize(radius * 2, radius * 2))) ; } break; case QInputDevice::DeviceType::Puck : case QInputDevice::DeviceType::Mouse : { const QString error(tr("Ce périphérique d'entrée n'est pas pris en charge par l'exemple.")) ;#if QT_CONFIG(statustip)               QStatusTipEvent status(error) ; QCoreApplication::sendEvent(this, &status) ;#else                qWarning() << error;
#endif} break; default: { const QString error(tr("Unknown tablet device - treating as stylus")) ;#if QT_CONFIG(statustip)               QStatusTipEvent status(error) ; QCoreApplication::sendEvent(this, &status) ;#else                qWarning() << error;
#endif} Q_FALLTHROUGH() ; case QInputDevice::DeviceType::Stylus : if (event->pointingDevice()->capabilities().testFlag(QPointingDevice::Capability::Rotation)) { m_brush.setStyle(Qt::SolidPattern) ; painter.setPen(Qt::NoPen) ; painter.setBrush(m_brush) ; QPolygonF poly ; qreal halfWidth = pressureToWidth(lastPoint.pressure) ; QPointF brushAdjust(qSin(qDegreesToRadians(-lastPoint.rotation)) * demi-largeur,                                    qCos(qDegreesToRadians(-lastPoint.rotation)) * halfWidth);
                poly<< lastPoint.pos + brushAdjust ; poly<< lastPoint.pos - brushAdjust ; halfWidth = m_pen.widthF() ; brushAdjust = QPointF(qSin(qDegreesToRadians(-event->rotation())) * demi-largeur,                                      qCos(qDegreesToRadians(-event->rotation())) * halfWidth);
                poly<<  event->position() - brushAdjust ; poly<<  event->position() + brushAdjust ; painter.drawConvexPolygon(poly) ; update(poly.boundingRect().toRect()) ; } else { painter.setPen(m_pen) ; painter.drawLine(lastPoint.pos,  event->position()) ; update(QRect(lastPoint.pos.toPoint(),  event->position().toPoint()).normalized() .adjusted(-maxPenRadius, -maxPenRadius, maxPenRadius, maxPenRadius)) ; } break; }

Dans cette fonction, nous dessinons sur la pixmap en fonction du mouvement de l'outil. Si l'outil utilisé sur la tablette est un stylet, nous voulons tracer une ligne entre la dernière position connue et la position actuelle. Nous supposons également qu'il s'agit d'un traitement raisonnable de tout appareil inconnu, mais nous mettons à jour la barre d'état avec un avertissement. S'il s'agit d'un aérographe, nous voulons dessiner un cercle rempli d'un dégradé doux, dont la densité peut dépendre de divers paramètres d'événement. Par défaut, elle dépend de la pression tangentielle, qui est la position de la molette du doigt sur l'aérographe. Si l'outil est un stylet de rotation, nous simulons un feutre en dessinant des segments de trait trapézoïdaux.

        case QInputDevice::DeviceType::Airbrush:
            {
                painter.setPen(Qt::NoPen);
                QRadialGradient grad(lastPoint.pos, m_pen.widthF() * 10.0);
                QColor color = m_brush.color();
                color.setAlphaF(color.alphaF() * 0.25);
                grad.setColorAt(0, m_brush.color());
                grad.setColorAt(0.5, Qt::transparent);
                painter.setBrush(grad);
                qreal radius = grad.radius();
                painter.drawEllipse(event->position(), radius, radius);
                update(QRect(event->position().toPoint() - QPoint(radius, radius), QSize(radius * 2, radius * 2)));
            }
            break;

Dans updateBrush(), nous définissons le stylo et le pinceau utilisés pour le dessin de manière à ce qu'ils correspondent à m_alphaChannelValuator, m_lineWidthValuator, m_colorSaturationValuator et m_color. Nous examinerons le code permettant de configurer m_brush et m_pen pour chacune de ces variables :

void TabletCanvas::updateBrush(const QTabletEvent *event)
{
    int hue, saturation, value, alpha;
    m_color.getHsv(&hue, &saturation, &value, &alpha);

    int vValue = int(((event->yTilt() + 60.0) / 120.0) * 255);
    int hValue = int(((event->xTilt() + 60.0) / 120.0) * 255);

Nous récupérons les valeurs de teinte, de saturation, de valeur et d'alpha de la couleur de dessin actuelle. hValue et vValue sont définies comme étant l'inclinaison horizontale et verticale sous la forme d'un nombre compris entre 0 et 255. Les valeurs originales sont exprimées en degrés de -60 à 60, c'est-à-dire que 0 équivaut à -60, 127 équivaut à 0 et 255 équivaut à 60 degrés. L'angle mesuré se situe entre l'appareil et la perpendiculaire de la tablette (voir QTabletEvent pour une illustration).

    switch (m_alphaChannelValuator) {
        case PressureValuator:
            m_color.setAlphaF(event->pressure());
            break;
        case TangentialPressureValuator:
            if (event->deviceType() == QInputDevice::DeviceType::Airbrush)
                m_color.setAlphaF(qMax(0.01, (event->tangentialPressure() + 1.0) / 2.0));
            else
                m_color.setAlpha(255);
            break;
        case TiltValuator:
            m_color.setAlpha(std::max(std::abs(vValue - 127),
                                      std::abs(hValue - 127)));
            break;
        default:
            m_color.setAlpha(255);
    }

Le canal alpha de QColor est indiqué sous la forme d'un nombre compris entre 0 et 255, où 0 est transparent et 255 est opaque, ou sous la forme d'un nombre à virgule flottante où 0 est transparent et 1,0 est opaque. pressure() renvoie la pression sous la forme d'un qreal compris entre 0,0 et 1,0. Nous obtenons les valeurs alpha les plus faibles (c'est-à-dire que la couleur est la plus transparente) lorsque le stylo est perpendiculaire à la tablette. Nous sélectionnons la plus grande des valeurs d'inclinaison verticale et horizontale.

    switch (m_colorSaturationValuator) {
        case VTiltValuator:
            m_color.setHsv(hue, vValue, value, alpha);
            break;
        case HTiltValuator:
            m_color.setHsv(hue, hValue, value, alpha);
            break;
        case PressureValuator:
            m_color.setHsv(hue, int(event->pressure() * 255.0), value, alpha);
            break;
        default:
            ;
    }

La saturation de la couleur dans le modèle de couleur HSV peut être donnée sous la forme d'un nombre entier entre 0 et 255 ou d'une valeur flottante entre 0 et 1. Nous avons choisi de représenter l'alpha sous la forme d'un nombre entier, c'est pourquoi nous appelons setHsv() avec des valeurs entières. Cela signifie que nous devons multiplier la pression par un nombre compris entre 0 et 255.

    switch (m_lineWidthValuator) {
        case PressureValuator:
            m_pen.setWidthF(pressureToWidth(event->pressure()));
            break;
        case TiltValuator:
            m_pen.setWidthF(std::max(std::abs(vValue - 127),
                                     std::abs(hValue - 127)) / 12);
            break;
        default:
            m_pen.setWidthF(1);
    }

La largeur du trait de crayon peut augmenter avec la pression, si on le souhaite. Mais lorsque la largeur du stylet est contrôlée par l'inclinaison, nous laissons la largeur augmenter en fonction de l'angle entre l'outil et la perpendiculaire de la tablette.

    if (event->pointerType() == QPointingDevice::PointerType::Eraser) {
        m_brush.setColor(Qt::white);
        m_pen.setColor(Qt::white);
        m_pen.setWidthF(event->pressure() * 10 + 1);
    } else {
        m_brush.setColor(m_color);
        m_pen.setColor(m_color);
    }
}

Nous vérifions enfin si le pointeur est le stylet ou la gomme. S'il s'agit de la gomme, nous fixons la couleur à la couleur d'arrière-plan de la pixmap et laissons la pression décider de la largeur du stylet, sinon nous fixons les couleurs que nous avons décidées précédemment dans la fonction.

Définition de la classe TabletApplication

Nous héritons de QApplication dans cette classe car nous voulons réimplémenter la fonction event().

class TabletApplication : public QApplication
{
    Q_OBJECT

public:
    using QApplication::QApplication;

    bool event(QEvent *event) override;
    void setCanvas(TabletCanvas *canvas)
        { m_canvas = canvas; }

private:
    TabletCanvas *m_canvas = nullptr;
};

TabletApplication Cette classe existe en tant que sous-classe de QApplication afin de recevoir les événements de proximité de la tablette et de les transmettre à TabletCanvas. Les événements TabletEnterProximity et TabletLeaveProximity sont envoyés à l'objet QApplication, tandis que les autres événements de la tablette sont envoyés au gestionnaire event() de QWidget, qui les transmet à tabletEvent().

Mise en œuvre de la classe TabletApplication

Voici l'implémentation de event():

bool TabletApplication::event(QEvent *event)
{
    if (event->type() == QEvent::TabletEnterProximity ||
        event->type() == QEvent::TabletLeaveProximity) {
        m_canvas->setTabletDevice(static_cast<QTabletEvent *>(event));
        return true;
    }
    return QApplication::event(event);
}

Nous utilisons cette fonction pour gérer les événements TabletEnterProximity et TabletLeaveProximity, qui sont générés lorsqu'un outil de dessin entre ou sort de la proximité de la tablette. Ici, nous appelons TabletCanvas::setTabletDevice(), qui appelle ensuite updateCursor(), qui mettra en place un curseur approprié. C'est la seule raison pour laquelle nous avons besoin des événements de proximité ; pour que le dessin soit correct, il suffit que TabletCanvas observe les événements device() et pointerType() dans chaque événement qu'il reçoit.

La fonction main()

Voici la fonction main() de l'exemple :

int main(int argv, char *args[])
{
    TabletApplication app(argv, args);
    TabletCanvas *canvas = new TabletCanvas;
    app.setCanvas(canvas);

    MainWindow mainWindow(canvas);
    mainWindow.resize(500, 500);
    mainWindow.show();
    return app.exec();
}

Nous créons ici un site MainWindow et l'affichons dans une fenêtre de premier niveau. Nous utilisons la classe TabletApplication. Nous devons définir le canevas après la création de l'application. Nous ne pouvons pas utiliser de classes qui implémentent la gestion des événements avant qu'un objet QApplication ne soit instancié.

Exemple de projet @ code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.