Auf dieser Seite

Beispiel für ein Grafiktablett

Dieses Beispiel zeigt, wie man ein Wacom-Tablet in Qt-Anwendungen verwendet.

Anwendung mit einem Zeichenbereich

Bei der Verwendung eines Tablets mit Qt-Anwendungen werden „ QTabletEvent“ generiert. Sie müssen den Ereignis-Handler ` tabletEvent()` neu implementieren, wenn Sie Tablet-Ereignisse verarbeiten möchten. Ereignisse werden ausgelöst, wenn das zum Zeichnen verwendete Werkzeug (Stift) in die Nähe des Tablets kommt und diese wieder verlässt (d. h., wenn es sich in der Nähe befindet, aber nicht auf das Tablet gedrückt wird), wenn das Werkzeug auf das Tablet gedrückt und wieder losgelassen wird, wenn das Werkzeug über das Tablet bewegt wird und wenn eine der Tasten am Werkzeug gedrückt oder losgelassen wird.

Die in „ QTabletEvent “ verfügbaren Informationen hängen vom verwendeten Gerät ab. Dieses Beispiel kann ein Tablet mit bis zu drei verschiedenen Zeichenwerkzeugen verarbeiten: einen Stift, eine Airbrush und einen Art-Pen. Für jedes dieser Werkzeuge enthält das Ereignis die Position des Werkzeugs, den Druck auf das Tablet, den Tastenstatus sowie die vertikale und horizontale Neigung (d. h. den Winkel zwischen dem Gerät und der Senkrechten zum Tablet, sofern die Tablet-Hardware diese Information bereitstellen kann). Die Airbrush verfügt über ein Fingerrad; dessen Position ist ebenfalls im Tablet-Ereignis verfügbar. Der Art Pen ermöglicht eine Drehung um die zur Tablet-Oberfläche senkrechte Achse, sodass er für Kalligraphie verwendet werden kann.

In diesem Beispiel implementieren wir ein Zeichenprogramm. Sie können mit dem Stylus auf dem Tablet zeichnen, so wie Sie mit einem Bleistift auf Papier zeichnen würden. Wenn Sie mit der Airbrush zeichnen, entsteht ein Sprühnebel aus virtueller Farbe; mit dem Fingerrad lässt sich die Dichte des Sprühnebels verändern. Wenn Sie mit dem Kunststift zeichnen, entsteht eine Linie, deren Breite und Endpunktwinkel von der Drehung des Stifts abhängen. Der Druck und die Neigung können ebenfalls zugewiesen werden, um die Alpha- und Sättigungswerte der Farbe sowie die Strichbreite zu ändern.

Das Beispiel besteht aus Folgendem:

  • Die Klasse „ MainWindow “ erbt von „ QMainWindow “, erstellt die Menüs und verbindet deren Slots und Signale.
  • Die Klasse „ TabletCanvas “ erbt von „ QWidget “ und empfängt Tablet-Ereignisse. Sie nutzt die Ereignisse, um auf eine Offscreen-Pixmap zu zeichnen, und rendert diese anschließend.
  • Die Klasse „ TabletApplication “ erbt von „ QApplication “. Diese Klasse verarbeitet Tablet-Näherungsereignisse.
  • Die Funktion ` main() ` erstellt ein ` MainWindow ` und zeigt es als Fenster der obersten Ebene an.

Definition der Klasse „MainWindow“

Das ` MainWindow ` erstellt ein ` TabletCanvas ` und legt es als zentrales Widget fest.

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() richtet die Menüs mit den Aktionen ein. Wir haben jeweils ein „ QActionGroup “ für die Aktionen, die den Alphakanal, die Farbsättigung und die Linienbreite ändern. Die Aktionsgruppen sind mit den Slots „ setAlphaValuator() “, „ setSaturationValuator() “ und „ setLineWidthValuator() “ verbunden, die Funktionen in „ TabletCanvas “ aufrufen.

