手写板示例

本例演示了如何在 Qt 应用程序中使用 Wacom 手写板。

在 Qt XML 应用程序中使用手写板时,会生成QTabletEvents。如果要处理手写板事件,则需要重新实现tabletEvent() 事件处理程序。当用于绘图的工具(手写笔)进入或离开手写板附近时(即手写板关闭但未按下时)、工具按下或释放时、工具在手写板上移动时以及工具上的按钮按下或释放时,都会产生事件。

QTabletEvent 中提供的信息取决于所使用的设备。本示例可处理带有三种不同绘图工具的手写板:触控笔、喷笔和艺术笔。对于其中任何一种工具,事件都将包含工具的位置、手写板上的压力、按钮状态、垂直倾斜度和水平倾斜度(即设备与手写板垂直面之间的角度,如果手写板硬件可以提供)。喷笔有一个指轮;其位置也可在平板电脑事件中查看。艺术笔可绕垂直于平板表面的轴线旋转,因此可用于书法。

在本示例中,我们实现了一个绘图程序。您可以使用触控笔在平板电脑上绘图,就像在纸上使用铅笔一样。使用喷笔绘画时,会喷出虚拟颜料;手指滚轮可用于改变喷出颜料的密度。用艺术笔作画时,会出现一条线,其宽度和端点角度取决于笔的旋转。压力和倾斜度也可以用来改变颜色的 alpha 值和饱和度以及笔触的宽度。

示例包括以下内容:

  • MainWindow 类继承于QMainWindow ,创建菜单并连接它们的插槽和信号。
  • TabletCanvas 类继承于QWidget ,并接收绘图板事件。它使用这些事件在屏幕外的像素图上绘制,然后进行渲染。
  • TabletApplication 类继承于QApplication 。该类处理平板电脑接近事件。
  • main() 函数创建一个MainWindow 并将其显示为顶层窗口。

主窗口类定义

MainWindow 创建TabletCanvas 并将其设置为中心部件。

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() 主窗口类定义我们为分别改变 alpha 通道、色彩饱和度和线宽的操作设置了一个 。操作组与 、 和 插槽相连,这些插槽调用 中的函数。QActionGroup setAlphaValuator() setSaturationValuator() setLineWidthValuator() TabletCanvas

主窗口类的实现

我们先来看一下构造函数MainWindow()

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

在构造函数中,我们调用createMenus() 来创建所有操作和菜单,并将画布设置为中心部件。

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

createMenus() 的开头,我们将填充文件菜单。我们使用 Qt 5.6 中引入的addAction() 的重载来创建一个带有快捷方式(和可选图标)的菜单项,将其添加到菜单中,并将其连接到插槽,所有这一切只需一行代码。我们使用QKeySequence 获取这些常用菜单项的特定平台标准快捷键。

我们还填充了画笔菜单。更改画笔的命令通常没有标准快捷键,因此我们使用tr() 使快捷键与应用程序的语言翻译一起翻译。

现在,我们将研究在Tablet菜单的子菜单中创建一组互斥操作,用于选择每个QTabletEvent 的哪个属性将用于改变正在绘制的线条或正在喷绘的颜色的半透明度(alpha 通道)。

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

我们希望用户能够选择绘图颜色的 alpha 分量是由手写板压力、倾斜度还是喷刷工具上拇指轮的位置来调节。我们为每种选择都设置了一个操作,另外还有一个操作用于选择不改变 alpha,即保持颜色不透明。我们将这些操作设置为可选中;这样,alphaChannelGroup 就能确保在任何时候都只选中其中一个操作。当一个操作被选中时,组会发出triggered() 信号,因此我们将其连接到MainWindow::setAlphaValuator() 。它需要知道从现在起要关注QTabletEvent 的哪个属性(估值器),因此我们使用QAction::data 属性来传递此信息。(为了实现这一点,枚举Valuator 必须是已注册的元类型,这样才能插入到QVariant 中。这可以通过 tabletcanvas.h 中的Q_ENUM 声明来实现)。

下面是setAlphaValuator() 的实现:

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

它只需从QAction::data() 中获取Valuator 枚举,并将其传递给TabletCanvas::setAlphaChannelValuator() 。如果我们不使用data 属性,则需要比较QAction 指针本身,例如在 switch 语句中。但这需要在类变量中保留指向每个QAction 的指针,以便进行比较。

下面是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);
}

