Exemple d'utilisation d'une tablette
Cet exemple montre comment utiliser une tablette Wacom dans des applications Qt.

Lorsque vous utilisez une tablette avec des applications Qt, des événements « QTabletEvent» sont générés. Vous devez réimplémenter le gestionnair tabletEvent() si vous souhaitez gérer les événements de la tablette. Des événements sont générés lorsque l'outil (stylet) utilisé pour dessiner entre dans la zone de proximité de la tablette et en sort (c'est-à-dire lorsqu'il est proche de la tablette mais n'appuie pas dessus), lorsque l'outil appuie sur la tablette puis la relâche, lorsque l'outil est déplacé sur la tablette, et lorsqu’un des boutons de l’outil est enfoncé ou relâché.
Les informations disponibles dans ` QTabletEvent ` dépendent du périphérique utilisé. Cet exemple permet de gérer une tablette comportant jusqu’à trois outils de dessin différents : un stylet, un aérographe et un stylo artistique. Pour chacun d’entre eux, l’événement contiendra la position de l’outil, la pression exercée sur la tablette, l’état des boutons, l’inclinaison verticale et l’inclinaison horizontale (c’est-à-dire l’angle entre l’appareil et la perpendiculaire à la tablette, si le matériel de la tablette permet de le fournir). L’aérographe est équipé d’une molette ; sa position 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, ce qui permet de l’utiliser 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 sert à modifier la densité du jet. Lorsque vous dessinez avec le stylo d'art, vous obtenez une ligne dont la largeur et l'angle des extrémités dépendent de la rotation du stylo. La pression et l'inclinaison peuvent également être utilisées pour modifier les valeurs d'opacité et de saturation de la couleur, ainsi que la largeur du trait.
L'exemple se compose des éléments suivants :
- La classe `
MainWindow` hérite de ` QMainWindow`, crée les menus et relie 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 dessiner sur une image pixmap hors écran, puis l'affiche. - 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 instance de `MainWindow` et l'affiche en tant que fenêtre de niveau supérieur.
Définition de la classe MainWindow
La classe ` MainWindow ` crée une instance de ` TabletCanvas ` et la définit 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() configure les menus avec les actions. Nous disposons d’un « QActionGroup » pour les actions qui modifient respectivement le canal alpha, la saturation des couleurs et l’épaisseur des traits. Les groupes d’actions sont connectés aux slots « setAlphaValuator() », « setSaturationValuator() » et « setLineWidthValuator() », qui appellent des fonctions dans « TabletCanvas ».
Implémentation de la classe MainWindow
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, puis 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 la fonction ` createMenus() `, nous remplissons le menu Fichier. Nous utilisons une surcharge de la fonction ` addAction()`, introduite dans Qt 5.6, pour créer un élément de menu avec un raccourci clavier (et éventuellement une icône), l’ajouter à son menu et le connecter à un slot, le tout en une seule ligne de code. Nous utilisons ` QKeySequence ` pour obtenir les raccourcis clavier standard spécifiques à la plateforme pour ces éléments de menu courants.
Nous remplissons également le menu « Pinceau ». La commande permettant de changer de pinceau ne dispose généralement pas de raccourci clavier standard ; nous utilisons donc tr() pour permettre la traduction du raccourci en même temps que la traduction linguistique de l’application.
Nous allons maintenant nous intéresser à la création d’un groupe d’actions mutuellement exclusives dans un sous-menu du menu « Tablet », permettant de sélectionner quelle propriété de chaque QTabletEvent sera utilisée pour moduler la translucidité (canal alpha) de la ligne en cours de tracé ou de la couleur appliquée à l’aérographe.
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 souhaitons que l’utilisateur puisse choisir si la composante alpha de la couleur de dessin doit être modulée par la pression de la tablette, son inclinaison ou la position de la molette de l’outil aérographe. Nous disposons d’une action pour chaque choix, ainsi que d’une action supplémentaire permettant de ne pas modifier l’alpha, c’est-à-dire de conserver la couleur opaque. Nous rendons ces actions cochables ; l’ alphaChannelGroup veillera alors à ce qu’une seule de ces actions soit cochée à tout moment. Le signal « triggered() » est émis par le groupe lorsqu’une action est cochée ; nous le connectons donc à « MainWindow::setAlphaValuator() ». Il faudra indiquer à l’action à quelle propriété (valuator) de l’ QTabletEvent elle doit désormais prêter attention ; nous utilisons donc la propriété « QAction::data » pour transmettre cette information. (Pour que cela soit possible, l’énumération Valuator doit être un métatype enregistré, afin de pouvoir être insérée dans un QVariant. C’est ce que permet 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()));
}Elle doit simplement récupérer l'énumération Valuator à partir de QAction::data(), puis la transmettre à TabletCanvas::setAlphaChannelValuator(). Si nous n'utilisions pas la propriété data, nous devrions à la place comparer le pointeur QAction lui-même, par exemple dans une instruction switch. 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 procédons à une initialisation paresseuse de l’ QColorDialog la première fois que l’utilisateur sélectionne « Couleur du pinceau… » 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, la méthode ` TabletCanvas::setColor() ` est appelée pour modifier la couleur de dessin. Comme il s’agit d’une boîte de dialogue non modale, l’utilisateur est libre de la laisser ouverte afin de pouvoir changer de couleur facilement et fréquemment, ou de la fermer pour la rouvrir ultérieurement.
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 la méthode QFileDialog pour permettre à l'utilisateur de sélectionner un fichier dans lequel enregistrer le dessin, puis nous appelons TabletCanvas::saveImage() pour l'écrire effectivement 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 permettons à l'utilisateur de sélectionner le fichier image à ouvrir à l'aide de ` QFileDialog` ; nous demandons ensuite au canvas de charger l'image avec ` 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 contenant 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 à l’aide d’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 permet de modifier le canal alpha, la saturation des couleurs et l’épaisseur du trait. Nous disposons d’une énumération répertoriant les propriétés de ` QTabletEvent ` grâce auxquelles 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 fournissons des fonctions d’accès pour celles-ci.
Nous dessinons sur une QPixmap à l’aide de m_pen et m_brush en utilisant m_color. À chaque fois qu’un QTabletEvent est reçu, le trait est tracé depuis lastPoint jusqu’au point indiqué dans l’ QTabletEvent actuel, puis la position et la rotation sont enregistrées dans lastPoint pour la prochaine fois. Les fonctions saveImage() et loadImage() enregistrent et chargent l’ QPixmap sur le disque. La carte de pixels est dessinée sur le widget dans paintEvent().
L'interprétation des événements provenant de la tablette s'effectue dans tabletEvent(), tandis que paintPixmap(), updateBrush() et updateCursor() sont des fonctions d'aide utilisées par tabletEvent().
Implémentation de la classe TabletCanvas
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 de nos variables de classe.
Voici l’implémentation de ` saveImage()` :
bool TabletCanvas::saveImage(const QString &file)
{
return m_pixmap.save(file);
}QPixmap implémente une fonctionnalité permettant de s'enregistrer sur le disque ; il suffit donc d'appeler la fonction ` 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 la fonction ` load()`, qui charge l'image depuis 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 enfoncé sur la tablette, retiré de celle-ci ou déplacé dessus. Nous définissons m_deviceDown sur true lorsqu’un outil est enfoncé sur la tablette ; nous savons alors que nous devons dessiner lorsque nous recevons des événements de déplacement. Nous avons implémenté la méthode ` updateBrush() ` pour mettre à jour ` m_brush ` et ` m_pen ` en fonction des propriétés d’événement de la tablette que l’utilisateur a choisi de prendre en compte. La fonction ` updateCursor() ` sélectionne un curseur représentant l’outil de dessin utilisé, de sorte que lorsque vous survolez la tablette avec l’outil à proximité, vous pouvez voir le type de trait que vous êtes sur le point de tracer.
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 stylet artistique (RotationStylus) est utilisé, la fonction updateCursor() est également appelée à chaque événement « TabletMove » et affiche un curseur pivoté afin que vous puissiez voir l’angle de la pointe du stylet.
Voici l’implémentation de la fonction 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 appelle `paintEvent()`, `m_pixmap` est instancié par défaut, de sorte que ` QPixmap::isNull()` renvoie ` true`. Maintenant que nous savons sur quel écran nous allons effectuer le rendu, nous pouvons créer une image bitmap 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 ; il se peut également qu'un écran ait une haute résolution (DPI) tandis qu'un autre n'en a pas. Nous devons également dessiner l'arrière-plan, car la couleur par défaut est le gris.
Ensuite, il suffit de dessiner la carte de pixels dans le coin supérieur gauche du widget.
Voici l'implémentation de la fonction ` 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(radius, radius), 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 cet exemple."));
#if QT_CONFIG(statustip)
QStatusTipEvent status(error);
QCoreApplication::sendEvent(this, &status);
#else
qWarning() << error;
#endif
}
break;
par défaut:
{
const QString error(tr("Tablette inconnue – traitée comme un stylet"));
#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)) * halfWidth,
qCos(qDegreesToRadians(-lastPoint.rotation)) * halfWidth);
poly << lastPoint.pos + brushAdjust ;
poly << lastPoint.pos - brushAdjust ;
halfWidth = m_pen.widthF() ;
brushAdjust = QPointF(qSin(qDegreesToRadians(-event->rotation())) * halfWidth,
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 carte de pixels en fonction du mouvement de l’outil. Si l’outil utilisé sur la tablette est un stylet, nous souhaitons tracer une ligne entre la dernière position connue et la position actuelle. Nous partons également du principe qu’il s’agit d’une gestion raisonnable pour tout périphérique 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, c'est-à-dire de la position de la molette de l'aérographe. Si l'outil est un stylet rotatif, 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 configurons le stylet et le pinceau utilisés pour le dessin afin qu’ils correspondent à m_alphaChannelValuator, m_lineWidthValuator, m_colorSaturationValuator et m_color. Nous allons examiner 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 luminosité et d’opacité de la couleur de dessin actuelle. hValue et vValue sont définis sur l’inclinaison horizontale et verticale, respectivement, sous la forme d’un nombre compris entre 0 et 255. Les valeurs d’origine sont exprimées en degrés, comprises entre -60 et 60, c’est-à-dire que 0 correspond à -60, 127 à 0 et 255 à 60 degrés. L’angle mesuré est celui formé entre l’appareil et la perpendiculaire à 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 exprimé sous la forme d’un nombre compris entre 0 et 255, où 0 correspond à la transparence et 255 à l’opacité, ou sous la forme d’un nombre à virgule flottante où 0 correspond à la transparence et 1,0 à l’opacité. ` pressure()` renvoie la pression sous la forme d’un `qreal` compris entre 0,0 et 1,0. On obtient les valeurs alpha les plus faibles (c’est-à-dire que la couleur est la plus transparente) lorsque le stylet est perpendiculaire à la tablette. On sélectionne 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 exprimée sous la forme d'un entier compris entre 0 et 255 ou d'une valeur à virgule flottante comprise entre 0 et 1. Nous avons choisi de représenter l'alpha sous forme d'entier ; nous appelons donc la fonction 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 stylet peut augmenter avec la pression, si on le souhaite. Mais lorsque la largeur du trait est contrôlée par l'inclinaison, nous faisons en sorte que la largeur augmente en fonction de l'angle formé entre l'outil et la perpendiculaire à 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);
}
}Enfin, nous vérifions si le pointeur est le stylet ou la gomme. S'il s'agit de la gomme, nous définissons la couleur sur la couleur d'arrière-plan de la carte de pixels et laissons la pression déterminer la largeur du trait ; sinon, nous définissons les couleurs que nous avons choisies précédemment dans la fonction.
Définition de la classe TabletApplication
Dans cette classe, nous héritons d'QApplication, car nous souhaitons 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 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 liés à la tablette sont envoyés au gestionnaire ` event() ` de ` QWidget`, qui les transmet à son tour à ` tabletEvent()`.
Implémentation 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 dans la zone de proximité de la tablette ou en sort. Ici, nous appelons ` TabletCanvas::setTabletDevice()`, qui appelle à son tour ` updateCursor()`, ce qui permet de définir un curseur approprié. C’est la seule raison pour laquelle nous avons besoin des événements de proximité ; pour garantir un tracé correct, il suffit que ` TabletCanvas ` observe les méthodes ` 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();
}Ici, nous créons une « MainWindow » et l'affichons sous forme de fenêtre de premier plan. Nous utilisons la classe « TabletApplication ». Nous devons configurer le canevas une fois l'application créée. Nous ne pouvons pas utiliser de classes qui implémentent la gestion des événements avant l'instanciation d'un objet « QApplication ».
© 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.