ROS2講座06 タッチパネルWidgetを作る

# 環境

この記事は以下の環境で動いています。

| 項目 |	値 |
|:--|:--|
| CPU | Core i5-8250U |
| Ubuntu | 22.04 |
| ROS2 | Humble |
| Qt | 5.15.3 |

# 概要

Qtでは様々なWidgetがありますが、タッチパットのようなGUIに対応するWidgetはデフォルトではありません。
基本クラスであるQWidgetを継承することでタッチパットのWidgetを作成します。

# ソースコード

## C++

```c++:qt_lecture/src/qt_sample5/touch_widget.hpp
#pragma once
#include <QtWidgets>

class TouchWidget : public QWidget
{
  Q_OBJECT
public:
  TouchWidget(QWidget *parent = 0);

  // property
  bool grayout_{false};
  QPointF latest_scaled_value_{};

  // event
  void setEnabled(bool enable);
  void paintEvent(QPaintEvent *event) override;
  void mouseMoveEvent(QMouseEvent *event) override;
  void mousePressEvent(QMouseEvent *event) override;
  void mouseReleaseEvent(QMouseEvent *event) override;

public Q_SLOTS:
  void setEnabled(int state);

Q_SIGNALS:
  void notifyScaledPoint(QPointF);
  void notifyScaledPoint(QString);

private:
  void setValue(QPointF point);
  int getPadSize(void) const;
  QPoint getCenter(void) const;
  QPointF getScaledPoint(const QPoint &point) const;
  QPoint getPadPoint(const QPointF &point) const;
};
```

* `QWidget`を継承したクラスを作ります。

