当使用数位板采用 Qt 应用程序时, QTabletEvent 被生成。需要重实现 tabletEvent() event handler if you want to handle tablet events. Events are generated when the device used for drawing enters and leaves the proximity of the tablet (i.e., when it is close but not pressed down on it), when a device is pushed down and released from it, and when a device is moved on the tablet.
The information available in QTabletEvent depends on the device used. The tablet in this example has two different devices for drawing: a stylus and an airbrush. For both devices the event contains the position of the device, pressure on the tablet, vertical tilt, and horizontal tilt (i.e, the angle between the device and the perpendicular of the tablet). The airbrush has a finger wheel; the position of this is also available in the tablet event.
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 paint; the finger wheel is used to change the density of the spray. The pressure and tilt can change the alpha and saturation values of the QColor and the width of the QPen used for drawing.
范例由以下组成:
MainWindow
类继承
QMainWindow
and creates the examples menus and connect their slots and signals.
TabletCanvas
类继承
QWidget
and receives tablet events. It uses the events to paint on a offscreen pixmap, which it draws onto itself.
TabletApplication
类继承
QApplication
. This class handles tablet events that are not sent to
tabletEvent()
. We will look at this later.
main()
函数创建
MainWindow
并将它展示作为顶层窗口。
The
MainWindow
创建
TabletCanvas
并将它设为其中心 Widget。
class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(TabletCanvas *canvas); private slots: void brushColorAct(); void alphaActionTriggered(QAction *action); void lineWidthActionTriggered(QAction *action); void saturationActionTriggered(QAction *action); void saveAct(); void loadAct(); void aboutAct(); private: void createActions(); void createMenus(); TabletCanvas *myCanvas; QAction *brushColorAction; QActionGroup *brushActionGroup; QActionGroup *alphaChannelGroup; QAction *alphaChannelPressureAction; QAction *alphaChannelTiltAction; QAction *noAlphaChannelAction; QActionGroup *colorSaturationGroup; QAction *colorSaturationVTiltAction; QAction *colorSaturationHTiltAction; QAction *colorSaturationPressureAction; QAction *noColorSaturationAction; QActionGroup *lineWidthGroup; QAction *lineWidthPressureAction; QAction *lineWidthTiltAction; QAction *lineWidthFixedAction; QAction *exitAction; QAction *saveAction; QAction *loadAction; QAction *aboutAction; QAction *aboutQtAction; QMenu *fileMenu; QMenu *brushMenu; QMenu *tabletMenu; QMenu *helpMenu; QMenu *colorSaturationMenu; QMenu *lineWidthMenu; QMenu *alphaChannelMenu; };
The QActions let the user select if the tablets pressure and tilt should change the pen width, color alpha component and color saturation.
createActions()
creates all actions, and
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
alphaActionTriggered()
,
colorSaturationActiontriggered()
,和
lineWidthActionTriggered()
slots, which calls functions in
myCanvas
.
We start width a look at the constructor
MainWindow()
:
MainWindow::MainWindow(TabletCanvas *canvas) { myCanvas = canvas; createActions(); createMenus(); myCanvas->setColor(Qt::red); myCanvas->setLineWidthType(TabletCanvas::LineWidthPressure); myCanvas->setAlphaChannelType(TabletCanvas::NoAlpha); myCanvas->setColorSaturationType(TabletCanvas::NoSaturation); setWindowTitle(tr("Tablet Example")); setCentralWidget(myCanvas); }
In the constructor we create the canvas, actions, and menus. We set the canvas as the center widget. We also initialize the canvas to match the state of our menus and start drawing with a red color.
这里是实现为
brushColorAct()
:
void MainWindow::brushColorAct() { QColor color = QColorDialog::getColor(myCanvas->color()); if (color.isValid()) myCanvas->setColor(color); }
We let the user pick a color with a
QColorDialog
. If it is valid, we set a new drawing color with
setColor()
.
这里是实现为
alphaActionTriggered()
:
void MainWindow::alphaActionTriggered(QAction *action) { if (action == alphaChannelPressureAction) { myCanvas->setAlphaChannelType(TabletCanvas::AlphaPressure); } else if (action == alphaChannelTiltAction) { myCanvas->setAlphaChannelType(TabletCanvas::AlphaTilt); } else { myCanvas->setAlphaChannelType(TabletCanvas::NoAlpha); } }
The
TabletCanvas
class supports two ways by which the alpha channel of the drawing color can be changed: tablet pressure and tilt. We have one action for each and an action if the alpha channel should not be changed.
这里是实现为
lineWidthActionTriggered()
:
void MainWindow::lineWidthActionTriggered(QAction *action) { if (action == lineWidthPressureAction) { myCanvas->setLineWidthType(TabletCanvas::LineWidthPressure); } else if (action == lineWidthTiltAction) { myCanvas->setLineWidthType(TabletCanvas::LineWidthTilt); } else { myCanvas->setLineWidthType(TabletCanvas::NoLineWidth); } }
We check which action is selected in
lineWidthGroup
, and set how the canvas should change the drawing line width.
这里是实现为
saturationActionTriggered()
:
void MainWindow::saturationActionTriggered(QAction *action) { if (action == colorSaturationVTiltAction) { myCanvas->setColorSaturationType(TabletCanvas::SaturationVTilt); } else if (action == colorSaturationHTiltAction) { myCanvas->setColorSaturationType(TabletCanvas::SaturationHTilt); } else if (action == colorSaturationPressureAction) { myCanvas->setColorSaturationType(TabletCanvas::SaturationPressure); } else { myCanvas->setColorSaturationType(TabletCanvas::NoSaturation); } }
We check which action is selected in
colorSaturationGroup
, and set how the canvas should change the color saturation of the drawing color.
这里是实现为
saveAct()
:
void MainWindow::saveAct() { QString path = QDir::currentPath() + "/untitled.png"; QString fileName = QFileDialog::getSaveFileName(this, tr("Save Picture"), path); if (!myCanvas->saveImage(fileName)) QMessageBox::information(this, "Error Saving Picture", "Could not save the image"); }
使用
QFileDialog
to let the user select a file to save the drawing in. It is the
TabletCanvas
that save the drawing, so we call its
saveImage()
函数。
这里是实现为
loadAct()
:
void MainWindow::loadAct() { QString fileName = QFileDialog::getOpenFileName(this, tr("Open Picture"), QDir::currentPath()); if (!myCanvas->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()
.
这里是实现为
aboutAct()
:
void MainWindow::aboutAct() { QMessageBox::about(this, tr("About Tablet Example"), tr("This example shows use of a Wacom tablet in Qt")); }
We show a message box with a short description of the example.
createActions()
creates all actions and action groups of the example. We look at the creation of one action group and its actions. See the
application example
if you want a high-level introduction to QActions.
这里是实现为
createActions
:
void MainWindow::createActions() { ... alphaChannelPressureAction = new QAction(tr("&Pressure"), this); alphaChannelPressureAction->setCheckable(true); alphaChannelTiltAction = new QAction(tr("&Tilt"), this); alphaChannelTiltAction->setCheckable(true); noAlphaChannelAction = new QAction(tr("No Alpha Channel"), this); noAlphaChannelAction->setCheckable(true); noAlphaChannelAction->setChecked(true); alphaChannelGroup = new QActionGroup(this); alphaChannelGroup->addAction(alphaChannelPressureAction); alphaChannelGroup->addAction(alphaChannelTiltAction); alphaChannelGroup->addAction(noAlphaChannelAction); connect(alphaChannelGroup, SIGNAL(triggered(QAction*)), this, SLOT(alphaActionTriggered(QAction*)));
We want the user to be able to choose if the drawing color's alpha component should be changed by the tablet pressure or tilt. We have one action for each choice and an action if the alpha channel is not to be changed, i.e, the color is 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 when an action is checked.
...
}
这里是实现为
createMenus()
:
void MainWindow::createMenus() { fileMenu = menuBar()->addMenu(tr("&File")); fileMenu->addAction(loadAction); fileMenu->addAction(saveAction); fileMenu->addSeparator(); fileMenu->addAction(exitAction); brushMenu = menuBar()->addMenu(tr("&Brush")); brushMenu->addAction(brushColorAction); tabletMenu = menuBar()->addMenu(tr("&Tablet")); lineWidthMenu = tabletMenu->addMenu(tr("&Line Width")); lineWidthMenu->addAction(lineWidthPressureAction); lineWidthMenu->addAction(lineWidthTiltAction); lineWidthMenu->addAction(lineWidthFixedAction); alphaChannelMenu = tabletMenu->addMenu(tr("&Alpha Channel")); alphaChannelMenu->addAction(alphaChannelPressureAction); alphaChannelMenu->addAction(alphaChannelTiltAction); alphaChannelMenu->addAction(noAlphaChannelAction); colorSaturationMenu = tabletMenu->addMenu(tr("&Color Saturation")); colorSaturationMenu->addAction(colorSaturationVTiltAction); colorSaturationMenu->addAction(colorSaturationHTiltAction); colorSaturationMenu->addAction(noColorSaturationAction); helpMenu = menuBar()->addMenu("&Help"); helpMenu->addAction(aboutAction); helpMenu->addAction(aboutQtAction); }
We create the menus of the example and add the actions to them.
The
TabletCanvas
class provides a surface on which the user can draw with a tablet.
class TabletCanvas : public QWidget { Q_OBJECT public: enum AlphaChannelType { AlphaPressure, AlphaTilt, NoAlpha }; enum ColorSaturationType { SaturationVTilt, SaturationHTilt, SaturationPressure, NoSaturation }; enum LineWidthType { LineWidthPressure, LineWidthTilt, NoLineWidth }; TabletCanvas(); bool saveImage(const QString &file); bool loadImage(const QString &file); void setAlphaChannelType(AlphaChannelType type) { alphaChannelType = type; } void setColorSaturationType(ColorSaturationType type) { colorSaturationType = type; } void setLineWidthType(LineWidthType type) { lineWidthType = type; } void setColor(const QColor &color) { myColor = color; } QColor color() const { return myColor; } void setTabletDevice(QTabletEvent::TabletDevice device) { myTabletDevice = device; } int maximum(int a, int b) { return a > b ? a : b; } protected: void tabletEvent(QTabletEvent *event); void paintEvent(QPaintEvent *event); void resizeEvent(QResizeEvent *event); private: void initPixmap(); void paintPixmap(QPainter &painter, QTabletEvent *event); Qt::BrushStyle brushPattern(qreal value); void updateBrush(QTabletEvent *event); AlphaChannelType alphaChannelType; ColorSaturationType colorSaturationType; LineWidthType lineWidthType; QTabletEvent::PointerType pointerType; QTabletEvent::TabletDevice myTabletDevice; QColor myColor; QPixmap pixmap; QBrush myBrush; QPen myPen; bool deviceDown; QPoint polyLine[3]; };
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
,和
penWidthType
, which we provide access functions for.
We draw on a
QPixmap
with
myPen
and
myBrush
使用
myColor
。
saveImage()
and
loadImage()
saves and loads the
QPixmap
to disk. The pixmap is drawn on the widget in
paintEvent()
。
pointerType
and
deviceType
keeps the type of pointer, which is either a pen or an eraser, and device currently used on the tablet, which is either a stylus or an airbrush.
The interpretation of events from the tablet is done in
tabletEvent()
;
paintPixmap()
,
updateBrush()
,和
brushPattern()
are helper functions used by
tabletEvent()
.
从查看构造函数开始:
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.
这里是实现为
saveImage()
:
bool TabletCanvas::saveImage(const QString &file) { return pixmap.save(file); }
QPixmap implements functionality to save itself to disk, so we simply call save() .
这里是实现为
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 .
这里是实现为
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
,和
lineWidthType
.
这里是实现为
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.
这里是实现为
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 } // FALL-THROUGH 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.
在
updateBrush()
we set the pen and brush used for drawing to match
alphaChannelType
,
lineWidthType
,
colorSaturationType
,和
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.
继承 QApplication in this class because we want to reimplement the event() 函数。
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
.
这里是实现为
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
.
main()
function
Here is the examples
main()
函数:
int main(int argv, char *args[]) { TabletApplication app(argv, args); TabletCanvas *canvas = new TabletCanvas; app.setCanvas(canvas); MainWindow mainWindow(canvas); #if defined(Q_OS_SYMBIAN) mainWindow.showMaximized(); #elif defined(Q_WS_MAEMO_5) || defined(Q_WS_SIMULATOR) mainWindow.show(); #else mainWindow.resize(500, 500); mainWindow.show(); #endif return app.exec(); }
在
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
对象被实例化。
文件: