Tablet Example▲
When you use a tablet with Qt applications, QTabletEvents are generated. You need to reimplement the tabletEvent() event handler if you want to handle tablet events. Events are generated when the tool (stylus) used for drawing enters and leaves the proximity of the tablet (i.e., when it is close but not pressed down on it), when the tool is pressed down and released from it, when the tool is moved across the tablet, and when one of the buttons on the tool is pressed or released.
The information available in QTabletEvent depends on the device used. This example can handle a tablet with up to three different drawing tools: a stylus, an airbrush, and an art pen. For any of these the event will contain the position of the tool, pressure on the tablet, button status, vertical tilt, and horizontal tilt (i.e, the angle between the device and the perpendicular of the tablet, if the tablet hardware can provide it). The airbrush has a finger wheel; the position of this is also available in the tablet event. The art pen provides rotation around the axis perpendicular to the tablet surface, so that it can be used for calligraphy.
In this example we implement a drawing program. You can use the stylus to draw on the tablet as you use a pencil on paper. When you draw with the airbrush you get a spray of virtual paint; the finger wheel is used to change the density of the spray. When you draw with the art pen, you get a a line whose width and endpoint angle depend on the rotation of the pen. The pressure and tilt can also be assigned to change the alpha and saturation values of the color and the width of the stroke.
The example consists of the following:
-
The MainWindow class inherits QMainWindow, creates the menus, and connects their slots and signals.
-
The TabletCanvas class inherits QWidget and receives tablet events. It uses the events to paint onto an offscreen pixmap, and then renders it.
-
The TabletApplication class inherits QApplication. This class handles tablet proximity events.
-
The main() function creates a MainWindow and shows it as a top level window.
MainWindow Class Definition▲
The MainWindow creates a TabletCanvas and sets it as its center widget.
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() sets up the menus with the actions. We have one QActionGroup for the actions that alter the alpha channel, color saturation and line width respectively. The action groups are connected to the setAlphaValuator(), setSaturationValuator(), and setLineWidthValuator() slots, which call functions in TabletCanvas.
MainWindow Class Implementation▲
We start with a look at the constructor MainWindow():
MainWindow::
MainWindow(TabletCanvas *
canvas)
:
m_canvas(canvas)
{
createMenus();
setWindowTitle(tr("Tablet Example"
));
setCentralWidget(m_canvas);
QCoreApplication::
setAttribute(Qt::
AA_CompressHighFrequencyEvents);
}
In the constructor we call createMenus() to create all the actions and menus, and set the canvas as the center widget.
void
MainWindow::
createMenus()
{
QMenu *
fileMenu =
menuBar()-&
gt;addMenu(tr("&File"
));
fileMenu-&
gt;addAction(tr("&Open..."
), QKeySequence::
Open, this
, &
amp;MainWindow::
load);
fileMenu-&
gt;addAction(tr("&Save As..."
), QKeySequence::
SaveAs, this
, &
amp;MainWindow::
save);
fileMenu-&
gt;addAction(tr("&New"
), QKeySequence::
New, this
, &
amp;MainWindow::
clear);
fileMenu-&
gt;addAction(tr("E&xit"
), QKeySequence::
Quit, this
, &
amp;MainWindow::
close);
QMenu *
brushMenu =
menuBar()-&
gt;addMenu(tr("&Brush"
));
brushMenu-&
gt;addAction(tr("&Brush Color..."
), tr("Ctrl+B"
), this
, &
amp;MainWindow::
setBrushColor);
At the beginning of createMenus() we populate the File menu. We use an overload of addAction(), introduced in Qt 5.6, to create a menu item with a shortcut (and optionally an icon), add it to its menu, and connect it to a slot, all with one line of code. We use QKeySequence to get the platform-specific standard key shortcuts for these common menu items.
We also populate the Brush menu. The command to change a brush does not normally have a standard shortcut, so we use tr() to enable translating the shortcut along with the language translation of the application.
Now we will look at the creation of one group of mutually-exclusive actions in a submenu of the Tablet menu, for selecting which property of each QTabletEvent will be used to vary the translucency (alpha channel) of the line being drawn or color being airbrushed. (See the application example if you want a high-level introduction to QActions.)
QMenu *
alphaChannelMenu =
tabletMenu-&
gt;addMenu(tr("&Alpha Channel"
));
QAction *
alphaChannelPressureAction =
alphaChannelMenu-&
gt;addAction(tr("&Pressure"
));
alphaChannelPressureAction-&
gt;setData(TabletCanvas::
PressureValuator);
alphaChannelPressureAction-&
gt;setCheckable(true
);
QAction *
alphaChannelTangentialPressureAction =
alphaChannelMenu-&
gt;addAction(tr("T&angential Pressure"
));
alphaChannelTangentialPressureAction-&
gt;setData(TabletCanvas::
TangentialPressureValuator);
alphaChannelTangentialPressureAction-&
gt;setCheckable(true
);
alphaChannelTangentialPressureAction-&
gt;setChecked(true
);
QAction *
alphaChannelTiltAction =
alphaChannelMenu-&
gt;addAction(tr("&Tilt"
));
alphaChannelTiltAction-&
gt;setData(TabletCanvas::
TiltValuator);
alphaChannelTiltAction-&
gt;setCheckable(true
);
QAction *
noAlphaChannelAction =
alphaChannelMenu-&
gt;addAction(tr("No Alpha Channel"
));
noAlphaChannelAction-&
gt;setData(TabletCanvas::
NoValuator);
noAlphaChannelAction-&
gt;setCheckable(true
);
QActionGroup *
alphaChannelGroup =
new
QActionGroup(this
);
alphaChannelGroup-&
gt;addAction(alphaChannelPressureAction);
alphaChannelGroup-&
gt;addAction(alphaChannelTangentialPressureAction);
alphaChannelGroup-&
gt;addAction(alphaChannelTiltAction);
alphaChannelGroup-&
gt;addAction(noAlphaChannelAction);
connect(alphaChannelGroup, &
amp;QActionGroup::
triggered,
this
, &
amp;MainWindow::
setAlphaValuator);
We want the user to be able to choose whether the drawing color's alpha component should be modulated by the tablet pressure, tilt, or the position of the thumbwheel on the airbrush tool. We have one action for each choice, and an additional action to choose not to change the alpha, that is, to keep the color opaque. We make the actions checkable; the alphaChannelGroup will then ensure that only one of the actions are checked at any time. The triggered() signal is emitted from the group when an action is checked, so we connect that to MainWindow::setAlphaValuator(). It will need to know which property (valuator) of the QTabletEvent to pay attention to from now on, so we use the QAction::data property to pass this information along. (In order for this to be possible, the enum Valuator must be a registered metatype, so that it can be inserted into a QVariant. That is accomplished by the Q_ENUM declaration in tabletcanvas.h.)
Here is the implementation of setAlphaValuator():
void
MainWindow::
setAlphaValuator(QAction *
action)
{
m_canvas-&
gt;setAlphaChannelValuator(qvariant_cast&
lt;TabletCanvas::
Valuator&
gt;(action-&
gt;data()));
}
It simply needs to retrieve the Valuator enum from QAction::data(), and pass that to TabletCanvas::setAlphaChannelValuator(). If we were not using the data property, we would instead need to compare the QAction pointer itself, for example in a switch statement. But that would require keeping pointers to each QAction in class variables, for comparison purposes.
Here is the implementation of setBrushColor():
void
MainWindow::
setBrushColor()
{
if
(!
m_colorDialog) {
m_colorDialog =
new
QColorDialog(this
);
m_colorDialog-&
gt;setModal(false
);
m_colorDialog-&
gt;setCurrentColor(m_canvas-&
gt;color());
connect(m_colorDialog, &
amp;QColorDialog::
colorSelected, m_canvas, &
amp;TabletCanvas::
setColor);
}
m_colorDialog-&
gt;setVisible(true
);
}
We do lazy initialization of a QColorDialog the first time the user chooses Brush color... from the menu or via the action shortcut. While the dialog is open, each time the user chooses a different color, TabletCanvas::setColor() will be called to change the drawing color. Because it is a non-modal dialog, the user is free to leave the color dialog open, so as to be able to conveniently and frequently change colors, or close it and re-open it later.
Here is the implementation of save():
bool
MainWindow::
save()
{
QString path =
QDir::
currentPath() +
"/untitled.png"
;
QString fileName =
QFileDialog::
getSaveFileName(this
, tr("Save Picture"
),
path);
bool
success =
m_canvas-&
gt;saveImage(fileName);
if
(!
success)
QMessageBox::
information(this
, "Error Saving Picture"
,
"Could not save the image"
);
return
success;
}
We use the QFileDialog to let the user select a file to save the drawing, and then call TabletCanvas::saveImage() to actually write it to the file.
Here is the implementation of load():
void
MainWindow::
load()
{
QString fileName =
QFileDialog::
getOpenFileName(this
, tr("Open Picture"
),
QDir::
currentPath());
if
(!
m_canvas-&
gt;loadImage(fileName))
QMessageBox::
information(this
, "Error Opening Picture"
,
"Could not open picture"
);
}
We let the user select the image file to be opened with a QFileDialog; we then ask the canvas to load the image with loadImage().
Here is the implementation of about():
void
MainWindow::
about()
{
QMessageBox::
about(this
, tr("About Tablet Example"
),
tr("This example shows how to use a graphics drawing tablet in Qt."
));
}
We show a message box with a short description of the example.
TabletCanvas Class Definition▲
The TabletCanvas class provides a surface on which the user can draw with a tablet.
class
TabletCanvas : public
QWidget
{
Q_OBJECT
public
:
enum
Valuator {
PressureValuator, TangentialPressureValuator,
TiltValuator, VTiltValuator, HTiltValuator, NoValuator }
;
Q_ENUM(Valuator)
TabletCanvas();
bool
saveImage(const
QString &
amp;file);
bool
loadImage(const
QString &
amp;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 &
amp;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 &
amp;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;
}
;
The canvas can change the alpha channel, color saturation, and line width of the stroke. We have an enum listing the QTabletEvent properties with which it is possible to modulate them. We keep a private variable for each: m_alphaChannelValuator, m_colorSaturationValuator and m_lineWidthValuator, and we provide accessor functions for them.
We draw on a QPixmap with m_pen and m_brush using m_color. Each time a QTabletEvent is received, the stroke is drawn from lastPoint to the point given in the current QTabletEvent, and then the position and rotation are saved in lastPoint for next time. The saveImage() and loadImage() functions save and load the QPixmap to disk. The pixmap is drawn on the widget in paintEvent().
The interpretation of events from the tablet is done in tabletEvent(), and paintPixmap(), updateBrush(), and updateCursor() are helper functions used by tabletEvent().
TabletCanvas Class Implementation▲
We start with a look at the constructor:
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);
}
In the constructor we initialize most of our class variables.
Here is the implementation of saveImage():
bool
TabletCanvas::
saveImage(const
QString &
amp;file)
{
return
m_pixmap.save(file);
}
QPixmap implements functionality to save itself to disk, so we simply call save().
Here is the implementation of loadImage():
bool
TabletCanvas::
loadImage(const
QString &
amp;file)
{
bool
success =
m_pixmap.load(file);
if
(success) {
update();
return
true
;
}
return
false
;
}
We simply call load(), which loads the image from file.
Here is the implementation of tabletEvent():
void
TabletCanvas::
tabletEvent(QTabletEvent *
event)
{
switch
(event-&
gt;type()) {
case
QEvent::
TabletPress:
if
(!
m_deviceDown) {
m_deviceDown =
true
;
lastPoint.pos =
event-&
gt;position();
lastPoint.pressure =
event-&
gt;pressure();
lastPoint.rotation =
event-&
gt;rotation();
}
break
;
case
QEvent::
TabletMove:
#ifndef Q_OS_IOS
if
(event-&
gt;pointingDevice() &
amp;&
amp; event-&
gt;pointingDevice()-&
gt;capabilities().testFlag(QPointingDevice::Capability::
Rotation))
updateCursor(event);
#endif
if
(m_deviceDown) {
updateBrush(event);
QPainter painter(&
amp;m_pixmap);
paintPixmap(painter, event);
lastPoint.pos =
event-&
gt;position();
lastPoint.pressure =
event-&
gt;pressure();
lastPoint.rotation =
event-&
gt;rotation();
}
break
;
case
QEvent::
TabletRelease:
if
(m_deviceDown &
amp;&
amp; event-&
gt;buttons() ==
Qt::
NoButton)
m_deviceDown =
false
;
update();
break
;
default
:
break
;
}
event-&
gt;accept();
}
We get three kind of events to this function: TabletPress, TabletRelease, and TabletMove, which are generated when a drawing tool is pressed down on, lifed up from, or moved across the tablet. We set m_deviceDown to true when a device is pressed down on the tablet; we then know that we should draw when we receive move events. We have implemented updateBrush() to update m_brush and m_pen depending on which of the tablet event properties the user has chosen to pay attention to. The updateCursor() function selects a cursor to represent the drawing tool in use, so that as you hover with the tool in proximity of the tablet, you can see what kind of stroke you are about to make.
void
TabletCanvas::
updateCursor(const
QTabletEvent *
event)
{
QCursor cursor;
if
(event-&
gt;type() !=
QEvent::
TabletLeaveProximity) {
if
(event-&
gt;pointerType() ==
QPointingDevice::PointerType::
Eraser) {
cursor =
QCursor(QPixmap(":/images/cursor-eraser.png"
), 3
, 28
);
}
else
{
switch
(event-&
gt;deviceType()) {
case
QInputDevice::DeviceType::
Stylus:
if
(event-&
gt;pointingDevice()-&
gt;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(&
amp;img);
QTransform transform =
painter.transform();
transform.translate(16
, 16
);
transform.rotate(event-&
gt;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);
}
If an art pen (RotationStylus) is in use, updateCursor() is also called for each TabletMove event, and renders a rotated cursor so that you can see the angle of the pen tip.
Here is the implementation of 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(&
amp;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-&
gt;rect().topLeft() *
devicePixelRatio(),
event-&
gt;rect().size() *
devicePixelRatio());
painter.drawPixmap(event-&
gt;rect().topLeft(), m_pixmap, pixmapPortion);
}
The first time Qt calls paintEvent(), m_pixmap is default-constructed, so QPixmap::isNull() returns true. Now that we know which screen we will be rendering to, we can create a pixmap with the appropriate resolution. The size of the pixmap with which we fill the window depends on the screen resolution, as the example does not support zoom; and it may be that one screen is high DPI while another is not. We need to draw the background too, as the default is gray.
After that, we simply draw the pixmap to the top left of the widget.
Here is the implementation of paintPixmap():
void
TabletCanvas::
paintPixmap(QPainter &
amp;painter, QTabletEvent *
event)
{
static
qreal maxPenRadius =
pressureToWidth(1.0
);
painter.setRenderHint(QPainter::
Antialiasing);
switch
(event-&
gt;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-&
gt;position(), radius, radius);
update(QRect(event-&
gt;position().toPoint() -
QPoint(radius, radius), QSize(radius *
2
, radius *
2
)));
}
break
;
case
QInputDevice::DeviceType::
Puck:
case
QInputDevice::DeviceType::
Mouse:
{
const
QString error(tr("This input device is not supported by the example."
));
#if QT_CONFIG(statustip)
QStatusTipEvent status(error);
QCoreApplication::
sendEvent(this
, &
amp;status);
#else
qWarning() &
lt;&
lt; error;
#endif
}
break
;
default
:
{
const
QString error(tr("Unknown tablet device - treating as stylus"
));
#if QT_CONFIG(statustip)
QStatusTipEvent status(error);
QCoreApplication::
sendEvent(this
, &
amp;status);
#else
qWarning() &
lt;&
lt; error;
#endif
}
Q_FALLTHROUGH();
case
QInputDevice::DeviceType::
Stylus:
if
(event-&
gt;pointingDevice()-&
gt;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 &
lt;&
lt; lastPoint.pos +
brushAdjust;
poly &
lt;&
lt; lastPoint.pos -
brushAdjust;
halfWidth =
m_pen.widthF();
brushAdjust =
QPointF(qSin(qDegreesToRadians(-
event-&
gt;rotation())) *
halfWidth,
qCos(qDegreesToRadians(-
event-&
gt;rotation())) *
halfWidth);
poly &
lt;&
lt; event-&
gt;position() -
brushAdjust;
poly &
lt;&
lt; event-&
gt;position() +
brushAdjust;
painter.drawConvexPolygon(poly);
update(poly.boundingRect().toRect());
}
else
{
painter.setPen(m_pen);
painter.drawLine(lastPoint.pos, event-&
gt;position());
update(QRect(lastPoint.pos.toPoint(), event-&
gt;position().toPoint()).normalized()
.adjusted(-
maxPenRadius, -
maxPenRadius, maxPenRadius, maxPenRadius));
}
break
;
}
}
In this function we draw on the pixmap based on the movement of the tool. If the tool used on the tablet is a stylus, we want to draw a line from the last-known position to the current position. We also assume that this is a reasonable handling of any unknown device, but update the status bar with a warning. If it is an airbrush, we want to draw a circle filled with a soft gradient, whose density can depend on various event parameters. By default it depends on the tangential pressure, which is the position of the finger wheel on the airbrush. If the tool is a rotation stylus, we simulate a felt marker by drawing trapezoidal stroke segments.
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-&
gt;position(), radius, radius);
update(QRect(event-&
gt;position().toPoint() -
QPoint(radius, radius), QSize(radius *
2
, radius *
2
)));
}
break
;
In updateBrush() we set the pen and brush used for drawing to match m_alphaChannelValuator, m_lineWidthValuator, m_colorSaturationValuator, and m_color. We will examine the code to set up m_brush and m_pen for each of these variables:
void
TabletCanvas::
updateBrush(const
QTabletEvent *
event)
{
int
hue, saturation, value, alpha;
m_color.getHsv(&
amp;hue, &
amp;saturation, &
amp;value, &
amp;alpha);
int
vValue =
int
(((event-&
gt;yTilt() +
60.0
) /
120.0
) *
255
);
int
hValue =
int
(((event-&
gt;xTilt() +
60.0
) /
120.0
) *
255
);
We fetch the current drawingcolor's hue, saturation, value, and alpha values. hValue and vValue are set to the horizontal and vertical tilt as a number from 0 to 255. The original values are in degrees from -60 to 60, i.e., 0 equals -60, 127 equals 0, and 255 equals 60 degrees. The angle measured is between the device and the perpendicular of the tablet (see QTabletEvent for an illustration).
switch
(m_alphaChannelValuator) {
case
PressureValuator:
m_color.setAlphaF(event-&
gt;pressure());
break
;
case
TangentialPressureValuator:
if
(event-&
gt;deviceType() ==
QInputDevice::DeviceType::
Airbrush)
m_color.setAlphaF(qMax(0.01
, (event-&
gt;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
);
}
The alpha channel of QColor is given as a number between 0 and 255 where 0 is transparent and 255 is opaque, or as a floating-point number where 0 is transparent and 1.0 is opaque. pressure() returns the pressure as a qreal between 0.0 and 1.0. We get the smallest alpha values (i.e., the color is most transparent) when the pen is perpendicular to the tablet. We select the largest of the vertical and horizontal tilt values.
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-&
gt;pressure() *
255.0
), value, alpha);
break
;
default
:
;
}
The color saturation in the HSV color model can be given as an integer between 0 and 255 or as a floating-point value between 0 and 1. We chose to represent alpha as an integer, so we call setHsv() with integer values. That means we need to multiply the pressure to a number between 0 and 255.
switch
(m_lineWidthValuator) {
case
PressureValuator:
m_pen.setWidthF(pressureToWidth(event-&
gt;pressure()));
break
;
case
TiltValuator:
m_pen.setWidthF(std::
max(std::
abs(vValue -
127
),
std::
abs(hValue -
127
)) /
12
);
break
;
default
:
m_pen.setWidthF(1
);
}
The width of the pen stroke can increase with pressure, if so chosen. But when the pen width is controlled by tilt, we let the width increase with the angle between the tool and the perpendicular of the tablet.
if
(event-&
gt;pointerType() ==
QPointingDevice::PointerType::
Eraser) {
m_brush.setColor(Qt::
white);
m_pen.setColor(Qt::
white);
m_pen.setWidthF(event-&
gt;pressure() *
10
+
1
);
}
else
{
m_brush.setColor(m_color);
m_pen.setColor(m_color);
}
}
We finally check whether the pointer is the stylus or the eraser. If it is the eraser, we set the color to the background color of the pixmap and let the pressure decide the pen width, else we set the colors we have decided previously in the function.
TabletApplication Class Definition▲
We inherit QApplication in this class because we want to reimplement the event() function.
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 exists as a subclass of QApplication in order to receive tablet proximity events and forward them to TabletCanvas. The TabletEnterProximity and TabletLeaveProximity events are sent to the QApplication object, while other tablet events are sent to the QWidget's event() handler, which sends them on to tabletEvent().
TabletApplication Class Implementation▲
Here is the implementation of event():
bool
TabletApplication::
event(QEvent *
event)
{
if
(event-&
gt;type() ==
QEvent::
TabletEnterProximity ||
event-&
gt;type() ==
QEvent::
TabletLeaveProximity) {
m_canvas-&
gt;setTabletDevice(static_cast
&
lt;QTabletEvent *&
gt;(event));
return
true
;
}
return
QApplication::
event(event);
}
We use this function to handle the TabletEnterProximity and TabletLeaveProximity events, which are generated when a drawing tool enters or leaves the proximity of the tablet. Here we call TabletCanvas::setTabletDevice(), which then calls updateCursor(), which will set an appropriate cursor. This is the only reason we need the proximity events; for the purpose of correct drawing, it is enough for TabletCanvas to observe the device() and pointerType() in each event that it receives.
The main() function▲
Here is the example's main() function:
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();
}
Here we create a MainWindow and display it as a top level window. We use the TabletApplication class. We need to set the canvas after the application is created. We cannot use classes that implement event handling before an QApplication object is instantiated.