環境
この記事は以下の環境で動いています。
| 項目 | 値 |
|---|---|
| CPU | Core i5-8250U |
| Ubuntu | 20.04 |
| ROS | Noetic |
| Qt | 5.12.8 |
インストールについてはROS講座02 インストールを参照してください。
またこの記事のプログラムはgithubにアップロードされています。ROS講座11 gitリポジトリを参照してください。
概要
これまでQtを使ってGUIを作る方法を解説してきましたが、Qtの記述は煩雑で、簡単に変更をする必要なあるUIを作るには不向きです。このためにQtQuickという仕組みがあります。QtQuickではGUIの設定が書かれたqmlファイルを読み込んでGUIを生成します。
今回はこれを使ってROSの通信をしてみます。
QtQuickの基本的な使い方
依存パッケージのインストール
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
ソースコード
プログラム本体
# 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
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ではスライダーを動かしたときに実行する分を書けます。
ビルド
cd ~/catkin_ws
catkin build
実行
各ターミナルごとに実行前にsource ~/catkin_ws/devel/setup.bashを実行する必要があります。
roscore
roscd qml_lecture/
rosrun qml_lecture qml_basic resources/basic.qml
ROSの通信を行う
方法としては2通りあって
- ROSの通信を行うエレメントを作成する
- ROSの通信を行うオブジェクトを製作してプログラム本体で接続する。
手軽なので、後者を使います。
ソースコード
ros通信用object
# 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を発します。
プログラム本体
# 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用)
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用)
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という名前に変換します。
-
ビルド
cd ~/catkin_ws
catkin build
実行
各ターミナルごとに実行前にsource ~/catkin_ws/devel/setup.bashを実行する必要があります。
roscore
roscd qml_lecture/
rosrun qml_lecture qml_ros resources/pub.qml
roscd qml_lecture/
rosrun qml_lecture qml_ros resources/sub.qml
参考
ROS_QML_Example
qml入門
カスタムエレメントを作る