Implementierung der Klasse „MainWindow“

Wir beginnen mit einem Blick auf den Konstruktor „ MainWindow() “:

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

Im Konstruktor rufen wir ` createMenus() ` auf, um alle Aktionen und Menüs zu erstellen, und legen die Leinwand als zentrales Widget fest.

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);

Zu Beginn von ` createMenus() ` füllen wir das Menü „Datei“ mit Einträgen. Wir verwenden eine in Qt 5.6 eingeführte Überladung von ` addAction()` (), um mit nur einer Codezeile einen Menüpunkt mit einer Tastenkombination (und optional einem Symbol) zu erstellen, ihn dem entsprechenden Menü hinzuzufügen und ihn mit einem Slot zu verbinden. Mit ` QKeySequence ` rufen wir die plattformspezifischen Standard-Tastenkombinationen für diese gängigen Menüpunkte ab.

Außerdem füllen wir das Menü „Pinsel“ mit Einträgen. Der Befehl zum Wechseln eines Pinsels verfügt normalerweise nicht über eine Standardtastenkombination, daher verwenden wir ` tr()` (), um die Übersetzung der Tastenkombination zusammen mit der Sprachübersetzung der Anwendung zu ermöglichen.

Nun sehen wir uns die Erstellung einer Gruppe sich gegenseitig ausschließender Aktionen in einem Untermenü des Menüs „Tablet“ an, mit denen ausgewählt werden kann, welche Eigenschaft der jeweiligen „ QTabletEvent “ verwendet wird, um die Transparenz (Alpha-Kanal) der gezeichneten Linie oder der mit dem Airbrush-Werkzeug aufgetragenen Farbe zu variieren.

    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);

Wir möchten, dass der Benutzer wählen kann, ob die Alpha-Komponente der Zeichenfarbe durch den Druck auf das Tablet, die Neigung oder die Position des Daumenrads am Airbrush-Werkzeug moduliert werden soll. Wir haben für jede Auswahl eine Aktion und eine zusätzliche Aktion, um den Alpha-Wert nicht zu ändern, d. h. die Farbe deckend zu belassen. Wir machen die Aktionen aktivierbar; der „ alphaChannelGroup “ stellt dann sicher, dass zu jedem Zeitpunkt nur eine der Aktionen aktiviert ist. Das Signal „ triggered() “ wird von der Gruppe ausgesendet, wenn eine Aktion aktiviert ist; daher verbinden wir dieses mit „ MainWindow::setAlphaValuator() “. Da das System wissen muss, auf welche Eigenschaft (Valuator) des „ QTabletEvent “ es von nun an achten soll, verwenden wir die Eigenschaft „ QAction::data “, um diese Information weiterzugeben. (Damit dies möglich ist, muss die Enumeration „ Valuator “ ein registrierter Metatyp sein, damit sie in einen „ QVariant “ eingefügt werden kann. Dies wird durch die Deklaration „ Q_ENUM “ in „tabletcanvas.h“ erreicht.)

Hier ist die Implementierung von ` setAlphaValuator()`:

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

Sie muss lediglich die Enumeration „ Valuator “ aus „ QAction::data()“ abrufen und diese an „ TabletCanvas::setAlphaChannelValuator() “ übergeben. Würden wir die Eigenschaft „ data “ nicht verwenden, müssten wir stattdessen den Zeiger „ QAction “ selbst vergleichen, beispielsweise in einer switch-Anweisung. Dies würde jedoch erfordern, dass wir zu Vergleichszwecken Zeiger auf jeden „ QAction “ in Klassenvariablen speichern.

Hier ist die Implementierung von „ 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);
}

