ROS2講座05 Qtをクラスで使う

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

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

# 概要
QtでGUI要素のシグナルでごくじ処理をするためにはClassの記述が必要です。
Qtのクラスの記述にはMOCや独自マクロの記述が必要です。

# ソースコード

## C++コード

```c++:qt_lecture/src/qt_sample4/main_dialog.hpp
#include <QDialog>
#include <QLabel>
#include <QPushButton>
#include <QLineEdit>
#include <QTimer>

class MainDialog : public QDialog
{
  Q_OBJECT
public:
  MainDialog(QWidget *parent);

public Q_SLOTS:
  void setLabelText();
  void clearButtonText();

Q_SIGNALS:
  void modifyText(QString);

private:
  QLabel *label_;
  QLineEdit *lineEdit_;
  QPushButton *setButton_;
  QTimer *timer_;
};
```

* GUIを記述するclassのheaderです。
* `QDialog`はGUIの画面を表すクラスです。これを継承して独自処理を追加します。
* Qtのクラスでは独自のマクロの記述が必要です。
    * `Q_OBJECT`はQtのマクロでQtのクラスの記述の最初に書きます。
    * `Q_SLOTS`はslot関数の定義の前に記述するマクロです。
    * `Q_SIGNALS`はsignalの前に記述するマクロです（こちらはアクセス指定子は必要ないです）。

```c++:qt_lecture/src/qt_sample4/main_dialog.cpp
#include <QDialog>
#include <QLabel>
#include <QPushButton>
#include <QLineEdit>
#include <QVBoxLayout>

#include "main_dialog.hpp"

MainDialog::MainDialog(QWidget *parent) : QDialog(parent)
{
  label_ = new QLabel(tr("empty"));
  setButton_ = new QPushButton(tr("Set"));
  lineEdit_ = new QLineEdit;
  timer_ = new QTimer;

  connect(setButton_, &QPushButton::clicked, this, &MainDialog::setLabelText);
  connect(timer_, &QTimer::timeout, this, &MainDialog::clearButtonText);
  connect(this, &MainDialog::modifyText, label_, &QLabel::setText);

  QVBoxLayout *layout = new QVBoxLayout;
  layout->addWidget(label_);
  layout->addWidget(lineEdit_);
  layout->addWidget(setButton_);
  setLayout(layout);
}

void MainDialog::setLabelText()
{
  QString text = lineEdit_->text();
  setButton_->setText("Done");
  timer_->start(500);
  Q_EMIT modifyText(text);
}

void MainDialog::clearButtonText()
{
  setButton_->setText("Set");
}
```

* コンストラクタでGUIの設定をしています。
* slot関数として`setLabelText()`を設定します。この中でイベント発生時に実行したい処理を書きます。
    * connect関数での設定により、押しボタンを押したときにイベントが起きます。
* signal関数として`modifyText()`を設定しています。`Q_EMIT modifyText(text);`と記述することでsignalを発生できます。
    * connect関数での設定により、このsignalが発生したときにラベルの文字が変更になります。
* `QTimer`によって指定の時間後にイベントを発生する(signalを発生する)ことが出来ます。
    * `timer_->start(500);`で500ms後に`timeout()`のシグナルが発生します。
* ビルド時にQt特有の処理としてMeta-Object Compiler(MOC)があります。これによってヘッダーはコンパイル前にマクロ処理が行われます。まれに`include`するファイルでトラブルが起きることがあります。その時は`include`を`#ifndef Q_MOC_RUN`~`#endif`で囲んでください。

```c++:qt_lecture/src/qt_sample4/main.cpp
#include <QApplication>
#include "main_dialog.hpp"

int main(int argc, char *argv[])
{
  QApplication app(argc, argv);
  QWidget *window = new QWidget;
  MainDialog *dialog = new MainDialog(window);
  dialog->show();
  return app.exec();
}
```

## 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_sample4
  src/qt_sample4/main.cpp
  src/qt_sample4/main_dialog.cpp
)
ament_target_dependencies(qt_sample4
  "rviz_common"
)
install(
  TARGETS qt_sample4
  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_sample4
```
![qt_sample4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254442/87e945d1-543e-2c82-0080-ba30327ac810.gif)

* ボタンを押すと文字入力の内容がラベルにコピーします。
* またボタンを押すとボタンの文字が一瞬「Done」になり、しばらく後「Set」に戻ります。

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