En esta página

Ejemplo de tableta

Este ejemplo muestra cómo utilizar una tableta Wacom en aplicaciones Qt.

Aplicación que muestra un área de dibujo

Cuando se utiliza una tableta con aplicaciones Qt, se generan QTabletEvents. Necesita reimplementar el manejador de eventos tabletEvent() si desea manejar los eventos de la tableta. Los eventos se generan cuando la herramienta (stylus) utilizada para dibujar entra y sale de la proximidad de la tableta (es decir, cuando se cierra pero no se presiona sobre ella), cuando la herramienta se presiona y se suelta de ella, cuando la herramienta se mueve a través de la tableta, y cuando se presiona o suelta uno de los botones de la herramienta.

La información disponible en QTabletEvent depende del dispositivo utilizado. Este ejemplo puede manejar una tableta con hasta tres herramientas de dibujo diferentes: un lápiz óptico, un aerógrafo y un bolígrafo artístico. Para cualquiera de ellos, el evento contendrá la posición de la herramienta, la presión sobre la tableta, el estado de los botones, la inclinación vertical y la inclinación horizontal (es decir, el ángulo entre el dispositivo y la perpendicular de la tableta, si el hardware de la tableta puede proporcionarlo). El aerógrafo tiene una rueda de dedos; la posición de ésta también está disponible en el evento de la tableta. El bolígrafo artístico proporciona rotación alrededor del eje perpendicular a la superficie de la tableta, de modo que puede utilizarse para caligrafía.

En este ejemplo implementamos un programa de dibujo. Puedes utilizar el lápiz para dibujar en la tableta como utilizas un lápiz sobre papel. Cuando dibujas con el aerógrafo, obtienes una pulverización de pintura virtual; la rueda del dedo se utiliza para cambiar la densidad de la pulverización. Cuando dibujas con el lápiz artístico, obtienes una línea cuya anchura y ángulo del punto final dependen de la rotación del lápiz. La presión y la inclinación también se pueden asignar para cambiar los valores alfa y de saturación del color y la anchura del trazo.

El ejemplo consiste en lo siguiente:

  • La clase MainWindow hereda de QMainWindow, crea los menús y conecta sus ranuras y señales.
  • La clase TabletCanvas hereda QWidget y recibe los eventos de la tableta. Utiliza los eventos para pintar sobre un pixmap fuera de la pantalla, y luego lo renderiza.
  • La clase TabletApplication hereda de QApplication. Esta clase maneja los eventos de proximidad de la tableta.
  • La función main() crea una MainWindow y la muestra como ventana de nivel superior.

Definición de la clase MainWindow

El MainWindow crea un TabletCanvas y lo establece como widget central.

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() configura los menús con las acciones. Tenemos un QActionGroup para las acciones que alteran el canal alfa, la saturación de color y el ancho de línea respectivamente. Los grupos de acciones están conectados a las ranuras setAlphaValuator(), setSaturationValuator(), y setLineWidthValuator(), que llaman a funciones en TabletCanvas.

Implementación de la clase MainWindow

Comenzamos con un vistazo al constructor MainWindow():

MainWindow::MainWindow(TabletCanvas *canvas)
    : m_canvas(canvas)
{
    createMenus();
    setWindowTitle(tr("Tablet Example"));
    setCentralWidget(m_canvas);
    QCoreApplication::setAttribute(Qt::AA_CompressHighFrequencyEvents);
}

En el constructor llamamos a createMenus() para crear todas las acciones y menús, y establecemos el lienzo como widget central.

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);

Al principio de createMenus() rellenamos el menú Archivo. Usamos una sobrecarga de addAction(), introducida en Qt 5.6, para crear un elemento de menú con un acceso directo (y opcionalmente un icono), añadirlo a su menú, y conectarlo a una ranura, todo con una línea de código. Utilizamos QKeySequence para obtener los atajos de teclado estándar específicos de la plataforma para estos elementos de menú comunes.

También rellenamos el menú Pincel. El comando para cambiar un pincel normalmente no tiene un atajo de teclado estándar, así que usamos tr() para permitir traducir el atajo de teclado junto con la traducción del idioma de la aplicación.

