Home · Examples 


Tablet Example

Code:

This example shows how to use a Wacom tablet in Qt applications.

When you use a tablet with Qt applications, QTabletEvents are genarated. You need to reimplement the 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.

The example consists of the following:

Tablet Class

The Tablet class creates a TabletCanvas and sets it as its center widget.
public class Tablet extends QMainWindow
{
    public static TabletCanvas myCanvas;

    private QAction brushColorAction;

    private QActionGroup alphaChannelGroup;
    private QAction alphaChannelPressureAction;
    private QAction alphaChannelTiltAction;
    private QAction noAlphaChannelAction;

    private QActionGroup colorSaturationGroup;
    private QAction colorSaturationVTiltAction;
    private QAction colorSaturationHTiltAction;
    private QAction colorSaturationPressureAction;
    private QAction noColorSaturationAction;

    private QActionGroup lineWidthGroup;
    private QAction lineWidthPressureAction;
    private QAction lineWidthTiltAction;
    private QAction lineWidthFixedAction;

    private QAction exitAction;
    private QAction saveAction;
    private QAction loadAction;
The QActions let the user select if the tablets pressure and tilt should change the pen width, color alpha component and color saturation. The method 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(), and lineWidthActionTriggered() slots, which calls methodss in myCanvas.

We start with a look at the constructor Tablet():