Wir führen eine verzögerte Initialisierung von ` QColorDialog ` durch , sobald der Benutzer zum ersten Mal „Pinselfarbe…“ aus dem Menü oder über die Aktionsverknüpfung auswählt. Solange der Dialog geöffnet ist, wird jedes Mal, wenn der Benutzer eine andere Farbe auswählt, die Methode ` TabletCanvas::setColor() ` aufgerufen, um die Zeichenfarbe zu ändern. Da es sich um einen nicht-modalen Dialog handelt, steht es dem Benutzer frei, den Farbdialog geöffnet zu lassen, um bequem und häufig die Farben ändern zu können, oder ihn zu schließen und später wieder zu öffnen.

Hier ist die Implementierung von „ 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;
}

Wir verwenden die Funktion „ QFileDialog “, damit der Benutzer eine Datei zum Speichern der Zeichnung auswählen kann, und rufen anschließend „ TabletCanvas::saveImage() “ auf, um die Zeichnung tatsächlich in die Datei zu schreiben.

Hier ist die Implementierung von ` 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");
}

Wir lassen den Benutzer die Bilddatei auswählen, die mit einem ` QFileDialog` geöffnet werden soll; anschließend weisen wir die Leinwand an, das Bild mit ` loadImage()` zu laden.

Hier ist die Implementierung von „ about() “:

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

Wir zeigen ein Meldungsfeld mit einer kurzen Beschreibung des Beispiels an.

Definition der Klasse „TabletCanvas“

Die Klasse „ TabletCanvas “ stellt eine Oberfläche bereit, auf der der Benutzer mit einem Tablet zeichnen kann.

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;
};

Die Leinwand kann den Alphakanal, die Farbsättigung und die Strichstärke des Strichs ändern. Wir verfügen über eine Enumeration, die die Eigenschaften von ` QTabletEvent ` auflistet, mit denen diese Parameter angepasst werden können. Für jede Eigenschaft führen wir eine private Variable: ` m_alphaChannelValuator`, ` m_colorSaturationValuator ` und ` m_lineWidthValuator`, und wir stellen Zugriffsfunktionen dafür bereit.

Wir zeichnen auf einem „ QPixmap “ mit „ m_pen “ und „ m_brush “ unter Verwendung von „ m_color “. Jedes Mal, wenn ein „ QTabletEvent “ empfangen wird, wird der Strich von „ lastPoint “ bis zu dem in der aktuellen „ QTabletEvent “ angegebenen Punkt gezeichnet, und anschließend werden Position und Drehung in „ lastPoint “ für das nächste Mal gespeichert. Die Funktionen „ saveImage() “ und „ loadImage() “ speichern und laden die „ QPixmap “ auf die Festplatte. Die Pixmap wird im Widget in „ paintEvent() “ gezeichnet.

Die Auswertung der Ereignisse vom Tablet erfolgt in tabletEvent(), und paintPixmap(), updateBrush() sowie updateCursor() sind Hilfsfunktionen, die von tabletEvent() verwendet werden.

Implementierung der Klasse „TabletCanvas“

Wir beginnen mit einem Blick auf den Konstruktor:

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);
}

Im Konstruktor initialisieren wir die meisten unserer Klassenvariablen.

Hier ist die Implementierung von „ saveImage() “:

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

QPixmap stellt eine Funktion bereit, mit der es sich selbst auf der Festplatte speichert, daher rufen wir einfach ` save()` auf.

Hier ist die Implementierung von ` loadImage()`:

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

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

Wir rufen einfach ` load()` auf, wodurch das Bild von file geladen wird.

Hier ist die Implementierung von ` 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();
}

Wir erhalten drei Arten von Ereignissen in dieser Funktion: ` TabletPress`, ` TabletRelease` und ` TabletMove`, die generiert werden, wenn ein Zeichenwerkzeug auf dem Tablet aufgedrückt, abgehoben oder über das Tablet bewegt wird. Wir setzen ` m_deviceDown ` auf ` true `, wenn ein Gerät auf dem Tablet aufgedrückt wird; dann wissen wir, dass wir zeichnen sollten, wenn wir Bewegungsereignisse erhalten. Wir haben „ updateBrush() “ implementiert, um „ m_brush “ und „ m_pen “ entsprechend den vom Benutzer ausgewählten Tablet-Ereigniseigenschaften zu aktualisieren. Die Funktion „ updateCursor() “ wählt einen Cursor aus, der das verwendete Zeichenwerkzeug darstellt, sodass Sie, wenn Sie mit dem Werkzeug in der Nähe des Tablets schweben, sehen können, welche Art von Strich Sie gerade zeichnen werden.

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);
}

