このページについて

タブレットの使用例

この例では、QtアプリケーションでWacomタブレットを使用する方法を示します。

描画領域を表示するアプリケーション

Qt アプリケーションでタブレットを使用すると、QTabletEventが生成されます。タブレットイベントを処理するには、tabletEvent() イベントハンドラを再実装する必要があります。 イベントは、描画に使用するツール(スタイラス)がタブレットの近傍に入ったときや離れたとき(つまり、タブレットに接触しているが押されていない状態)、ツールが押されたり離されたりしたとき、ツールがタブレット上を移動したとき、 また、ツールのボタンのいずれかが押されたり離されたりした際にも発生します。

QTabletEvent で利用可能な情報は、使用するデバイスによって異なります。この例では、スタイラス、エアブラシ、アートペンの最大3種類の描画ツールを備えたタブレットを処理できます。 いずれの場合も、イベントにはツールの位置、タブレットへの筆圧、ボタンの状態、垂直方向の傾き、および水平方向の傾き(つまり、タブレットのハードウェアが提供できる場合、デバイスとタブレットに対する垂直線との角度)が含まれます。 エアブラシにはフィンガーホイールが搭載されており、その位置情報もタブレットのイベントから取得可能です。アートペンはタブレット表面に垂直な軸を中心とした回転機能を備えており、書道に使用することができます。

この例では、描画プログラムを実装します。スタイラスを使って、紙に鉛筆で描くのと同じようにタブレット上に描画することができます。エアブラシで描画すると、仮想の絵の具が吹き出します。フィンガーホイールを使って、吹き出しの濃淡を調整します。 アートペンで描画すると、ペンの回転に応じて線の太さや終点の角度が変化する線が描かれます。また、筆圧や傾きを割り当てることで、色の不透明度や彩度、ストロークの太さを変更することも可能です。

この例は以下の内容で構成されています:

  • MainWindow クラスはQMainWindow を継承し、メニューを作成して、それらのスロットとシグナルを接続します。
  • TabletCanvas クラスはQWidget を継承し、タブレットのイベントを受信します。このイベントを使用してオフスクリーンのピクマップに描画を行い、その後レンダリングします。
  • TabletApplication クラスはQApplication を継承しています。このクラスは、タブレットの近接イベントを処理します。
  • main() 関数は、MainWindow を作成し、それをトップレベルウィンドウとして表示します。

MainWindowクラスの定義

MainWindow はTabletCanvas を作成し、それを中心ウィジェットとして設定します。

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() アクションを含むメニューを設定します。アルファチャンネル、色の彩度、線幅をそれぞれ変更するアクション用に、QActionGroup が1つずつ用意されています。アクショングループは、setAlphaValuator() 、setSaturationValuator() 、setLineWidthValuator() の各スロットに接続されており、これらはTabletCanvas 内の関数を呼び出します。

MainWindowクラスの実装

まず、コンストラクタMainWindow() を見てみましょう:

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

コンストラクタ内では、createMenus() を呼び出してすべてのアクションとメニューを作成し、キャンバスを中央のウィジェットとして設定します。

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

createMenus() の冒頭で、[ファイル]メニューの内容を設定します。 Qt 5.6で導入されたaddAction()のオーバーロードを使用し、1行のコードで、ショートカット(およびオプションでアイコン)を持つメニュー項目を作成し、そのメニューに追加し、スロットに接続します。QKeySequence を使用して、これらの一般的なメニュー項目に対するプラットフォーム固有の標準キーショートカットを取得します。

また、「Brush」メニューの内容も設定します。ブラシを変更するコマンドには通常、標準のショートカットキーがないため、tr() を使用して、アプリケーションの言語翻訳に合わせてショートカットキーの変換を有効にします。

次に、「Tablet」メニューのサブメニュー内に、相互に排他的なアクションのグループを1つ作成する方法について見ていきます。これは、描画中の線やエアブラシで塗られる色の半透明度(アルファチャンネル)を変化させるために、各QTabletEvent のどのプロパティを使用するかを選択するためのものです。

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

ユーザーが、描画色のアルファ成分を、タブレットの筆圧、傾き、またはエアブラシツールのサムホイールの位置のいずれによって調整するかを選択できるようにしたいと考えています。 各選択肢ごとに1つのアクションを用意し、さらにアルファを変更しない(つまり、色を不透明のままにする)ための追加のアクションも用意しています。これらのアクションにチェックボックスを設定し、alphaChannelGroup がいつでもいずれか1つのアクションのみがチェックされるように制御します。 アクションがチェックされると、グループからtriggered() シグナルが発信されるため、これをMainWindow::setAlphaValuator() に接続します。今後、QTabletEvent のどのプロパティ(valuator)に注目すべきかを判別する必要があるため、QAction::data プロパティを使用してこの情報を伝達します。 (これを実現するためには、列挙型Valuator が登録済みのメタ型である必要があり、そうすることでQVariant に挿入できるようになります。これは、tabletcanvas.h内のQ_ENUM 宣言によって実現されています。)