    public Tablet()
    {
        myCanvas = new TabletCanvas();

        createActions();
        createMenus();

        myCanvas.setColor(new QColor(Qt.GlobalColor.red));
        myCanvas.setLineWidthType(LineWidthType.LineWidthPressure);
        myCanvas.setAlphaChannelType(AlphaChannelType.NoAlpha);
        myCanvas.setColorSaturationType(ColorSaturationType.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.

Here is the implementation of brushColorAct():

    public void 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().

Here is the implementation of alphaActionTriggered():

    public void alphaActionTriggered(QAction action)
    {
        if (action.equals(alphaChannelPressureAction)) {
            myCanvas.setAlphaChannelType(AlphaChannelType.AlphaPressure);
        } else if (action.equals(alphaChannelTiltAction)) {
            myCanvas.setAlphaChannelType(AlphaChannelType.AlphaTilt);
        } else {
            myCanvas.setAlphaChannelType(AlphaChannelType.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.

Here is the implementation of lineWidthActionTriggered():

    public void lineWidthActionTriggered(QAction action)
    {
        if (action.equals(lineWidthPressureAction)) {
            myCanvas.setLineWidthType(LineWidthType.LineWidthPressure);
        } else if (action.equals(lineWidthTiltAction)) {
            myCanvas.setLineWidthType(LineWidthType.LineWidthTilt);
        } else {
            myCanvas.setLineWidthType(LineWidthType.NoLineWidth);
        }
    }
We check which action is selected in lineWidthGroup, and set how the canvas should change the drawing line width.

Here is the implementation of saturationActionTriggered():

    public void saturationActionTriggered(QAction action)
    {
        if (action.equals(colorSaturationVTiltAction)) {
            myCanvas.setColorSaturationType(ColorSaturationType.SaturationVTilt);
        } else if (action.equals(colorSaturationHTiltAction)) {
            myCanvas.setColorSaturationType(ColorSaturationType.SaturationHTilt);
        } else if (action.equals(colorSaturationPressureAction)) {
            myCanvas.setColorSaturationType(ColorSaturationType.SaturationPressure);
        } else {
            myCanvas.setColorSaturationType(ColorSaturationType.NoSaturation);
        }
    }
We check which action is selected in colorSaturationGroup, and set how the canvas should change the color saturation of the drawing color.

Here is the implementation of saveAct():

    public void saveAct()
    {
        String path = QDir.currentPath() + tr("/untitled.png");
        String fileName = QFileDialog.getSaveFileName(this, tr("Save Picture"),
                                 path);

        if (!myCanvas.saveImage(fileName))
            QMessageBox.information(this, tr("Error Saving Picture"),
                                          tr("Could not save the image"));
    }
We use the 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() method.

Here is the implementation of loadAct():

    public void loadAct()
    {
        String fileName = QFileDialog.getOpenFileName(this, tr("Open Picture"),
                                                            QDir.currentPath());

        if (!myCanvas.loadImage(fileName))
            QMessageBox.information(this, tr("Error Opening Picture"),
                                          tr("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 aboutAct():

    public void aboutAct()
    {
        QMessageBox.about(this, tr("About Tablet Example"),
                       tr("This example shows use of a Wacom tablet in Jambi 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.

Here is the implementation of createActions:

    private void 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);
        alphaChannelGroup.triggered.connect(this, "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....
    }
Here is the implementation of createMenus():
    private void 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.

TabletCanvas Class

The TabletCanvas class provides a surface on which the user can draw with a tablet.
    class TabletCanvas extends QWidget
    {
        private AlphaChannelType alphaChannelType;
        private ColorSaturationType colorSaturationType;
        private LineWidthType lineWidthType;
        private QTabletEvent.TabletDevice myTabletDevice;
        private QColor myColor;

        private QImage image;
        private QBrush myBrush;
        private QPen myPen;
        private boolean deviceDown;
        private QPoint polyLine[] = new QPoint[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, and penWidthType, which we provide access methods for.

We draw on a QImage with myPen and myBrush using myColor. The saveImage() and loadImage() saves and loads the QImage to disk. The image is drawn on the widget in paintEvent(). The 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(); paintImage(), updateBrush(), and brushPattern() are helper methods used by tabletEvent().

We start with a look at the constructor:

        public TabletCanvas()
        {
            myBrush = new QBrush();
            myPen = new QPen();
            image = new QImage(500, 500, QImage.Format.Format_ARGB32);
            QPainter painter = new QPainter(image);
            painter.fillRect(0, 0, 499, 499, new QBrush(new QColor(Qt.GlobalColor.white)));
            setAutoFillBackground(true);
            deviceDown = false;
            myColor = new QColor(Qt.GlobalColor.red);
            myTabletDevice = QTabletEvent.TabletDevice.Stylus;
            alphaChannelType = AlphaChannelType.NoAlpha;
            colorSaturationType = ColorSaturationType.NoSaturation;
            lineWidthType = LineWidthType.LineWidthPressure;
        }
In the constructor we initialize our class variables. We need to draw the background of our image, as the default is gray.

Here is the implementation of saveImage():

        boolean saveImage(String file)
        {
            return image.save(file);
        }
QImage implements functionality to save itself to disk, so we simply call save().

Here is the implementation of loadImage():

        boolean loadImage(String file)
        {
            boolean success = image.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():

        protected void tabletEvent(QTabletEvent event)
        {
            switch (event.type()) {
                case TabletPress:
                    if (deviceDown)
                        deviceDown = true;
                    break;
                case TabletRelease:
                    if (!deviceDown)
                        deviceDown = false;
                    break;
                case TabletMove:
                    polyLine[2] = polyLine[1];
                    polyLine[1] = polyLine[0];
                    polyLine[0] = event.pos();

                    if (deviceDown) {
                        updateBrush(event);
                        QPainter painter = new QPainter(image);
                        paintImage(painter, event);
                    }
                    break;
                default:
                    break;
            }
            update();
        }
We get three kind of events to this method: 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 paintImage() helper methods to update myBrush and myPen after the state of alphaChannelType, colorSaturationType, and lineWidthType.

Here is the implementation of paintEvent():

        protected void paintEvent(QPaintEvent event)
        {
            QPainter painter = new QPainter(this);
            painter.drawImage(new QPoint(0, 0), image);
        }
We simply draw the image to the top left of the widget.

Here is the implementation of paintImage():

        private void paintImage(QPainter painter, QTabletEvent event)
        {
            QPoint brushAdjust = new QPoint(10, 10);

            switch (myTabletDevice) {
                case Stylus:
                    painter.setBrush(myBrush);
                    painter.setPen(myPen);
                    painter.drawLine(polyLine[1], event.pos());
                    break;
                case Airbrush:
                    myBrush.setColor(myColor);
                    myBrush.setStyle(brushPattern(event.pressure()));
                    painter.setPen(Qt.PenStyle.NoPen);
                    painter.setBrush(myBrush);

                    for (int i = 0; i < 3; ++i) {
                        painter.drawEllipse(new QRect(polyLine[i].subtract(brushAdjust),
                                                      polyLine[i].add(brushAdjust)));
                    }
                    break;
                default:
                    System.err.println("Unsupported tablet device.");
            }
        }
In this method we draw on the image 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. 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().
        private Qt.BrushStyle brushPattern(double value)
        {
            int pattern = ((int) (value * 100.0)) % 7;

            switch (pattern) {
                case 0:
                    return Qt.BrushStyle.SolidPattern;
                case 1:
                    return Qt.BrushStyle.Dense1Pattern;
                case 2:
                    return Qt.BrushStyle.Dense2Pattern;
                case 3:
                    return Qt.BrushStyle.Dense3Pattern;
               case 4:
                    return Qt.BrushStyle.Dense4Pattern;
                case 5:
                    return Qt.BrushStyle.Dense5Pattern;
                case 6:
                    return Qt.BrushStyle.Dense6Pattern;
                default:
                    return Qt.BrushStyle.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:

        private void updateBrush(QTabletEvent event)
        {
            int hue, value, alpha;
            hue = myColor.hue();
            value = myColor.value();
            alpha = myColor.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(Math.max(Math.abs(vValue - 127), Math.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(Math.max(Math.abs(vValue - 127), Math.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().equals(QTabletEvent.PointerType.Eraser)) {
                myBrush.setColor(new QColor(Qt.GlobalColor.white));
                myPen.setColor(new QColor(Qt.GlobalColor.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 image an let the pressure decide the pen width, else we set the colors we have set up previously in the method.

TabletApplication Class

We inherit
QApplication in this class because we want to reimplement the event() method.

The TabletEnterProximity and TabletLeaveProximity events are 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.

Here is the implementation of event():

        public boolean event(QEvent event)
        {
            if (event.type().equals(QEvent.Type.TabletEnterProximity) ||
                event.type().equals(QEvent.Type.TabletLeaveProximity)) {
                Tablet.myCanvas.setTabletDevice(((QTabletEvent) event).device());
                return true;
            }
            return super.event(event);
        }
We use this method 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() method

main() Here is the examples main() method:
    public static void main(String args[])
    {
        Tablet.TabletApplication.initialize(args);

        Tablet mainWindow = new Tablet();
        mainWindow.resize(500, 500);
        mainWindow.show();

        Tablet.TabletApplication.exec();
    }
In the main() method we create a Tablet 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.


Copyright © 2009 Nokia Corporation and/or its subsidiary(-ies) Trademarks
Qt Jambi 4.5.2_01