ROS2講座04 Qtの基本機能

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

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

# 概要
ROS2ではGUIライブライｒであるQtをよく使用します。Rvizのパネル表示やRqtでも使われています。
ここでは基本的なQtのプログラムを解説します。

# 基本のウィンドウの表示

## C++コード

```c++:qt_lecture/src/qt_sample1/main.cpp
#include <QApplication>
#include <QLabel>

int main(int argc, char *argv[]) {
  QApplication app(argc, argv);
  QLabel* label = new QLabel("######## basic1 ########");
  label->show();
  return app.exec();
}
```

* `QApplication app(argc, argv);`はQtのプログラムで最初に必要な記述です。
* `QLabel`はWidgetの一種です。WidgetはUI要素に対応するクラスで`QLabel`はテキスト表示を行います。
    * クラスのインスタンスを生成した後から文字を設定することもできますが、今回のようにコンストラクターでも文字を設定できます。
    * `QLabel`のメソッド等詳細は[公式サイト](https://doc.qt.io/qt-5/qlabel.html)が詳しいです。
    * `show()`メソッドを呼ぶことでGUIに表示します。
    * `app.exec()`を呼ぶことでGUIが開始します。


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

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

ament_package()
```

* Qtのcmakeは直に書くといろいろ面倒なのでROS2のマクロを使います。
* rviz_commonへの依存を記述すれば、Qtへの依存が入ります。

## ビルド&実行

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

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

![qt_sample1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254442/0524d23a-f47e-a60a-8712-1ba0b87cdbb7.png)

GUIが表示されます。

# レイアウト

ここまでの方法では1つのWidgetしか表示できません。Layoutを使うことで複数のWidgetを表示することが出来ます。

## c++

```c++:qt_lecture/src/qt_sample2/main.cpp
#include <QApplication>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QGridLayout>
#include <QPushButton>

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

  QWidget* window1 = new QWidget;
  QPushButton* button1A = new QPushButton("Button 1A");
  QPushButton* button1B = new QPushButton("Button 1B");
  QPushButton* button1C = new QPushButton("Button 1C");
  QHBoxLayout* layout1  = new QHBoxLayout;
  layout1->addWidget(button1A);
  layout1->addWidget(button1B);
  layout1->addWidget(button1C);
  window1->setLayout(layout1);
  window1->show();

  QWidget* window2 = new QWidget;
  QPushButton* button2A = new QPushButton("Button 2A");
  QPushButton* button2B = new QPushButton("Button 2B");
  QPushButton* button2C = new QPushButton("Button 2C");
  QVBoxLayout* layout2  = new QVBoxLayout;
  layout2->addWidget(button2A);
  layout2->addWidget(button2B);
  layout2->addWidget(button2C);
  window2->setLayout(layout2);
  window2->show();

  QWidget* window3 = new QWidget;
  QPushButton* button3A = new QPushButton("Button 3A");
  QPushButton* button3B = new QPushButton("Button 3B");
  QPushButton* button3C = new QPushButton("Button 3C");
  QGridLayout* layout3  = new QGridLayout;
  layout3->addWidget(button3A,0,0);
  layout3->addWidget(button3B,0,1);
  layout3->addWidget(button3C,1,0,1,2);
  window3->setLayout(layout3);
  window3->show();

  return app.exec();
}
```

* ここでは3種類のLayoutを使います。
    * `QHBoxLayout`は横一列に並べます。`addWidget()`で最初に追加した要素が左に来ます。
    * `QVBoxLayout`は縦一列に並べます。`addWidget()`で最初に追加した要素が上に来ます。
    * `QGridLayout`はグリッド上に並べます。`addWidget()`でマス目中の位置とサイズを指定します。
* `QPushButton`は押しボタンに対応するWidgetです。

## ビルド&実行

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

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

![qt_sample2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254442/e8f445b5-60a2-65e0-5f67-2b9acd93545e.png)

3つのウィンドウが表示されます。

# Signal-Slotの接続
ここまでではGUI要素が表示をしているだけです。GUI同士が連携するためにはSignalとSlotという機構を使います。
ROSでのpub-subのようなもので2つのwidget間をつなぐ仕組みで、特定のイベントが起きるとあるWidgetのSignalが発生します。そうすると紐づけられた別のWidgetのSlotを実行することが出来ます。
SignalとSlotは`connect`関数で紐づけることが出来ます。

```c++:qt_lecture/src/qt_sample3/main.cpp
#include <QApplication>
#include <QVBoxLayout>
#include <QPushButton>
#include <QLineEdit>
#include <QLabel>

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

  QWidget* window = new QWidget;
  QVBoxLayout* layout  = new QVBoxLayout;
  QPushButton* button  = new QPushButton("Quit");
  QLineEdit*   edit    = new QLineEdit("");
  QLabel*      label   = new QLabel("");

  layout->addWidget(button);
  layout->addWidget(edit);
  layout->addWidget(label);
  window->setLayout(layout);

  QObject::connect(button, &QPushButton::clicked, &app, &QApplication::quit);
  QObject::connect(edit, &QLineEdit::textChanged, label, &QLabel::setText);

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

* 今回は3つの押しボタン（`QPushButton`）、文字入力(`QLineEdit`)、文字表示(`QLabel`)の3つのWidgetが登場します。
* 押しボタンが押されたときに、ウィンドウが終了するように`connect`で設定します。
    * 押しボタンが押されたときに`QPushButton::clicked`のsignalが発生します。
    * `QApplication::quit`のslotによって、windowを終了します。
* 文字入力した内容が、文字表示に反映されます。
    * 文字入力の内容が変更されたときに`QLineEdit::textChanged`のsignalが発生します。
    * `QLabel::setText`のslotによって、文字表示の内容を設定します。

## ビルド&実行

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

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

![qt_sample3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254442/12b060a7-7039-76ec-3029-83b040a3a8f3.gif)

UI要素を操作することで別のUI要素のアクションが起きます。

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