ROS講座117 QtQuick(qml)を使う

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

| 項目 | 値 |
|:-:|:-:|
| 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を使ってGUIを作る方法を解説してきましたが、Qtの記述は煩雑で、簡単に変更をする必要なあるUIを作るには不向きです。このためにQtQuickという仕組みがあります。QtQuickではGUIの設定が書かれたqmlファイルを読み込んでGUIを生成します。
今回はこれを使ってROSの通信をしてみます。

# QtQuickの基本的な使い方

依存パッケージのインストール

```shell
sudo apt-get install qtquickcontrols2-5-dev 
sudo apt-get install qml-module-qtqml-models2 
sudo apt-get install qml-module-qtquick-window2
sudo apt-get install qml-module-qtquick-controls
```

## ソースコード

### プログラム本体

```c++:qml_lecture/src/qml_basic.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>

int main(int argc, char** argv)
{
  // Init Qt
  QGuiApplication app(argc, argv);

  if (argc < 2)
  {
    printf("qml filename is ewquired.\n");
    return 0;
  }

  QQmlApplicationEngine engine(&app);
  engine.load(QUrl(argv[1]));

  return app.exec();
}
```

`QQmlApplicationEngine engine(&app)`でqmlのエンジンをインスタンス化して、`engine.load(QUrl("*****"))`でqmlスクリプトを読み込みます。`QUrl`の中はファイルの相対or絶対パスを書きます。

### qml

```qml:qml_lecture/resources/basic.qml
import QtQuick 2.3
import QtQuick.Window 2.2
import QtQuick.Controls 1.2

Window {
    Column {
        Button {
            text: "Ok"
            onClicked: text_label.text = "ok"
        }
        Button {
            text: "Cancel"
            onClicked: text_label.text = "cancel"
        }
        Text {
            id: text_label
            text: "Hello World!"
        }
        Slider {
            onValueChanged : text_label.text = value
        }
    }
    visible: true
}
```

* qmlではGUIの設定をします。「エレメント」と呼ばれる要素（上記ではwindow、buttonなど）を並べます。
* `Window`はGUIのウィンドウを作るエレメントです。
* `Culumn`はエレメントを縦に並べるエレメントです。
* `Button`はボタンを出現させるエレメントです。`text`はボタンの中に書かれる文字です。`onClicked`に書いた文がボタンを押したときに実行されます。
* `Text`は文字を表示するエレメントです。`id`は各々のエレメントを識別する要素です。上記のように`id`をつけると、ほかのエレメントからも`(id名).(要素名)`の形式でアクセスることが出来ます。
* `Slider`はスライダーを表示する要素です。`onValueChanged`ではスライダーを動かしたときに実行する分を書けます。

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

## 実行
各ターミナルごとに実行前にsource ~/catkin_ws/devel/setup.bashを実行する必要があります。

```shell:ターミナル1
roscore
```

```shell:ターミナル2
roscd qml_lecture/
rosrun qml_lecture qml_basic resources/basic.qml 
```

![qt_qml1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254442/07e72cf6-2224-44f6-3cb3-60a2504d4e0e.gif)


# ROSの通信を行う

方法としては2通りあって

* ROSの通信を行うエレメントを作成する
* ROSの通信を行うオブジェクトを製作してプログラム本体で接続する。

手軽なので、後者を使います。

## ソースコード

### ros通信用object

```c++:qml_lecture/src/pubsub_object.h
#pragma once

#include <ros/ros.h>
#include <std_msgs/String.h>

#include <QObject>
#include <QVariant>
#include <QDebug>

class RosStringObject : public QObject
{
  Q_OBJECT
public:
  RosStringObject(QObject* parent = nullptr)
  {
    connect(this, &RosStringObject::publishString, this, &RosStringObject::publishStringSlot);
    string_pub_ = nh_.advertise<std_msgs::String>("chatter", 10);
    string_sub_ = nh_.subscribe("chatter", 10, &RosStringObject::stringCallback, this);
  }

signals:
  void publishString(QString s);
  void subscribeString(QString text);

private slots:
  void publishStringSlot(QString s)
  {
    std_msgs::String msg;
    msg.data = s.toStdString();
    string_pub_.publish(msg);
  }

private:
  void stringCallback(const std_msgs::String& msg)
  {
    emit subscribeString(QString(msg.data.c_str()));
  }

  ros::NodeHandle nh_;
  ros::Publisher string_pub_;
  ros::Subscriber string_sub_;
};
```