Wenn ein Art-Pen (RotationStylus) verwendet wird, wird bei jedem „ TabletMove “-Ereignis zusätzlich „ updateCursor() “ aufgerufen, wodurch ein gedrehter Cursor dargestellt wird, sodass Sie den Winkel der Stiftspitze erkennen können.

Hier ist die Implementierung von 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);
}

Wenn Qt zum ersten Mal `paintEvent()` aufruft, wird `m_pixmap` mit dem Standardkonstruktor initialisiert, sodass ` QPixmap::isNull()` ` true` zurückgibt. Da wir nun wissen, auf welchem Bildschirm wir rendern werden, können wir eine Pixmap mit der entsprechenden Auflösung erstellen. Die Größe der Pixmap, mit der wir das Fenster füllen, hängt von der Bildschirmauflösung ab, da das Beispiel keinen Zoom unterstützt; außerdem kann es sein, dass ein Bildschirm eine hohe DPI-Auflösung hat, ein anderer hingegen nicht. Wir müssen auch den Hintergrund zeichnen, da dieser standardmäßig grau ist.

Danach zeichnen wir die Pixmap einfach in die obere linke Ecke des Widgets.

Hier ist die Implementierung von ` 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("Dieses Eingabegerät wird von diesem Beispiel nicht unterstützt."));
#if QT_CONFIG(statustip)
                QStatusTipEvent status(error);
                QCoreApplication::sendEvent(this, &status);
#else
                qWarning() << error;
#endif
            }
            break;
        default:
            {
                const QString error(tr("Unbekanntes Tablet-Gerät – wird als Stift behandelt"));
#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;
    }
}

In dieser Funktion zeichnen wir auf der Pixmap basierend auf der Bewegung des Werkzeugs. Wenn das auf dem Tablet verwendete Werkzeug ein Stift ist, möchten wir eine Linie von der zuletzt bekannten Position zur aktuellen Position zeichnen. Wir gehen außerdem davon aus, dass dies eine sinnvolle Vorgehensweise für jedes unbekannte Gerät ist, aktualisieren jedoch die Statusleiste mit einer Warnung. Handelt es sich um einen Airbrush, möchten wir einen Kreis zeichnen, der mit einem sanften Farbverlauf gefüllt ist, dessen Intensität von verschiedenen Ereignisparametern abhängen kann. Standardmäßig hängt dies vom tangentialen Druck ab, also der Position des Fingerrads am Airbrush. Ist das Werkzeug ein Rotationsstift, simulieren wir einen Filzstift, indem wir trapezförmige Strichsegmente zeichnen.

        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;

In ` updateBrush() ` legen wir den Stift und den Pinsel für das Zeichnen so fest, dass sie mit ` m_alphaChannelValuator`, ` m_lineWidthValuator`, ` m_colorSaturationValuator` und ` m_color` übereinstimmen. Wir werden den Code untersuchen, um ` m_brush ` und ` m_pen ` für jede dieser Variablen einzurichten:

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);

Wir rufen den Farbton, die Sättigung, den Helligkeitswert und den Alpha-Wert der aktuellen Zeichenfarbe ab. „ hValue “ und „ vValue “ werden auf die horizontale und vertikale Neigung als Zahl zwischen 0 und 255 gesetzt. Die ursprünglichen Werte liegen in Grad zwischen -60 und 60, d. h., 0 entspricht -60, 127 entspricht 0 und 255 entspricht 60 Grad. Der gemessene Winkel wird zwischen dem Gerät und der Senkrechten zum Tablet gemessen (siehe QTabletEvent für eine Veranschaulichung).

    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);
    }

Der Alphakanal von ` QColor ` wird als Zahl zwischen 0 und 255 angegeben, wobei 0 transparent und 255 undurchsichtig ist, oder als Gleitkommazahl, wobei 0 transparent und 1,0 undurchsichtig ist. ` pressure()` gibt den Druck als `qreal` zwischen 0,0 und 1,0 zurück. Die kleinsten Alpha-Werte (d. h., die Farbe ist am transparentesten) erhalten wir, wenn der Stift senkrecht zum Tablet steht. Wir wählen den größeren der vertikalen und horizontalen Neigungswerte aus.

    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:
            ;
    }

Die Farbsättigung im HSV-Farbmodell kann als Ganzzahl zwischen 0 und 255 oder als Gleitkommazahl zwischen 0 und 1 angegeben werden. Da wir uns dafür entschieden haben, den Alpha-Wert als Ganzzahl darzustellen, rufen wir ` setHsv()` mit Ganzzahlwerten auf. Das bedeutet, dass wir den Druck mit einer Zahl zwischen 0 und 255 multiplizieren müssen.

    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);
    }

Die Breite des Stiftstrichs kann bei entsprechender Einstellung mit dem Druck zunehmen. Wenn die Stiftbreite jedoch durch die Neigung gesteuert wird, lassen wir die Breite mit dem Winkel zwischen dem Stift und der Senkrechten zum Tablet zunehmen.

    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);
    }
}

Abschließend prüfen wir, ob es sich bei dem Zeiger um den Stift oder den Radierer handelt. Handelt es sich um den Radierer, setzen wir die Farbe auf die Hintergrundfarbe der Pixmap und lassen den Druck die Strichbreite bestimmen; andernfalls setzen wir die Farben, die wir zuvor in der Funktion festgelegt haben.

Klassendefinition „TabletApplication“

Wir erben in dieser Klasse von „ QApplication “, da wir die Funktion „ event()“ neu implementieren möchten.

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 existiert als Unterklasse von ` QApplication `, um Tablet-Näherungsereignisse zu empfangen und an ` TabletCanvas` weiterzuleiten. Die Ereignisse ` TabletEnterProximity ` und ` TabletLeaveProximity ` werden an das Objekt ` QApplication ` gesendet, während andere Tablet-Ereignisse an den ` event() `-Handler von ` QWidget` gesendet werden, der sie wiederum an ` tabletEvent()` weiterleitet.

Implementierung der Klasse „TabletApplication“

Hier ist die Implementierung von ` 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);
}

Wir verwenden diese Funktion zur Verarbeitung der Ereignisse „ TabletEnterProximity “ und „ TabletLeaveProximity “, die ausgelöst werden, wenn ein Zeichenwerkzeug in die Nähe des Tablets gelangt oder diese verlässt. Hier rufen wir „ TabletCanvas::setTabletDevice() “ auf, das wiederum „ updateCursor() “ aufruft, wodurch ein geeigneter Cursor gesetzt wird. Dies ist der einzige Grund, warum wir die Näherungsereignisse benötigen; für korrektes Zeichnen reicht es aus, dass „ TabletCanvas “ die Methoden „ device()“ und „ pointerType()“ in jedem empfangenen Ereignis überwacht.

Die Funktion „ main() “

Hier ist die Funktion „ main() “ aus dem Beispiel:

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();
}

Hier erstellen wir ein „ MainWindow “ und zeigen es als Fenster auf oberster Ebene an. Wir verwenden die Klasse „ TabletApplication “. Wir müssen die Leinwand erst nach dem Erstellen der Anwendung einrichten. Wir können keine Klassen verwenden, die die Ereignisbehandlung implementieren, bevor ein „ QApplication “-Objekt instanziiert wurde.

Beispielprojekt @ 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.