環境
この記事は以下の環境で動いています。
項目 | 値 |
---|---|
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入門
カスタムエレメントを作る