以下に、setAlphaValuator() の実装を示します:

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

これは単に、QAction::data() からValuator 列挙型を取得し、それをTabletCanvas::setAlphaChannelValuator() に渡すだけです。もしdata プロパティを使用していなければ、代わりに、例えば switch 文などでQAction ポインタ自体を比較する必要がありました。しかし、その場合は、比較のために各QAction へのポインタをクラス変数に保持しておく必要が生じます。

以下に、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);
}

ユーザーがメニューから、あるいはアクションショートカットを介して「Brush color...」を選択した最初の時点で、QColorDialog の遅延初期化を行います。 ダイアログが開いている間、ユーザーが別の色を選択するたびに、TabletCanvas::setColor() が呼び出され、描画色が変更されます。これは非モーダルダイアログであるため、ユーザーは色ダイアログを開いたままにして、便利に頻繁に色を変更することも、一度閉じて後で再度開くことも自由にできます。

以下に、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;
}

QFileDialog を使用して、ユーザーに描画内容を保存するファイルを選ばせ、その後TabletCanvas::saveImage() を呼び出して実際にファイルに書き込みます。

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

QFileDialog を使用して、ユーザーに開く画像ファイルを選択してもらい、その後、loadImage() を使用してキャンバスにその画像を読み込ませます。

以下は、about() の実装です:

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

この例の概要を簡潔に説明したメッセージボックスを表示します。

TabletCanvas クラスの定義

TabletCanvas クラスは、ユーザーがタブレットを使って描画できるキャンバスを提供します。

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

このキャンバスでは、ストロークのアルファチャンネル、彩度、線幅を変更できます。これらを調整するために使用できるQTabletEvent のプロパティを一覧にした列挙型を用意しています。それぞれについて、m_alphaChannelValuator 、m_colorSaturationValuator 、m_lineWidthValuator というプライベート変数を保持し、これらに対するアクセサ関数を提供しています。

m_color`QPixmap ` を使用して、`m_pen ` および `m_brush ` を持つ `QPixmap ` を描画します。`QTabletEvent ` が受信されるたびに、`lastPoint ` から現在の `QTabletEvent` で指定された点までストロークが描画され、その後、次回のために位置と回転が `lastPoint ` に保存されます。`saveImage() ` および `loadImage() ` 関数は、` ` をディスクに保存および読み込みます。ピクマップは `paintEvent()` でウィジェット上に描画されます。

タブレットからのイベントの処理はtabletEvent() で行われ、paintPixmap() 、updateBrush() 、およびupdateCursor() は、tabletEvent() で使用されるヘルパー関数です。

TabletCanvas クラスの実装

まず、コンストラクタから見ていきましょう:

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

コンストラクタ内では、クラス変数のほとんどを初期化します。

以下に、saveImage() の実装を示します:

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

QPixmap は自身をディスクに保存する機能を実装しているため、単にsave()を呼び出します。

以下は、loadImage() の実装です:

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

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

単にload()を呼び出すだけで、file から画像が読み込まれます。

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

この関数には、TabletPress 、TabletRelease 、TabletMove の3種類のイベントが送信されます。これらは、描画ツールをタブレット上で押し下げたとき、離したとき、または移動させたときに発生します。デバイス(描画ツール)がタブレット上で押し下げられた際に、m_deviceDown をtrue に設定します。これにより、移動イベントを受信した際に描画を行うべきであることを把握できます。 また、updateBrush() を実装し、ユーザーが注目するように選択したタブレットのイベントプロパティに応じて、m_brush およびm_pen を更新するようにしました。updateCursor() 関数は、使用中の描画ツールを表すカーソルを選択します。これにより、ツールをタブレットに近づけてホバーさせると、どのようなストロークが描かれるかを確認できます。

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

アートペン(RotationStylus )が使用されている場合、各TabletMove イベントごとにupdateCursor() も呼び出され、ペン先の角度が確認できるよう、回転したカーソルが表示されます。

以下に、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);
}

Qt が初めて paintEvent() を呼び出す際、m_pixmap はデフォルトコンストラクタで初期化されるため、QPixmap::isNull() はtrue を返します。レンダリング先の画面が判明したため、適切な解像度を持つピクマップを作成できます。 この例ではズーム機能をサポートしていないため、ウィンドウを塗りつぶすピクマップのサイズは画面の解像度によって異なります。また、ある画面は高DPIである一方、別の画面はそうではない場合もあります。デフォルトでは背景が灰色になるため、背景も描画する必要があります。

その後、ウィジェットの左上にピクマップを描画するだけです。

以下に、paintPixmap() の実装を示します:

