Tablet-Beispiel
Dieses Beispiel zeigt, wie man ein Wacom-Tablett in Qt-Anwendungen verwendet.
Wenn Sie ein Tablett mit Qt-Anwendungen verwenden, wird QTabletEventgeneriert. Sie müssen den tabletEvent() Event-Handler neu implementieren, wenn Sie Tablet-Ereignisse behandeln wollen. Ereignisse werden erzeugt, wenn das zum Zeichnen verwendete Werkzeug (Stift) in die Nähe des Tabletts kommt und es verlässt (d.h. wenn es geschlossen, aber nicht darauf gedrückt wird), wenn das Werkzeug gedrückt und losgelassen wird, wenn das Werkzeug über das Tablett bewegt wird und wenn eine der Tasten des Werkzeugs gedrückt oder losgelassen wird.
Die auf QTabletEvent verfügbaren Informationen hängen von dem verwendeten Gerät ab. Dieses Beispiel kann mit einem Tablett mit bis zu drei verschiedenen Zeichenwerkzeugen umgehen: einem Stylus, einem Airbrush und einem Zeichenstift. Für jedes dieser Werkzeuge enthält das Ereignis die Position des Werkzeugs, den Druck auf das Tablett, den Status der Tasten, die vertikale und horizontale Neigung (d. h. den Winkel zwischen dem Gerät und der Senkrechten des Tabletts, sofern die Tablett-Hardware dies unterstützen kann). Die Airbrush verfügt über ein Fingerrad, dessen Position ebenfalls im Tablett-Ereignis angezeigt wird. Der Zeichenstift kann um die Achse senkrecht zur Tablettoberfläche gedreht werden, so dass er für Kalligraphie verwendet werden kann.
In diesem Beispiel implementieren wir ein Zeichenprogramm. Sie können mit dem Stift auf dem Tablett zeichnen, wie Sie einen Bleistift auf Papier verwenden. Wenn Sie mit der Airbrush zeichnen, erhalten Sie einen virtuellen Farbstrahl; mit dem Fingerrad können Sie die Dichte des Strahls verändern. Wenn Sie mit dem Kunststift zeichnen, erhalten Sie eine Linie, deren Breite und Endpunktwinkel von der Drehung des Stifts abhängen. Druck und Neigung können auch zugewiesen werden, um die Alpha- und Sättigungswerte der Farbe und die Breite des Strichs zu ändern.
Das Beispiel besteht aus folgendem:
- Die Klasse
MainWindow
erbt QMainWindow, erstellt die Menüs und verbindet deren Slots und Signale. - Die Klasse
TabletCanvas
erbt QWidget und empfängt Tablet-Ereignisse. Sie verwendet die Ereignisse, um auf eine Pixmap außerhalb des Bildschirms zu malen, und rendert sie dann. - Die Klasse
TabletApplication
erbt von QApplication. Diese Klasse verarbeitet Ereignisse zur Annäherung an das Tablett. - Die Funktion
main()
erstellt einMainWindow
und zeigt es als Fenster der obersten Ebene an.
Definition der Klasse MainWindow
Die Funktion 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 eine QActionGroup für die Aktionen, die jeweils den Alphakanal, die Farbsättigung und die Linienstärke verändern. Die Aktionsgruppen sind mit den Slots setAlphaValuator()
, setSaturationValuator()
und setLineWidthValuator()
verbunden, die Funktionen in TabletCanvas
aufrufen.
Implementierung der MainWindow-Klasse
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 Dateimenü aus. Wir verwenden eine Überladung von addAction(), die in Qt 5.6 eingeführt wurde, um ein Menüelement mit einer Verknüpfung (und optional einem Symbol) zu erstellen, es zu seinem Menü hinzuzufügen und es mit einem Slot zu verbinden, alles mit einer Zeile Code. Wir verwenden QKeySequence, um die plattformspezifischen Standardtastenkombinationen für diese allgemeinen Menüpunkte zu erhalten.
Wir fügen auch das Menü Pinsel hinzu. Für den Befehl zum Ändern eines Pinsels gibt es normalerweise keine Standardtastenkombination, daher verwenden wir tr(), um die Übersetzung der Tastenkombination zusammen mit der Sprachübersetzung der Anwendung zu ermöglichen.
Jetzt werden wir uns die Erstellung einer Gruppe von sich gegenseitig ausschließenden Aktionen in einem Untermenü des Tablet-Menüs ansehen, um auszuwählen, welche Eigenschaft von jedem QTabletEvent verwendet wird, um die Transluzenz (Alphakanal) der gezeichneten Linie oder der Farbe, die mit Airbrush bearbeitet wird, zu verändern.
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);
Der Benutzer soll wählen können, ob die Alphakomponente der Zeichenfarbe durch den Druck auf das Tablett, die Neigung oder die Position des Daumenrads am Airbrush-Werkzeug moduliert werden soll. Wir haben eine Aktion für jede Wahl und eine zusätzliche Aktion, um die Alphakomponente nicht zu ändern, d. h. die Farbe undurchsichtig zu lassen. Wir machen die Aktionen überprüfbar; alphaChannelGroup
stellt dann sicher, dass immer nur eine der Aktionen aktiviert ist. Das Signal triggered()
wird von der Gruppe ausgegeben, wenn eine Aktion geprüft wird, also verbinden wir es mit MainWindow::setAlphaValuator()
. Es muss wissen, welche Eigenschaft (valuator) von QTabletEvent von nun an zu beachten ist, also verwenden wir die Eigenschaft QAction::data, um diese Information weiterzugeben. (Damit dies möglich ist, muss das Enum Valuator
ein registrierter Metatyp sein, so dass es in ein 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 das Valuator
enum von QAction::data() abrufen und an TabletCanvas::setAlphaChannelValuator()
übergeben. Würden wir die Eigenschaft data
nicht verwenden, müssten wir stattdessen den QAction -Zeiger selbst vergleichen, zum Beispiel in einer switch-Anweisung. Dies würde jedoch erfordern, dass Zeiger auf jede QAction in Klassenvariablen zu Vergleichszwecken gespeichert werden.
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); }
Wenn der Benutzer zum ersten Mal "Pinselfarbe..." aus dem Menü oder über das Aktionskürzel auswählt, wird eine "Lazy Initialization" von QColorDialog durchgeführt. Während der Dialog geöffnet ist, wird jedes Mal, wenn der Benutzer eine andere Farbe wählt, 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 Farben zu ändern, 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 QFileDialog, damit der Benutzer eine Datei zum Speichern der Zeichnung auswählen kann, und rufen dann TabletCanvas::saveImage()
auf, um sie 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 QFileDialog geöffnet werden soll, und bitten dann die Leinwand, das Bild mit loadImage()
zu laden.
Hier sehen Sie 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 Nachrichtenfeld mit einer kurzen Beschreibung des Beispiels an.
Definition der Klasse TabletCanvas
Die Klasse TabletCanvas
bietet eine Oberfläche, 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 haben ein Enum, das die QTabletEvent Eigenschaften auflistet, mit denen es möglich ist, sie zu modulieren. Wir behalten für jede eine private Variable: m_alphaChannelValuator
m_colorSaturationValuator
und m_lineWidthValuator
, und wir stellen Zugriffsfunktionen für sie bereit.
Wir zeichnen auf ein QPixmap mit m_pen
und m_brush
mit m_color
. Jedes Mal, wenn ein QTabletEvent empfangen wird, wird der Strich von lastPoint
bis zu dem Punkt gezeichnet, der im aktuellen QTabletEvent angegeben ist, und dann werden die Position und die 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 auf dem Widget in paintEvent()
gezeichnet.
Die Interpretation der Ereignisse vom Tablet erfolgt in tabletEvent()
, und paintPixmap()
, updateBrush()
und updateCursor()
sind Hilfsfunktionen, die von tabletEvent()
verwendet werden.
Implementierung der TabletCanvas-Klasse
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 Der Konstruktor implementiert die Funktion, sich selbst auf der Festplatte zu speichern, also 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, das das Bild von file lädt.
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 für diese Funktion: TabletPress
TabletRelease
und TabletMove
, die erzeugt werden, wenn ein Zeichenwerkzeug auf das Tablett gedrückt, von diesem abgehoben oder über das Tablett bewegt wird. Wir setzen m_deviceDown
auf true
, wenn ein Gerät auf das Tablett gedrückt wird; so wissen wir, dass wir zeichnen sollten, wenn wir Bewegungsereignisse erhalten. Wir haben updateBrush()
implementiert, um m_brush
und m_pen
zu aktualisieren, je nachdem, welche der Tablett-Ereigniseigenschaften der Benutzer ausgewählt hat, um darauf zu achten. Die Funktion updateCursor()
wählt einen Cursor aus, um das verwendete Zeichenwerkzeug darzustellen, so dass Sie, wenn Sie mit dem Werkzeug in der Nähe des Tabletts schweben, sehen können, welche Art von Strich Sie gerade machen 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 Zeichenstift (RotationStylus
) verwendet wird, wird auch updateCursor()
bei jedem TabletMove
Ereignis aufgerufen und ein gedrehter Cursor dargestellt, damit Sie den Winkel der Stiftspitze sehen 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 standardmäßig konstruiert, so dass 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; und es kann sein, dass ein Bildschirm einen hohen DPI-Wert hat und ein anderer nicht. Auch der Hintergrund muss gezeichnet werden, da er standardmäßig grau ist.
Danach zeichnen wir die Pixmap einfach oben links in das Widget.
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 - Behandlung als Stift"));#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 Tablett verwendete Werkzeug ein Stift ist, wollen wir eine Linie von der letzten bekannten Position zur aktuellen Position zeichnen. Wir gehen auch davon aus, dass dies ein vernünftiger Umgang mit einem unbekannten Gerät ist, aktualisieren aber die Statusleiste mit einer Warnung. Wenn es sich um einen Airbrush handelt, wollen wir einen Kreis zeichnen, der mit einem weichen Farbverlauf gefüllt ist, dessen Dichte von verschiedenen Ereignisparametern abhängen kann. Standardmäßig hängt sie vom tangentialen Druck ab, d. h. von der Position des Fingerrads auf der Airbrush. Wenn das Werkzeug ein Rotationsstift ist, 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()
stellen wir den zum Zeichnen verwendeten Stift und Pinsel so ein, dass er m_alphaChannelValuator
, m_lineWidthValuator
, m_colorSaturationValuator
und m_color
entspricht. Wir werden den Code zum Einrichten von m_brush
und m_pen
für jede dieser Variablen untersuchen:
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 holen die Werte für Farbton, Sättigung, Wert und Alpha der aktuellen Zeichenfarbe. hValue
und vValue
werden auf die horizontale und vertikale Neigung als Zahl von 0 bis 255 gesetzt. Die Originalwerte sind in Grad von -60 bis 60 angegeben, d. h. 0 ist gleich -60, 127 ist gleich 0 und 255 ist gleich 60 Grad. Der gemessene Winkel ist der zwischen dem Gerät und der Senkrechten des Tabletts (siehe QTabletEvent für eine 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); }
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. Wir erhalten die kleinsten Alphawerte (d.h. die Farbe ist am transparentesten), wenn der Stift senkrecht zum Tablett steht. Wir wählen den größten Wert für die vertikale und horizontale Neigung 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 Fließkommawert zwischen 0 und 1 angegeben werden. Wir haben uns dafür entschieden, Alpha als Ganzzahl darzustellen, also rufen wir setHsv() mit Ganzzahlwerten auf. Das heißt, wir müssen den Druck mit einer Zahl zwischen 0 und 255 multiplizieren.
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 mit dem Druck zunehmen, wenn dies gewünscht wird. Aber wenn die Stiftbreite durch die Neigung gesteuert wird, lassen wir die Breite mit dem Winkel zwischen dem Werkzeug und der Senkrechten auf dem Tablett wachsen.
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); } }
Schließlich prüfen wir, ob der Zeiger der Stift oder der Radiergummi ist. Wenn es sich um den Radiergummi handelt, setzen wir die Farbe auf die Hintergrundfarbe der Pixmap und lassen den Druck die Stiftbreite bestimmen, ansonsten setzen wir die Farben, die wir zuvor in der Funktion festgelegt haben.
Definition der Klasse TabletApplication
Wir erben QApplication in dieser Klasse, weil wir die Funktion event() neu implementieren wollen.
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 Ereignisse in der Nähe des Tablets zu empfangen und sie an TabletCanvas
weiterzuleiten. Die Ereignisse TabletEnterProximity
und TabletLeaveProximity
werden an das Objekt QApplication gesendet, während andere Tablett-Ereignisse an den Handler event()
von QWidget gesendet werden, der sie 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, um die Ereignisse TabletEnterProximity
und TabletLeaveProximity
zu behandeln, die erzeugt werden, wenn ein Zeichenwerkzeug in die Nähe des Tabletts kommt oder es verlässt. Hier rufen wir TabletCanvas::setTabletDevice()
auf, das dann updateCursor()
aufruft, wodurch ein entsprechender Cursor gesetzt wird. Dies ist der einzige Grund, warum wir die Annäherungsereignisse benötigen; für das korrekte Zeichnen reicht es aus, dass TabletCanvas
die device() und pointerType() in jedem Ereignis, das es empfängt, beobachtet.
Die Funktion main()
Hier ist die main()
Funktion des Beispiels:
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 der obersten Ebene an. Wir verwenden die Klasse TabletApplication
. Wir müssen die Leinwand nach der Erstellung der Anwendung festlegen. Wir können keine Klassen verwenden, die eine Ereignisbehandlung implementieren, bevor ein QApplication Objekt instanziiert ist.
© 2025 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.