当用户第一次从菜单或通过快捷操作选择 "刷颜色... "时,我们会对QColorDialog 进行懒初始化。当对话框打开时,每次用户选择不同的颜色,TabletCanvas::setColor() 都会被调用以更改绘图颜色。由于这是一个非模式对话框,因此用户可以自由地打开颜色对话框,以便方便地频繁更改颜色,或者关闭对话框,稍后再重新打开。

下面是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;
}

我们使用QFileDialog 让用户选择保存绘图的文件,然后调用TabletCanvas::saveImage() 将其实际写入文件。

以下是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");
}

我们通过QFileDialog 让用户选择要打开的图像文件;然后通过loadImage() 要求画布加载图像。

以下是about() 的实现:

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

我们显示一个信息框,其中包含对示例的简短描述。

TabletCanvas 类定义

TabletCanvas 类提供了一个表面,用户可以在上面使用平板电脑绘图。

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

画布可以改变笔画的 alpha 通道、颜色饱和度和线宽。我们有一个枚举,列出了QTabletEvent 的属性,可以对这些属性进行调节。我们为每个属性保留一个私有变量:m_alphaChannelValuator m_colorSaturationValuatorm_lineWidthValuator ,并为它们提供访问函数。

我们使用m_pen 绘制QPixmap ,使用m_color 绘制m_brush 。每次收到QTabletEvent 时,笔触都会从lastPoint 画到当前QTabletEvent 中给出的点,然后将位置和旋转保存在lastPoint 中,以备下次使用。saveImage()loadImage() 函数将QPixmap 保存并加载到磁盘。像素图绘制在paintEvent() 的 widget 上。

对来自平板电脑的事件的解释在tabletEvent() 中完成,paintPixmap()updateBrush()updateCursor()tabletEvent() 使用的辅助函数。

TabletCanvas 类的实现

我们先来看看构造函数:

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

在构造函数中,我们将初始化大部分类变量。

下面是saveImage() 的实现:

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

QPixmap 实现了将自身保存到磁盘的功能,因此我们只需调用 () 即可。save

下面是loadImage() 的实现:

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

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

我们只需调用load() 即可从file 加载图像。

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

我们可以从该函数获取三种事件:TabletPressTabletReleaseTabletMove ,当绘图工具被按下、从平板上抬起或在平板上移动时,这些事件就会产生。当设备被按在平板上时,我们将m_deviceDown 设置为true ;这样我们就知道,当收到移动事件时,我们应该进行绘制。我们实现了updateBrush() ,以便根据用户选择关注的平板电脑事件属性更新m_brushm_penupdateCursor() 函数会选择一个光标来代表正在使用的绘图工具,这样当您将工具悬停在平板电脑附近时,就可以看到您将要绘制的笔画。

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

如果使用的是艺术笔 (RotationStylus),updateCursor() 也会在每次TabletMove 事件中被调用,并渲染一个旋转光标,以便您可以看到笔尖的角度。

以下是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);
}

Qt 第一次调用 paintEvent() 时,m_pixmap 是默认构建的,因此QPixmap::isNull() 返回true 。既然我们知道要渲染到哪个屏幕,就可以创建一个具有适当分辨率的像素图。我们填充窗口的像素图大小取决于屏幕分辨率,因为示例不支持缩放;而且可能一个屏幕是高 DPI,而另一个屏幕不是。我们还需要绘制背景,因为默认背景为灰色。

之后,我们只需将像素图绘制到 widget 的左上方即可。

以下是paintPixmap() 的实现:

