태블릿 예제
이 예제는 Qt 애플리케이션에서 Wacom 태블릿을 사용하는 방법을 보여줍니다.

Qt 애플리케이션에서 태블릿을 사용하면 ` QTabletEvent`가 생성됩니다. 태블릿 이벤트를 처리하려면 ` tabletEvent()` 이벤트 핸들러를 재구현해야 합니다. 이벤트는 그리기에 사용되는 도구(스타일러스)가 태블릿 근처에 접근하거나 떠날 때(즉, 태블릿에 닿아 있지만 눌리지 않은 상태), 도구를 눌렀다가 놓을 때, 태블릿 위에서 도구를 이동할 때, 도구의 버튼 중 하나가 눌리거나 놓일 때 발생합니다.
QTabletEvent 에서 확인할 수 있는 정보는 사용 중인 기기에 따라 다릅니다. 이 예제는 스타일러스, 에어브러시, 아트 펜 등 최대 세 가지의 서로 다른 그리기 도구를 지원하는 태블릿을 처리할 수 있습니다. 이 중 어느 도구를 사용하든 이벤트에는 도구의 위치, 태블릿에 가해지는 압력, 버튼 상태, 수직 기울기 및 수평 기울기(즉, 태블릿 하드웨어가 이를 제공할 수 있는 경우, 장치와 태블릿에 수직인 선 사이의 각도)가 포함됩니다. 에어브러시에는 핑거 휠이 있으며, 이 휠의 위치 정보도 태블릿 이벤트에서 확인할 수 있습니다. 아트 펜은 태블릿 표면에 수직인 축을 중심으로 회전할 수 있어 캘리그라피에 사용할 수 있습니다.
이 예제에서는 그리기 프로그램을 구현합니다. 스타일러스를 사용하여 마치 종이에 연필로 그리는 것처럼 태블릿에 그림을 그릴 수 있습니다. 에어브러시로 그리면 가상 페인트가 분사되며, 핑거 휠을 사용하여 분사 밀도를 조절할 수 있습니다. 아트 펜으로 그리면 펜의 회전 각도에 따라 선의 두께와 끝점 각도가 결정됩니다. 또한 압력과 기울기 값을 할당하여 색상의 투명도(알파)와 채도, 그리고 획의 두께를 조절할 수도 있습니다.
이 예제는 다음과 같이 구성되어 있습니다:
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 가 하나씩 있습니다. 액션 그룹은 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()`의 오버로드 버전을 사용하여 단축키(선택적으로 아이콘 포함)가 있는 메뉴 항목을 생성하고, 해당 메뉴에 추가하며, 슬롯에 연결하는 모든 작업을 단 한 줄의 코드로 수행합니다. ` QKeySequence `를 사용하여 이러한 일반적인 메뉴 항목에 대한 플랫폼별 표준 키 단축키를 가져옵니다.
또한 ‘Brush’ 메뉴도 채웁니다. 브러시를 변경하는 명령에는 일반적으로 표준 단축키가 없으므로, tr()를 사용하여 애플리케이션의 언어 번역과 함께 단축키도 변환되도록 설정합니다.
이제 ‘태블릿’ 메뉴의 하위 메뉴에서 상호 배타적인 동작 그룹 하나를 생성하는 방법을 살펴보겠습니다. 이 그룹은 그려지는 선이나 에어브러시로 칠하는 색상의 반투명도(알파 채널)를 조절하는 데 사용할 각 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);사용자가 드로잉 색상의 알파 구성 요소를 태블릿의 압력, 기울기, 또는 에어브러시 도구의 썸휠 위치 중 어느 것으로 조절할지 선택할 수 있도록 하고자 합니다. 각 선택 항목마다 하나의 동작이 있으며, 알파 값을 변경하지 않고(즉, 색상을 불투명하게 유지하기 위해) 선택하지 않도록 하는 추가 동작도 있습니다. 이 동작들을 체크박스로 구현하며, 그룹 제어( alphaChannelGroup )는 언제든지 하나의 동작만 선택되도록 보장합니다. 액션이 선택되면 그룹에서 ` 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 속성을 사용하지 않는다면, 대신 QAction 포인터 자체를 비교해야 할 것입니다(예: switch 문에서). 하지만 그렇게 하려면 비교를 위해 각 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 를 사용하여 m_pen 와 m_brush 를 통해 QPixmap 를 그립니다. QTabletEvent 가 수신될 때마다, lastPoint 에서 현재 QTabletEvent 에 지정된 지점까지 선을 그리고, 다음 번을 위해 위치와 회전 정보를 lastPoint 에 저장합니다. saveImage() 및 loadImage() 함수는 QPixmap 를 디스크에 저장하고 불러옵니다. 픽스맵은 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 라는 세 가지 유형의 이벤트가 전달되는데, 이는 태블릿 위에서 그리기 도구를 눌렀을 때, 떼었을 때, 또는 이동했을 때 발생합니다. 태블릿에서 도구를 눌렀을 때 m_deviceDown 를 true 로 설정합니다. 그러면 이동 이벤트를 수신했을 때 그리기를 수행해야 한다는 것을 알 수 있습니다. 사용자가 주목하기로 선택한 태블릿 이벤트 속성에 따라 m_brush 및 m_pen 을 업데이트하기 위해 updateBrush() 를 구현했습니다. 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()`의 구현 코드입니다:
void TabletCanvas::paintPixmap(QPainter&painter, QTabletEvent*event)
{
static qreal maxPenRadius = pressureToWidth(1.0);
painter.setRenderHint(QPainter::Antialiasing);
switch (event->deviceType()) {
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;
case QInputDevice::DeviceType::Puck:
case QInputDevice::DeviceType::Mouse:
{
const QString error(tr("이 입력 장치는 이 예제에서 지원되지 않습니다."));
#if QT_CONFIG(statustip)
QStatusTipEvent status(error);
QCoreApplication::sendEvent(this, &status);
#else
qWarning() << error;
#endif
}
break;
default:
{
const QString error(tr("알 수 없는 태블릿 기기 - 스타일러스로 처리 중"));
#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::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 ` 객체가 인스턴스화되기 전에는 이벤트 처리를 구현하는 클래스를 사용할 수 없습니다.
© 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.