* `publishString()`は他のエレメントからたたいてもらうためのsignalです。Qtではsignalはインターフェースの役割しか果たさないので実装は書けません。`connect()`で`publishStringSlot()`をつないで、この中で実装を書きます。
* ROSトピックを受けると`stringCallback()`が呼ばれます。この中で`emit subscribeString()`によってsignalを発します。

### プログラム本体

```c++:qml_lecture/src/qml_ros.cpp
#include <ros/ros.h>
#include <std_msgs/String.h>

#include <QCoreApplication>
#include <QObject>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QUrl>
#include <QString>
#include <QtQml>
#include <QtConcurrent/QtConcurrent>
#include <QFuture>
#include <QFutureWatcher>

#include "pubsub_object.h"

int main(int argc, char** argv)
{
  if (argc < 2)
  {
    printf("qml filename is ewquired.\n");
    return 0;
  }

  ros::init(argc, argv, "qml_ros", ros::init_options::AnonymousName);
  ros::NodeHandle nh;

  QGuiApplication app(argc, argv);
  RosStringObject ros_string(&app);

  // for ros spin()
  QFutureWatcher<void> rosThread;
  rosThread.setFuture(QtConcurrent::run(&ros::spin));
  QObject::connect(&rosThread, &QFutureWatcher<void>::finished, &app, &QCoreApplication::quit);
  QObject::connect(&app, &QCoreApplication::aboutToQuit, []() { ros::shutdown(); });

  QQmlApplicationEngine engine(&app);
  engine.rootContext()->setContextProperty("ros_string", &ros_string);
  engine.load(QUrl(argv[1]));

  return app.exec();
}
```

* プログラム中央は`ros::spin()`を実行しているものです。別スレッドで回して、Qtのプログラムが終了すると一緒に落ちるようにしています。
* `engine.rootContext()->setContextProperty()`では先ほど作ったROS通信用のobjectをqtに接続しています。
    * 第1引数はオブジェクトの名前を設定します。qmlファイルからはこの名前で指定します。

### qml (pub用)

```c++:qml_lecture/resources/pub.qml
import QtQuick 2.3
import QtQuick.Window 2.2
import QtQuick.Controls 1.2

Window {
    Column {
        Button {
            text: "OK"
            onClicked: ros_string.publishString("OK")
        }
        Button {
            text: "Cancel"
            onClicked: ros_string.publishString("Cancel")
        }
    }
    visible: true
}
```

* `Button`の`onClicked`で`ros_string.publishString()`を記述しています。これによって上記のROS通信用objectにsignalが発行されます。

### qml (sub用)

```c++:qml_lecture/resources/sub.qml
import QtQuick 2.3
import QtQuick.Window 2.2
import QtQuick.Controls 1.2

Window {
    Column {
        Text {
            id: label_text
            text: "unknown"
        }
    }
    Connections{
        target:ros_string
        onSubscribeString: label_text.text = text
    }
    visible: true
}
```

* `Connection`はqtプログラムの中での`connect()`に相当する処理が行えるエレメントです。
    * `target`ではプログラム本体で定義したobjectの名前を指定します。
    * `onSubscribeString`はシグナル名です。`onSubscribeString`とするとROS通信用のobjectの`subscribeString`という名前のsignalが発行されたときの動作をします。`xxxYyyy`という名前のシグナルなら`onXxxxYyyy`という名前に変換します。

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

## 実行
各ターミナルごとに実行前にsource ~/catkin_ws/devel/setup.bashを実行する必要があります。

```shell:ターミナル1
roscore
```

```shell:ターミナル2
roscd qml_lecture/
rosrun qml_lecture qml_ros resources/pub.qml 
```

```shell:ターミナル3
roscd qml_lecture/
rosrun qml_lecture qml_ros resources/sub.qml 
```

![qt_qml2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254442/25b8d01d-9ee4-0ed1-61d6-6adc79f5bddb.gif)

# 参考
[ROS_QML_Example](https://github.com/Ipiano/ROS_QML_Example)
[qml入門](https://qiita.com/DAR/items/400a1725ddedcaa6ef0e)
[カスタムエレメントを作る](https://qiita.com/helicalgear/items/2beb08e1333854fdebda)

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