voidTabletCanvas::paintPixmap(QPainter&painter QTabletEvent*事件 {静态qrealmaxPenRadius=pressureToWidth(1.0); painter.setRenderHint(QPainter::Antialiasing);switch(event->deviceType()) {caseQInputDevice::DeviceType::Airbrush: { painter.setPen(Qt::NoPen);           QRadialGradientgrad(lastPoint.pos,m_pen.widthF()* 10.0);        QColorcolor=m_brush.color(); color.setAlphaF(color.alphaF()*0 .25); grad.setColorAt(0,m_brush.color()); grad.setColorAt(0.5 Qt::transparent); painter.setBrush(grad);                qrealradius=grad.radius(); painter.drawEllipse(event->position(),radius,radius); update(QRect(事件->位置().toPoint()-QPoint(半径半径 QSize(radius* 2,radius* 2)); }break;caseQInputDevice::DeviceType::Puck:caseQInputDevice::DeviceType::Mouse: {constQStringerror(tr("This input device is not supported by the example."));#if QT_CONFIG(statustip)               QStatusTipEventstatus(error);                QCoreApplication::sendEvent(this, &status);#else                qWarning() << error;
#endif}break;默认:{constQStringerror(tr("Unknown tablet device - treating as stylus"));#if QT_CONFIG(statustip)               QStatusTipEventstatus(error);                QCoreApplication::sendEvent(this, &status);#else                qWarning() << error;
#endif} Q_FALLTHROUGH();caseQInputDevice::DeviceType::Stylus:if(event->pointingDevice()->capabilities().testFlag(QPointingDevice::Capability::Rotation)) { m_brush.setStyle(Qt::SolidPattern); painter.setPen(Qt::NoPen);painter.setBrush(m_brush);                QPolygonFpoly       qrealhalfWidth=pressureToWidth(lastPoint.pressure);                QPointFbrushAdjust(qSin(qDegreesToRadians(-lastPoint.rotation))*halfWidth                                    qCos(qDegreesToRadians(-lastPoint.rotation)) * halfWidth);
                poly<<lastPoint.pos+brushAdjust; poly<<lastPoint.pos-brushAdjust; halfWidth=m_pen.widthF(); brushAdjust=(  (  (-event->rotation())). 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; } }

在这个函数中,我们根据工具的移动在像素图上绘图。如果在手写板上使用的工具是触控笔,我们希望从最后一个已知位置到当前位置绘制一条直线。我们还假设这是对任何未知设备的合理处理,但会更新状态栏并发出警告。如果是喷笔,我们希望绘制一个填充有柔和渐变的圆,其密度取决于各种事件参数。默认情况下,它取决于切向压力,也就是喷笔上手指滚轮的位置。如果工具是旋转触笔,我们可以通过绘制梯形笔画段来模拟毛毡标记。

        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;

updateBrush() 中,我们设置了用于绘图的笔和画笔,以匹配m_alphaChannelValuator,m_lineWidthValuator,m_colorSaturationValuatorm_color 。我们将检查为每个变量设置m_brushm_pen 的代码:

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

我们将获取当前绘图颜色的色调、饱和度、值和 alpha 值。hValuevValue 设置为水平和垂直倾斜度,数值范围为 0 至 255。原始值的单位是从 -60 到 60 度,即 0 等于 -60,127 等于 0,255 等于 60 度。测量的角度是设备与平板垂直面之间的角度(请参阅QTabletEvent 以获取说明)。

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

QColor 的 alpha 通道用介于 0 和 255 之间的数字表示,其中 0 表示透明,255 表示不透明,或者用浮点数表示,其中 0 表示透明,1.0 表示不透明。pressure() 返回的压力值是介于 0.0 和 1.0 之间的 qreal 值。当笔垂直于平板时,我们会得到最小的 alpha 值(即颜色最透明)。我们选择最大的垂直和水平倾斜值。

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

HSV 颜色模型中的颜色饱和度可以用 0 到 255 之间的整数表示,也可以用 0 到 1 之间的浮点数表示。我们选择用整数表示 alpha 值,因此我们使用整数值调用setHsv()。这意味着我们需要将压力乘以 0 到 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);
    }

如果需要,笔画的宽度可以随压力的增加而增加。但是,当笔的宽度由倾斜度控制时,我们会让宽度随工具与平板垂直面之间的角度增加。

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

最后,我们要检查指针是触控笔还是橡皮擦。如果是橡皮擦,我们会将颜色设置为像素图的背景色,并让压力决定笔的宽度,否则我们会设置之前在函数中决定的颜色。

TabletApplication 类定义

我们在该类中继承了QApplication ,因为我们要重新实现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 该类作为 的子类存在,以便接收平板电脑接近事件并将其转发到 。 和 事件会发送到 对象,而其他平板电脑事件会发送到 的 处理程序,该处理程序会将这些事件发送到 () 。QApplication TabletCanvas TabletEnterProximity TabletLeaveProximity QApplication QWidget event() tabletEvent

TabletApplication 类的实现

下面是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);
}

我们使用该函数来处理TabletEnterProximityTabletLeaveProximity 事件,这些事件在绘图工具进入或离开平板电脑附近时发生。在这里,我们调用TabletCanvas::setTabletDevice() ,然后调用updateCursor() ,后者将设置适当的光标。这就是我们需要接近事件的唯一原因;为了正确绘图,TabletCanvas 只需观察其接收到的每个事件中的device() 和pointerType() 即可。

main() 函数

下面是示例中的main() 函数:

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

在此,我们创建一个MainWindow 并将其显示为顶层窗口。我们使用TabletApplication 类。我们需要在应用程序创建后设置画布。我们不能在QApplication 对象实例化之前使用实现事件处理的类。

示例项目 @ code.qt.io

© 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.