voidTabletCanvas::paintPixmap(QPainter&painter,QTabletEvent*event)
{
    staticqreal maxPenRadius=pressureToWidth(1.0);
    painter.setRenderHint(QPainter::Antialiasing);

    switch(event->deviceType()) {
        caseQInputDevice::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;
        caseQInputDevice::DeviceType::Puck:
        caseQInputDevice::DeviceType::Mouse:
            {
                constQString error(tr("この入力デバイスは、このサンプルではサポートされていません。"));
#if QT_CONFIG(statustip)
                QStatusTipEvent status(error);
                QCoreApplication::sendEvent(this, &status);
#else
                qWarning() << error;
#endif
            }
            break;
        default:
            {
                constQString error(tr("不明なタブレット端末 - スタイラスとして扱います"));
#if QT_CONFIG(statustip)
                QStatusTipEvent status(error);
                QCoreApplication::sendEvent(this, &status);
#else
                qWarning() << error;
#endif
            }
            Q_FALLTHROUGH();
        caseQInputDevice::DeviceType::Stylus:
            if(event->pointingDevice()->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<<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;
    }
}

この関数では、ツールの動きに基づいてピクマップ上に描画を行います。タブレットで使用されているツールがスタイラスの場合、最後に認識された位置から現在の位置まで線を引きます。また、未知のデバイスに対してもこれが妥当な処理であると仮定しますが、その場合はステータスバーに警告を表示して更新します。 エアブラシの場合は、さまざまなイベントパラメータに応じて濃淡が変化する、柔らかなグラデーションで塗りつぶされた円を描画します。デフォルトでは、エアブラシ上のフィンガーホイールの位置である接線方向の圧力に基づいて濃淡が決まります。ツールが回転式スタイラスの場合は、台形のストロークセグメントを描画することで、フェルトペンを模倣します。

        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;

updateBrush() では、描画に使用するペンとブラシを、m_alphaChannelValuator 、m_lineWidthValuator 、m_colorSaturationValuator 、およびm_color に一致するように設定します。これらの各変数について、m_brush およびm_pen を設定するコードを確認していきます。

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

現在の描画色(drawingcolor)の色相(hue)、彩度(saturation)、明度(value)、不透明度(alpha)の値を取得します。hValue とvValue には、水平および垂直の傾きを0から255までの数値として設定します。 元の値は -60 度から 60 度の範囲の角度で表されます。つまり、0 は -60 度、127 は 0 度、255 は 60 度に相当します。測定される角度は、デバイスとタブレットの垂直線との間の角度です(図解についてはQTabletEvent を参照してください)。

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

QColor のアルファチャンネルは、0が透明、255が不透明となる0から255までの数値、あるいは0が透明、1.0が不透明となる浮動小数点数として指定されます。pressure()は、0.0から1.0の範囲のqrealとして筆圧を返します。 ペンがタブレットに対して垂直なとき、アルファ値は最小(つまり、色が最も透明)になります。垂直方向と水平方向の傾き値のうち、大きい方を選択します。

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

HSV色モデルにおける彩度は、0から255までの整数、または0から1までの浮動小数点値として指定できます。アルファを整数で表現することにしたため、setHsv() を呼び出す際には整数値を使用します。つまり、筆圧を0から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);
    }

設定次第では、筆圧に応じてペンのストローク幅を広くすることも可能です。ただし、ペンの幅を傾きで制御する場合は、ツールとタブレットの垂直線との角度に応じて幅を広くするようにしています。

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

最後に、ポインタがスタイラスか消しゴムかを確認します。消しゴムの場合は、ピクマップの背景色に設定し、筆幅を筆圧で決定します。それ以外の場合は、この関数内で事前に決定した色を設定します。

TabletApplication クラスの定義

このクラスでは、event() 関数を再実装したいので、QApplication を継承しています。

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 このクラスは、タブレットの近接イベントを受信し、それらをTabletCanvas に転送するために、QApplication のサブクラスとして存在します。TabletEnterProximity およびTabletLeaveProximity イベントはQApplication オブジェクトに送信され、その他のタブレットイベントはQWidget のevent() ハンドラに送信され、そこからtabletEvent()に転送されます。

TabletApplication クラスの実装

以下に、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);
}

この関数を使用して、描画ツールがタブレットの近傍に入ったり離れたりした際に発生するTabletEnterProximity およびTabletLeaveProximity イベントを処理します。 ここではTabletCanvas::setTabletDevice() を呼び出し、それがupdateCursor() を呼び出すことで、適切なカーソルが設定されます。プロキシミティイベントが必要なのは、これが唯一の理由です。正しく描画を行うためには、TabletCanvas が受信する各イベントにおいて、device()およびpointerType()を監視するだけで十分です。

main() 関数

以下に、この例のmain() 関数を示します:

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

ここでは、MainWindow を作成し、トップレベルのウィンドウとして表示します。TabletApplication クラスを使用します。アプリケーションの作成後にキャンバスを設定する必要があります。QApplication オブジェクトがインスタンス化される前は、イベント処理を実装するクラスを使用することはできません。

サンプルプロジェクト @ 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.