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

当在 Qt 应用程序中使用数位板时,会生成QTabletEvent事件。若要处理数位板事件,您需要重写tabletEvent() 事件处理函数。 当用于绘图的工具(触控笔)进入或离开数位板的近距离范围时(即触控笔靠近数位板但未按下时)、当工具被按下或松开时、当工具在数位板上移动时, 以及工具上的某个按钮被按下或松开时,都会触发事件。
QTabletEvent 中可获取的信息取决于所使用的设备。本示例可处理最多支持三种不同绘图工具的绘图板:触控笔、喷枪和艺术笔。 对于上述任何一种工具,事件都将包含工具的位置、在绘图板上的压力、按钮状态、垂直倾斜度和水平倾斜度(即设备与绘图板垂线之间的角度,如果绘图板硬件能够提供该数据的话)。 喷枪配有指轮;其位置信息同样包含在绘图板事件中。艺术笔支持围绕垂直于绘图板表面的轴进行旋转,因此可用于书法创作。
在本示例中,我们将实现一个绘图程序。您可以像用铅笔在纸上画画一样,使用触控笔在绘图板上绘图。使用喷笔绘图时,会产生虚拟颜料的喷溅效果;指轮用于调节喷溅的密度。 使用艺术笔绘图时,所绘线条的宽度和端点角度取决于笔的旋转角度。还可以将笔压和倾斜度用于调整颜色的透明度和饱和度,以及笔画的宽度。
该示例包含以下内容:
MainWindow类继承自QMainWindow ,负责创建菜单并连接其插槽和信号。TabletCanvas类继承自QWidget ,并接收数位板事件。它利用这些事件在屏外位图上绘制图像,然后进行渲染。TabletApplication类继承自QApplication 。该类负责处理平板电脑的接近事件。main()函数会创建一个MainWindow对象,并将其作为顶级窗口显示。
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::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() 来实现快捷键随应用程序语言翻译而同步翻译。
现在,我们将探讨如何在“数位板”菜单的子菜单中创建一组互斥操作,用于选择每个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成分应由数位板的压力、倾斜度,还是喷枪工具上滚轮的位置来调节。 针对每种选项,我们设置了一个操作,并额外提供一个操作用于选择不更改透明度(即保持颜色不透明)。我们将这些操作设为可选状态;此时“alphaChannelGroup ”将确保任何时候仅有一个操作被选中。 当某个操作被选中时,组会发出triggered() 信号,因此我们将该信号连接到MainWindow::setAlphaValuator() 。它需要知道从现在起应关注QTabletEvent 的哪个属性(valuator),因此我们使用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);
}当用户首次通过菜单或快捷键选择“Brush color...”时,我们会延迟初始化一个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;
};该画布可以调整笔触的透明度、色彩饱和度以及线宽。我们提供了一个枚举,列出了QTabletEvent 的各项属性,通过这些属性可以对上述参数进行调节。我们为每一项都维护了一个私有变量:m_alphaChannelValuator 、m_colorSaturationValuator 和m_lineWidthValuator ,并为它们提供了访问函数。
我们利用m_color 在QPixmap 上绘制m_pen 和m_brush 。每次接收到QTabletEvent 时,都会从lastPoint 绘制笔画至当前QTabletEvent 指定的点,随后将位置和旋转信息保存到lastPoint 以备下次使用。saveImage() 和loadImage() 函数负责将QPixmap 保存至磁盘并从磁盘加载。在paintEvent() 中,将位图绘制到控件上。
来自数位板的事件解析在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();
}该函数会接收三类事件:TabletPress 、TabletRelease 和TabletMove ,这些事件分别在绘图工具按下、抬起或在数位板上移动时触发。当设备在数位板上被按下时,我们将m_deviceDown 设置为true ;这样,当接收到移动事件时,我们就知道应该进行绘制。 我们已实现updateBrush() 方法,根据用户选择关注的绘图板事件属性,相应地更新m_brush 和m_pen 。updateCursor() 函数会选择一个光标来表示当前使用的绘图工具,这样当您将工具悬停在绘图板附近时,就能预览即将绘制的笔画效果。
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 ),则每次发生TabletMove 事件时,还会调用updateCursor() ,并渲染一个旋转的光标,以便您看到笔尖的角度。
以下是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,而另一个则不是。我们还需要绘制背景,因为默认背景是灰色的。
之后,我们只需将该像素图绘制到控件的左上角即可。
以下是paintPixmap() 的实现代码:
voidTabletCanvas::paintPixmap(QPainter&painter,QTabletEvent*event)
{
staticqreal maxPenRadius=pressureToWidth(1.0);
painter.setRenderHint(QPainter::Antialiasing);
switch(event->deviceType()) {
caseQInputDevice::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;
caseQInputDevice::DeviceType::Puck:
caseQInputDevice::DeviceType::Mouse:
{
constQString error(tr("本示例不支持此输入设备。"));
#if QT_CONFIG(statustip)
QStatusTipEvent status(error);
QCoreApplication::sendEvent(this, &status);
#else
qWarning() << error;
#endif
}
break;
default:
{
constQString error(tr("未知平板设备——将其视为触控笔"));
#if QT_CONFIG(statustip)
QStatusTipEvent status(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);
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;
}
}在此函数中,我们根据工具的移动在位图上进行绘制。如果数位板上使用的工具是触控笔,则希望从已知的最后位置绘制一条线至当前位置。我们也假设这是对任何未知设备的合理处理方式,但会通过状态栏显示警告信息。 如果是喷笔,我们将绘制一个填充柔和渐变色的圆形,其渐变密度可取决于各种事件参数。默认情况下,它取决于切向压力,即喷笔上指轮的位置。如果工具是旋转式触控笔,我们将通过绘制梯形笔画段来模拟毡笔的效果。
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_colorSaturationValuator 以及m_color 对应。我们将查看用于为这些变量设置m_brush 和m_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);我们获取当前绘图颜色(drawingcolor)的色相、饱和度、明度和透明度值。hValue 和vValue 分别设置为水平和垂直倾斜角,数值范围为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 的透明度通道以 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);
}我们使用此函数来处理TabletEnterProximity 和TabletLeaveProximity 事件,这些事件是在绘图工具进入或离开数位板的邻近区域时触发的。 在此我们调用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 对象实例化之前,不能使用实现事件处理的类。
© 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.