```c++:qt_lecture/src/qt_sample5/touch_widget.cpp
#include "touch_widget.hpp"
#include <sstream>
#include <iostream>

TouchWidget::TouchWidget(QWidget *parent) : QWidget(parent)
{
  setMinimumSize(100, 100);
  setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding));
}

void TouchWidget::setEnabled(bool enable)
{
  grayout_ = !enable;
  update();
}

void TouchWidget::setEnabled(int state)
{
  grayout_ = (state != Qt::Checked);
  update();
}

void TouchWidget::paintEvent(QPaintEvent *event)
{
  (void)event;
  QPoint center = getCenter();
  int pad_size = getPadSize();

  QPainter painter(this);
  if (!grayout_)
  {
    painter.setBrush(Qt::white);
    painter.setPen(Qt::black);
  }
  else
  {
    painter.setBrush(Qt::lightGray);
    painter.setPen(Qt::darkGray);
  }

  painter.drawRect(QRect(center.x() - pad_size / 2, center.y() - pad_size / 2, pad_size, pad_size));
  painter.drawLine(center.x(), center.y() - pad_size / 2, center.x(), center.y() + pad_size / 2);
  painter.drawLine(center.x() - pad_size / 2, center.y(), center.x() + pad_size / 2, center.y());

  if (!grayout_)
  {
    QPen arrow;
    arrow.setWidth(pad_size / 20);
    arrow.setColor(Qt::black);
    arrow.setCapStyle(Qt::RoundCap);
    arrow.setJoinStyle(Qt::RoundJoin);
    painter.setPen(arrow);

    QPoint arrows[2];
    arrows[0] = center;
    arrows[1] = getPadPoint(latest_scaled_value_);
    painter.drawPolyline(arrows, 2);
  }
}

void TouchWidget::mouseMoveEvent(QMouseEvent *event)
{
  if (!grayout_)
  {
    setValue(getScaledPoint(QPoint(event->x(), event->y())));
    update();
  }
}

void TouchWidget::mousePressEvent(QMouseEvent *event)
{
  if (!grayout_)
  {
    setValue(getScaledPoint(QPoint(event->x(), event->y())));
    update();
  }
}

void TouchWidget::mouseReleaseEvent(QMouseEvent *event)
{
  (void)event;
  setValue(QPointF{});
  update();
}

// private
void TouchWidget::setValue(QPointF point)
{
  latest_scaled_value_ = point;
  Q_EMIT notifyScaledPoint(point);
  std::string stm = std::to_string(point.x()) + "," + std::to_string(point.y());
  QString text = QString::fromStdString(stm.c_str());
  Q_EMIT notifyScaledPoint(text);
}

int TouchWidget::getPadSize(void) const
{
  int w = width();
  int h = height();
  int half_size = ((w > h) ? (h - 1) : (w - 1)) / 2;
  return half_size * 2;
}

QPoint TouchWidget::getCenter(void) const
{
  return QPoint((width() - 1) / 2, (height() - 1) / 2);
}

QPointF TouchWidget::getScaledPoint(const QPoint &point) const
{
  int pad_size = getPadSize();
  QPoint center = getCenter();
  float scaled_x = (float)(point.x() - center.x()) / (pad_size / 2);
  float scaled_y = (float)(point.y() - center.y()) / (pad_size / 2);

  QPointF output;
  output.setX(std::min(std::max(scaled_x, -1.0f), 1.0f));
  output.setY(std::min(std::max(scaled_y, -1.0f), 1.0f));
  return output;
}

QPoint TouchWidget::getPadPoint(const QPointF &point) const
{
  int pad_size = getPadSize();
  QPoint center = getCenter();

  QPoint output;
  output.setX(center.x() + point.x() * (pad_size / 2));
  output.setY(center.y() + point.y() * (pad_size / 2));
  return output;
}
```
* コンストラクタではWidgetのサイズの設定をします。
    * `setMinimumSize()`ではWidgetの最小サイズを決めます。ウィンドウを縮小してもこれ以上は小さくならなくなります。
    * `setSizePolicy()`ではウィンドウ全体のサイズを変えた時にこのwidgetのサイズをどう変化させるかを指定します。縦横それぞれ指定できます。`Expanding`は広げられるときは広げるといったオプションです。ほかのオプションについては[公式ページ](https://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum)を参照
* GUIのイベントが起きた時のcallbackをoverrideして処理を追加しています。
    * `mousePressEvent()`はポインターがWidget上で左ボタンが押下されたとき、`mouseMoveEvent()`は左ボタンを押下しながらポインターが動いたときに発生します。この時にタッチパッドの値を更新します。
        * `update()`を呼ぶことによって
    *  `mouseReleaseEvent()`は左ボタンが離されたときに発生します。このタイミングでタッチパットの値をクリアしています。
    * `paintEvent()`は画面描画をするときに呼ばれるcallbackです。この中で背景の枠と、ドラックしている場所への線を描画します。

```c++:qt_lecture/src/qt_sample5/main.cpp
#include <QApplication>
#include <QVBoxLayout>
#include <QLabel>
#include <QCheckBox>
#include "touch_widget.hpp"

int main(int argc, char *argv[])
{
  QApplication app(argc, argv);

  QWidget *window = new QWidget;
  QVBoxLayout *layout = new QVBoxLayout;
  TouchWidget *touch_widget = new TouchWidget();
  touch_widget->setEnabled(false);
  QCheckBox *check = new QCheckBox("enable");
  QLabel *label = new QLabel("");

  layout->addWidget(touch_widget);
  layout->addWidget(label);
  layout->addWidget(check);
  window->setLayout(layout);

  QObject::connect(check, &QCheckBox::stateChanged, touch_widget, qOverload<int>(&TouchWidget::setEnabled));
  QObject::connect(touch_widget, qOverload<QString>(&TouchWidget::notifyScaledPoint), label, &QLabel::setText);

  window->show();
  return app.exec();
}
```

* `QCheckBox`での設定によって、タッチパッドの有効無効を切り替えます。
* タッチパッドでの取得値を`QLabel`に表示します。
* Widgetのsignal/slotにオーバーロードメソッド（引数だけが違う同じ名前のメソッド）がある場合はconnectで`unknown overload`のエラーが出ます。この場合は` qOverload<QString>()`のマクロを記述することで、オーバーロードメソッドの関数型を記述することで指定することが出来ます。

## cmake

```cmake:qt_lecture/CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(qt_lecture)

# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rviz_common REQUIRED)

set(CMAKE_AUTOMOC ON)

add_executable(qt_sample5
  src/qt_sample5/main.cpp
  src/qt_sample5/touch_widget.cpp
)
ament_target_dependencies(qt_sample5
  "rviz_common"
)
install(
  TARGETS qt_sample5
  DESTINATION lib/${PROJECT_NAME}
)

ament_package()
```

* Qtのcmakeは直に書くといろいろ面倒なのでROS2のマクロを使います。
* rviz_commonへの依存を記述すれば、Qtへの依存が入ります。
* Qtのマクロを実行するためには`set(CMAKE_AUTOMOC ON)`の記述が必要です。

# ビルド&実行

```shell:ビルド
source /opt/ros/humble/setup.bash
cd ros2_ws
colcon build
```

```shell:実行
ros2 run qt_lecture qt_sample5
```

![qt_sample5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254442/c94523f0-1679-eb94-3ef8-c870d78d544a.gif)

* タッチパッドで取得した値をlabelに表示します。

# 目次ページへのリンク
[ROS2講座の目次へのリンク](https://qiita.com/srs/items/5f0d765343ca78f524ed)