The canvas can change the alpha channel, color saturation, and line width of the drawing. We have one enum for each of these; their values decide if it is the tablet pressure or tilt that will alter them. We keep a private variable for each, the alphaChannelType, colorSturationType, and penWidthType, which we provide access functions for.
TabletCanvas Class Implementation
We start with a look at the constructor:
TabletCanvas::TabletCanvas()
{
resize(500, 500);
myBrush = QBrush();
myPen = QPen();
initPixmap();
setAutoFillBackground(true);
deviceDown = false;
myColor = Qt::red;
myTabletDevice = QTabletEvent::Stylus;
alphaChannelType = NoAlpha;
colorSaturationType = NoSaturation;
lineWidthType = LineWidthPressure;
}
void TabletCanvas::initPixmap()
{
QPixmap newPixmap = QPixmap(width(), height());
newPixmap.fill(Qt::white);
QPainter painter(&newPixmap);
if (!pixmap.isNull())
painter.drawPixmap(0, 0, pixmap);
painter.end();
pixmap = newPixmap;
}
In the constructor we initialize our class variables. We need to draw the background of our pixmap, as the default is gray.
Here is the implementation of saveImage():
bool TabletCanvas::saveImage(const QString &file)
{
return 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 &file)
{
bool success = pixmap.load(file);
if (success) {
update();
return true;
}
return false;
}
We simply call load(), which loads the image in file.
Here is the implementation of tabletEvent():
void TabletCanvas::tabletEvent(QTabletEvent *event)
{
switch (event->type()) {
case QEvent::TabletPress:
if (!deviceDown) {
deviceDown = true;
polyLine[0] = polyLine[1] = polyLine[2] = event->pos();
}
break;
case QEvent::TabletRelease:
if (deviceDown)
deviceDown = false;
break;
case QEvent::TabletMove:
polyLine[2] = polyLine[1];
polyLine[1] = polyLine[0];
polyLine[0] = event->pos();
if (deviceDown) {
updateBrush(event);
QPainter painter(&pixmap);
paintPixmap(painter, event);
}
break;
default:
break;
}
update();
}
We get three kind of events to this function: TabletPress, TabletRelease, and TabletMove, which is generated when a device is pressed down on, leaves, or moves on the tablet. We set the deviceDown to true when a device is pressed down on the tablet; we then know when we should draw when we receive move events. We have implemented the updateBrush() and paintPixmap() helper functions to update myBrush and myPen after the state of alphaChannelType, colorSaturationType, and lineWidthType.
Here is the implementation of paintEvent():
void TabletCanvas::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.drawPixmap(0, 0, pixmap);
}
We simply draw the pixmap to the top left of the widget.
Here is the implementation of paintPixmap():
void TabletCanvas::paintPixmap(QPainter &painter, QTabletEvent *event)
{
QPoint brushAdjust(10, 10);
switch (myTabletDevice) {
case QTabletEvent::Airbrush:
myBrush.setColor(myColor);
myBrush.setStyle(brushPattern(event->pressure()));
painter.setPen(Qt::NoPen);
painter.setBrush(myBrush);
for (int i = 0; i < 3; ++i) {
painter.drawEllipse(QRect(polyLine[i] - brushAdjust,
polyLine[i] + brushAdjust));
}
break;
case QTabletEvent::Puck:
case QTabletEvent::FourDMouse:
case QTabletEvent::RotationStylus:
{
const QString error(tr("This input device is not supported by the example."));
#ifndef QT_NO_STATUSTIP
QStatusTipEvent status(error);
QApplication::sendEvent(this, &status);
#else
qWarning() << error;
#endif
}
break;
default:
{
const QString error(tr("Unknown tablet device - treating as stylus"));
#ifndef QT_NO_STATUSTIP
QStatusTipEvent status(error);
QApplication::sendEvent(this, &status);
#else
qWarning() << error;
#endif
}
case QTabletEvent::Stylus:
painter.setBrush(myBrush);
painter.setPen(myPen);
painter.drawLine(polyLine[1], event->pos());
break;
}
}
In this function we draw on the pixmap based on the movement of the device. If the device used on the tablet is a stylus we want to draw a line between the positions of the stylus recorded in polyLine. We also assume that this is a reasonable handling of any unknown device, but update the statusbar with a warning so that the user can see that for his tablet he might have to implement special handling. If it is an airbrush we want to draw a circle of points with a point density based on the tangential pressure, which is the position of the finger wheel on the airbrush. We use the Qt::BrushStyle to draw the points as it has styles that draw points with different density; we select the style based on the tangential pressure in brushPattern().
Qt::BrushStyle TabletCanvas::brushPattern(qreal value)
{
int pattern = int((value) * 100.0) % 7;
switch (pattern) {
case 0:
return Qt::SolidPattern;
case 1:
return Qt::Dense1Pattern;
case 2:
return Qt::Dense2Pattern;
case 3:
return Qt::Dense3Pattern;
case 4:
return Qt::Dense4Pattern;
case 5:
return Qt::Dense5Pattern;
case 6:
return Qt::Dense6Pattern;
default:
return Qt::Dense7Pattern;
}
}
We return a brush style with a point density that increases with the tangential pressure.
In updateBrush() we set the pen and brush used for drawing to match alphaChannelType, lineWidthType, colorSaturationType, and myColor. We will examine the code to set up myBrush and myPen for each of these variables:
void TabletCanvas::updateBrush(QTabletEvent *event)
{
int hue, saturation, value, alpha;
myColor.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);
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 (alphaChannelType) {
case AlphaPressure:
myColor.setAlpha(int(event->pressure() * 255.0));
break;
case AlphaTilt:
myColor.setAlpha(maximum(abs(vValue - 127), abs(hValue - 127)));
break;
default:
myColor.setAlpha(255);
}
The alpha channel of QColor is given as a number between 0 and 255 where 0 is transparent and 255 is opaque. pressure() returns the pressure as a qreal between 0.0 and 1.0. By subtracting 127 from the tilt values and taking the absolute value 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 value.
switch (colorSaturationType) {
case SaturationVTilt:
myColor.setHsv(hue, vValue, value, alpha);
break;
case SaturationHTilt:
myColor.setHsv(hue, hValue, value, alpha);
break;
case SaturationPressure:
myColor.setHsv(hue, int(event->pressure() * 255.0), value, alpha);
break;
default:
;
}
The colorsaturation is given as a number between 0 and 255. It is set with setHsv(). We can set the tilt values directly, but must multiply the pressure to a number between 0 and 255.
switch (lineWidthType) {
case LineWidthPressure:
myPen.setWidthF(event->pressure() * 10 + 1);
break;
case LineWidthTilt:
myPen.setWidthF(maximum(abs(vValue - 127), abs(hValue - 127)) / 12);
break;
default:
myPen.setWidthF(1);
}
The width of the pen increases with the pressure. When the pen width is controlled with the tilt we let the width increse with the angle between the device and the perpendicular of the tablet.
if (event->pointerType() == QTabletEvent::Eraser) {
myBrush.setColor(Qt::white);
myPen.setColor(Qt::white);
myPen.setWidthF(event->pressure() * 10 + 1);
} else {
myBrush.setColor(myColor);
myPen.setColor(myColor);
}
}
We finally check wether the pointer is the stylus or the eraser. If it is the eraser, we set the color to the background color of the pixmap an let the pressure decide the pen width, else we set the colors we have set up 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:
TabletApplication(int &argv, char **args)
: QApplication(argv, args) {}
bool event(QEvent *event);
void setCanvas(TabletCanvas *canvas)
{ myCanvas = canvas; }
private:
TabletCanvas *myCanvas;
};
We keep a TabletCanvas we send the device type of the events we handle in the event() function to. The TabletEnterProximity and TabletLeaveProximity events are not sendt to the QApplication object, while other tablet events are sendt to the QWidget's event(), which sends them on to tabletEvent(). Since we want to handle these events we have implemented TabletApplication.
TabletApplication Class Implementation
Here is the implementation of event():
bool TabletApplication::event(QEvent *event)
{
if (event->type() == QEvent::TabletEnterProximity ||
event->type() == QEvent::TabletLeaveProximity) {
myCanvas->setTabletDevice(
static_cast<QTabletEvent *>(event)->device());
return true;
}
return QApplication::event(event);
}
We use this function to handle the TabletEnterProximity and TabletLeaveProximity events, which is generated when a device enters and leaves the proximity of the tablet. The intended use of these events is to do work that is dependent on what kind of device is used on the tablet. This way, you don't have to do this work when other events are generated, which is more frequently than the leave and enter proximity events. We call setTabletDevice() in TabletCanvas.
The main() function
Here is the examples 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();
}
In the main() function we create a MainWinow 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.