Ahora veremos la creación de un grupo de acciones mutuamente excluyentes en un submenú del menú Tabla, para seleccionar qué propiedad de cada QTabletEvent se utilizará para variar la translucidez (canal alfa) de la línea que se está dibujando o del color que se está aerografiando.

    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);

Queremos que el usuario pueda elegir si el componente alfa del color dibujado debe ser modulado por la presión de la tableta, la inclinación o la posición de la rueda selectora de la herramienta de aerografía. Tenemos una acción para cada opción, y una acción adicional para elegir no cambiar el alfa, es decir, para mantener el color opaco. Hacemos que las acciones sean comprobables; el alphaChannelGroup se asegurará entonces de que sólo una de las acciones sea comprobada en cualquier momento. La señal triggered() se emite desde el grupo cuando se comprueba una acción, así que la conectamos a MainWindow::setAlphaValuator(). Necesitará saber a qué propiedad (valorador) de QTabletEvent prestar atención a partir de ahora, así que usamos la propiedad QAction::data para pasar esta información. (Para que esto sea posible, el enum Valuator debe ser un metatipo registrado, para que pueda ser insertado en un QVariant. Esto se consigue mediante la declaración Q_ENUM en tabletcanvas.h.)

Aquí está la implementación de setAlphaValuator():

void MainWindow::setAlphaValuator(QAction *action)
{
    m_canvas->setAlphaChannelValuator(qvariant_cast<TabletCanvas::Valuator>(action->data()));
}

Simplemente necesita recuperar el enum Valuator de QAction::data(), y pasarlo a TabletCanvas::setAlphaChannelValuator(). Si no estuviéramos usando la propiedad data, necesitaríamos comparar el puntero QAction, por ejemplo en una sentencia switch. Pero eso requeriría mantener punteros a cada QAction en variables de clase, para propósitos de comparación.

Aquí está la implementación de 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);
}

Hacemos una inicialización perezosa de QColorDialog la primera vez que el usuario elige Color de pincel... desde el menú o a través del atajo de acción. Mientras el diálogo esté abierto, cada vez que el usuario elija un color diferente, se llamará a TabletCanvas::setColor() para cambiar el color del dibujo. Como se trata de un diálogo no modal, el usuario es libre de dejar abierto el diálogo de color, para poder cambiar de color cómoda y frecuentemente, o cerrarlo y volver a abrirlo más tarde.

He aquí la implementación de 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;
}

Usamos QFileDialog para permitir al usuario seleccionar un archivo para guardar el dibujo, y luego llamamos a TabletCanvas::saveImage() para escribirlo en el archivo.

Esta es la implementación de 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");
}

Dejamos que el usuario seleccione el archivo de imagen a abrir con QFileDialog; luego pedimos al lienzo que cargue la imagen con loadImage().

Esta es la implementación de about():

void MainWindow::about()
{
    QMessageBox::about(this, tr("About Tablet Example"),
                       tr("This example shows how to use a graphics drawing tablet in Qt."));
}

Mostramos un cuadro de mensaje con una breve descripción del ejemplo.

Definición de la clase TabletCanvas

La clase TabletCanvas proporciona una superficie sobre la que el usuario puede dibujar con una tableta.

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;
};

El lienzo puede cambiar el canal alfa, la saturación de color y el ancho de línea del trazo. Tenemos un enum listando las propiedades de QTabletEvent con las que es posible modularlas. Mantenemos una variable privada para cada una: m_alphaChannelValuator, m_colorSaturationValuator y m_lineWidthValuator, y proporcionamos funciones accesorias para ellas.

Dibujamos sobre un QPixmap con m_pen y sobre m_brush utilizando m_color. Cada vez que se recibe un QTabletEvent, el trazo se dibuja desde lastPoint hasta el punto dado en el actual QTabletEvent, y luego la posición y la rotación se guardan en lastPoint para la próxima vez. Las funciones saveImage() y loadImage() guardan y cargan el QPixmap en el disco. El mapa de píxeles se dibuja en el widget en paintEvent().

