ROS講座70 Qtを使う2（Layout、SIGANAL・SLOT）

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

| 項目 | 値 |
|:-:|:-:|
| CPU | Core i5-8250U |
| Ubuntu | 20.04 |
| ROS | Noetic |
| Qt | 5.12.8 |

インストールについては[ROS講座02 インストール](https://qiita.com/srs/items/e0e0a9dc3f94c2d3348e)を参照してください。
またこの記事のプログラムはgithubにアップロードされています。[ROS講座11 gitリポジトリ](https://qiita.com/srs/items/25a56a6abda41dcf396e)を参照してください。

# 概要
前回はQtのQLabelだけを使いましたが、今回はまずいろいろなWidgetを使っていきます。
そのために、まず1つのウィンドウにいろいろなWidgetを詰める方法を説明します。前回のプログラムでは1つのウィンドウには1つのwidgetを表示することしかできません。しかしlayout機能を使うと複数の子Widgetを持つ親Widgetを作成することができます。
また、QtではGUIでイベントが起きたときに通知する仕組みがあります。これをSIGNALとSLOTと呼びます。

# Layout

## ソースコード
Widgetを横に積んでいくQHBoxLayout、縦に積んでいくQVBoxLayout、グリッド上に配置するQGridLayoutの3つの例を説明します。

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

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

16行目の`layout1->addWidget(button1A);`で、layout1の子にbutton1Aを追加します。このように追加するだけで自動的にレイアウトが生成されます。
またGridを使う方法だと`layout3->addWidget(button3C,1,0,1,2);`のように後ろに引数が増えます。`1,0,1,2`の最初の2つはy(下方向が正)とx（右が正）の位置で、残りの2つは幅と高さです。


### CMakeList.txt
c++ファイルのビルドの設定を追加します。

```cmake:qt_lecture/CMakeLists.txt
target_link_libraries(qt_basic2
  ${catkin_LIBRARIES}
  ${QT_LIBRARIES}
)
```

## ビルド
```shell
cd ~/catkin_ws
catkin build
```

## 実行
以下のコマンドを実行すると以下のような画面が表示されます。
各ターミナルごとに実行前にsource ~/catkin_ws/devel/setup.bashを実行する必要があります。

```
rosrun qt_lecture qt_basic2
```

![qt_basic2.png](https://qiita-image-store.s3.amazonaws.com/0/254442/8dcd97b5-6f38-8956-a844-440ea7a95a95.png)

# SIGNALとSLOT
何かしらのイベント（ボタンクリック、テキストボックスの文字が更新された）をSIGNALと呼び、そのイベントによって駆動されるものをSLOTとQtの世界では呼びます。

## ソースコード

### c++
```c++:qt_lecture/src/qt_basic3.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();
}
```

* 24、25行目の`QObject::connect()`関数ががイベントの通知、実行をしている部分です。
  * 24行目はbuttonのclicked()のSIGNALが発生したときにappのquit()のSLOTを実行するという部分です。
  * 25行目はeditのtextchange()のSIGNALが発生したときにlabelのsetText()のSLOTを実行するという部分です。この2つの関数はそれぞれQstringの引数を持っています。この引数がSIGNALからSLOTに送られます。
* connect関数は上記のようなFunctionベースの書き方とは別に以下のようなStringベースの書き方もあります。機能的には同じですがStringベースだと名前を間違えてもビルド時に検出されないという欠点があります。上記のようなFunctionベースの書き方が推奨です。

```c++:Stringベースの書き方
QObject::connect(button, SIGNAL(clicked()), &app, SLOT(quit()));
QObject::connect(edit, SIGNAL(textChanged(QString)), label, SLOT(setText(QString)));
```
### CMakeList.txt

```cmake:/qt_lecture/CMakeLists.txt
add_executable(qt_basic3 src/qt_basic3.cpp)
target_link_libraries(qt_basic3
  ${catkin_LIBRARIES}
  ${QT_LIBRARIES}
)
```

## ビルド
```shell
cd ~/catkin_ws
catkin build
```

## 実行
以下のコマンドを実行すると以下のような画面が表示されます。

```shell
rosrun  qt_lecture qt_basic3
```

![qt_basic3.png](https://qiita-image-store.s3.amazonaws.com/0/254442/b8f7f6ea-dada-0136-8724-297c7e8b0b50.png)
quitを押すと画面が閉じます。またテキストボックスに文字を入れるとその下に同じ文字が表示されます。

# コメント
今回はQwidgetが持っているSIGNALをほかのQwidgetが持っているSLOTに繋ぐという方法をとりました。自作のCallback関数をSLOTとして繋ぐためにはこの後説明するクラス化が必要です。

# 参考
[Qtプログラミング入門](https://densan-labs.net/tech/qt/)

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