La interpretación de los eventos de la tableta se realiza en tabletEvent(), y paintPixmap(), updateBrush(), y updateCursor() son funciones de ayuda utilizadas por tabletEvent().

Implementación de la clase TabletCanvas

Empezaremos echando un vistazo al 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);
}

En el constructor inicializamos la mayoría de las variables de nuestra clase.

Aquí está la implementación de saveImage():

bool TabletCanvas::saveImage(const QString &file)
{
    return m_pixmap.save(file);
}

QPixmap implementa la funcionalidad de guardarse en disco, por lo que simplemente llamamos a save().

Aquí está la implementación de loadImage():

bool TabletCanvas::loadImage(const QString &file)
{
    bool success = m_pixmap.load(file);

    if (success) {
        update();
        return true;
    }
    return false;
}

Simplemente llamamos a load(), que carga la imagen desde file.

He aquí la implementación de 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();
}

Esta función recibe tres tipos de eventos: TabletPress TabletRelease y TabletMove, que se generan cuando una herramienta de dibujo es presionada, levantada o movida a través de la tableta. Establecemos m_deviceDown en true cuando un dispositivo es presionado sobre la tableta; entonces sabemos que debemos dibujar cuando recibimos eventos de movimiento. Hemos implementado updateBrush() para actualizar m_brush y m_pen dependiendo de a cuál de las propiedades de eventos de la tableta el usuario ha elegido prestar atención. La función updateCursor() selecciona un cursor para representar la herramienta de dibujo en uso, de forma que al pasar con la herramienta cerca de la tableta, se pueda ver qué tipo de trazo se va a realizar.

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);
}

Si se está utilizando un lápiz artístico (RotationStylus), también se llama a updateCursor() para cada evento TabletMove, y muestra un cursor girado para que pueda ver el ángulo de la punta del lápiz.

Aquí está la implementación de 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);
}

La primera vez que Qt llama a paintEvent(), m_pixmap se construye por defecto, por lo que QPixmap::isNull() devuelve true. Ahora que sabemos a qué pantalla vamos a renderizar, podemos crear un pixmap con la resolución adecuada. El tamaño del pixmap con el que rellenamos la ventana depende de la resolución de la pantalla, ya que el ejemplo no soporta zoom; y puede ser que una pantalla tenga altos DPI mientras que otra no. También necesitamos dibujar el fondo, ya que por defecto es gris.

Después de eso, simplemente dibujamos el pixmap en la parte superior izquierda del widget.

Aquí está la implementación de paintPixmap():

void TabletCanvas::paintPixmap(QPainter&painter, QTabletEvent * evento) { static qreal maxPenRadius = pressureToWidth(1.0); painter.setRenderHint(QPainter::Antialiasing); switch (event->deviceType()) { case QInputDevice::TipoDispositivo::Aerógrafo: { 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::transparente); painter.setBrush(grad);                qreal radius = grad.radius(); painter.drawEllipse(event->position(), radius, radius); update(QRect(evento->posición().toPunto() - QPoint(radio, radio), QSize(radio * 2, radio * 2))); } break; case QInputDevice::DeviceType::Puck: case QInputDevice::DeviceType::Ratón: { const QString error(tr("Este dispositivo de entrada no es soportado por el ejemplo."));#if QT_CONFIG(statustip)               QStatusTipEvent status(error);              QCoreApplication::sendEvent(this, &status);#else                qWarning() << error;
#endif} break; default: { const QString error(tr("Dispositivo de tableta desconocido - tratando como stylus"));#if QT_CONFIG(statustip)               QStatusTipEvent status(error);              QCoreApplication::sendEvent(this, &status);#else                qWarning() << error;
#endif} Q_FALLTHROUGH(); case QInputDevice::DeviceType::Stylus: if (event->pointingDevice()->capabilities().testFlag(QPointingDevice::Capacidad::Rotación)) { 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; }

En esta función dibujamos sobre el pixmap basándonos en el movimiento de la herramienta. Si la herramienta utilizada en la tableta es un lápiz óptico, queremos dibujar una línea desde la última posición conocida hasta la posición actual. También asumimos que este es un manejo razonable de cualquier dispositivo desconocido, pero actualizamos la barra de estado con una advertencia. Si es un aerógrafo, queremos dibujar un círculo relleno con un degradado suave, cuya densidad puede depender de varios parámetros del evento. Por defecto depende de la presión tangencial, que es la posición de la rueda del dedo en el aerógrafo. Si la herramienta es un estilete de rotación, simulamos un rotulador dibujando segmentos de trazo trapezoidal.

        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;

En updateBrush() configuramos el lápiz y el pincel utilizados para dibujar de forma que coincidan con m_alphaChannelValuator, m_lineWidthValuator, m_colorSaturationValuator, y m_color. Examinaremos el código para configurar m_brush y m_pen para cada una de estas variables:

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);

Obtenemos el tono, la saturación, el valor y los valores alfa del color de dibujo actual. hValue y vValue se ajustan a la inclinación horizontal y vertical como un número de 0 a 255. Los valores originales están en grados de -0 a -255 . Los valores originales están en grados de -60 a 60, es decir, 0 es igual a -60, 127 es igual a 0 y 255 es igual a 60 grados. El ángulo medido es entre el dispositivo y la perpendicular de la tableta (véase QTabletEvent para una ilustración).

    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);
    }

El canal alfa de QColor se da como un número entre 0 y 255 donde 0 es transparente y 255 es opaco, o como un número de punto flotante donde 0 es transparente y 1.0 es opaco. pressure() devuelve la presión como un qreal entre 0.0 y 1.0. Obtenemos los valores alfa más pequeños (es decir, el color es más transparente) cuando el lápiz está perpendicular a la tableta. Seleccionamos el mayor de los valores de inclinación vertical y horizontal.

    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:
            ;
    }

La saturación de color en el modelo de color HSV se puede dar como un entero entre 0 y 255 o como un valor de punto flotante entre 0 y 1. Elegimos representar alfa como un entero, por lo que llamamos a setHsv() con valores enteros. Eso significa que tenemos que multiplicar la presión a un número entre 0 y 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);
    }

El ancho del trazo de la pluma puede aumentar con la presión, si así se elige. Pero cuando el ancho de la pluma es controlado por la inclinación, dejamos que el ancho aumente con el ángulo entre la herramienta y la perpendicular de la tableta.

    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);
    }
}

Por último, comprobamos si el puntero es el lápiz o el borrador. Si es el borrador, establecemos el color al color de fondo del pixmap y dejamos que la presión decida el ancho del lápiz, si no establecemos los colores que hemos decidido previamente en la función.

Definición de la Clase TabletApplication

Heredamos QApplication en esta clase porque queremos reimplementar la función 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 existe como subclase de QApplication para recibir los eventos de proximidad de la tableta y reenviarlos a TabletCanvas. Los eventos TabletEnterProximity y TabletLeaveProximity se envían al objeto QApplication, mientras que otros eventos de la tableta se envían al manejador event() de QWidget's, que los envía a tabletEvent().

Implementación de la clase TabletApplication

Esta es la implementación de 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);
}

Usamos esta función para manejar los eventos TabletEnterProximity y TabletLeaveProximity, que se generan cuando una herramienta de dibujo entra o sale de la proximidad de la tableta. Aquí llamamos a TabletCanvas::setTabletDevice(), que a su vez llama a updateCursor(), que establecerá un cursor apropiado. Esta es la única razón por la que necesitamos los eventos de proximidad; para dibujar correctamente, basta con que TabletCanvas observe los device() y pointerType() en cada evento que recibe.

La función main()

He aquí la función main() del ejemplo:

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();
}

Aquí creamos un MainWindow y lo mostramos como una ventana de nivel superior. Usamos la clase TabletApplication. Necesitamos establecer el canvas después de crear la aplicación. No podemos usar clases que implementen el manejo de eventos antes de que un objeto QApplication sea instanciado.

Proyecto de ejemplo @ code.qt.io

© 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.