環境
本記事は以下の環境を想定して記述している。
項目 | 値 |
---|---|
OS | Ubuntu 22.04 |
ROS | ROS 2 Humble |
Qt | Qt5.15.3 |
概要
実習ROS 2 Qtを使う1では、Qtを利用したGUIの作成方法とROSのビルドツールを使ったビルド方法を説明した。しかし、配置したオブジェクトに対して必要な処理を実装していないため、ボタンを操作したりテキストエディタを編集しても何の変化も起きなかった。この記事では、「シグナル」と「スロット」と呼ばれる機能を使ってオブジェクトの処理を実装する方法を説明する。
このページは、ROS講座70 Qtを使う2(Layout、SIGNAL・SLOT)の一部とROS講座71 Qtを使う3(classの作成)の内容をROS 2対応させたものである。
前準備
前提条件
このチュートリアルの実施前に、次の環境を準備すること。
-
実習ROS 2 Pub&Sub通信のチュートリアルを実施し、ワークスペース
ros2_lecture_ws
を作成する。 - 実習ROS 2 Qtを使う1 Qtの環境構築を実施する。
ROS 2パッケージの作成
パッケージqt_basic2
を作成する。
cd ~/ros2_lecture_ws/src
ros2 pkg create --build-type ament_cmake qt_basic2
シグナル・スロット
Qtではシグナルやスロットと呼ばれる機能を使って応答処理を実装する。シグナル・スロットの基本的な使い方について説明する。
シグナル・スロットとは
-
シグナル
ウィジェットへの入力や内部状態の変化といったイベントが発生したときに、そのイベントを別のウィジェットに発信する仕組みである。シグナルの例を以下に示す。- プッシュボタンが押下される:ウィジェット
QPushButton
のシグナルclicked
- エディタの文字が編集される:ウィジェット
QLineEdit
のシグナルtextChanged
- スライダーの値が変更される:ウィジェット
QSlider
のシグナルvalueChanged
- プッシュボタンが押下される:ウィジェット
-
スロット
シグナル発生時の応答処理を記述したC++の関数であり、シグナルに対して紐づけて利用する。GUIのイベントが発生してシグナルが送られたとき、そのシグナルに紐づけられたスロットの処理が実行される。POSIXのシグナルハンドラに相当するものである。
各ウィジェットの持つシグナルやスロットは、Qt Documentation - Qt Widgets C++ Classesから該当のウィジェットを選択して、"Public Slots"や"Signals"の項目から確認できる。
シグナル・スロットを接続する
シグナルとスロットを紐づけ、シグナル発生時のスロットを設定することを「接続する」と呼ぶ。シグナルとスロットを接続することで、オブジェクトの操作などのイベント発生時の応答処理が実装できる。
シグナルとスロットの接続はconnect()
関数を利用する。connect()
関数の記載形式を以下に示す。ここではオブジェクト1の持つシグナルと、オブジェクト2の持つスロットを接続する場合を例に記している。
connect(*オブジェクト1*, *シグナル名*, *オブジェクト2*, *スロット名*)
なお、一つのシグナルを複数のスロットに、あるいは複数のシグナルを一つのスロットに接続することもできる。
Qt Documentation - Signals & Slots
シグナルとスロットの引数
シグナルおよびスロットはそれぞれ引数を持つ。引数を利用するとシグナルとスロットの間でデータの受け渡しができる。接続するスロットとシグナルは、以下の2つの条件をともに満たす必要がある。
- 引数のデータ型が同じ
- 引数の数が同じか、シグナルよりスロットの引数が少ない
シグナルよりスロットの引数が少ない場合、シグナルの余分な引数は無視される。また、引数の条件を満たさないスロットとシグナルを接続した場合は、以下のようなエラーが出てビルドに失敗する。
error: static assertion failed: Signal and slot arguments are not compatible.
シグナル・スロットを利用したGUIを作成
シグナルとスロットの仕組みを利用したGUIを作成する。
GUIには1行のテキストエディタ、文字列を表示するラベルとプッシュボタンを配置する。シグナルとスロットの仕組みを利用して、以下の2つの応答処理を実装する。
- テキストエディタの文字を編集 → 編集した文字をラベルに表示
- プッシュボタン押下 → GUIを閉じる。
ソースファイルの作成
ディレクトリqt_basic2/src/
内に、QApplicationとQWidgetを使ってウィンドウを表示するプログラムsignal_slot.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;
QLineEdit* edit = new QLineEdit("");
QLabel* label = new QLabel("");
QPushButton* button = new QPushButton("Quit");
layout->addWidget(edit);
layout->addWidget(label);
layout->addWidget(button);
window->setLayout(layout);
QObject::connect(edit, &QLineEdit::textChanged, label, &QLabel::setText);
QObject::connect(button, &QPushButton::clicked, &app, &QApplication::quit);
window->show();
return app.exec();
}
signal_slot.cpp
におけるシグナルとスロットの接続設定を説明する。
以下の行ではテキストエディタの編集を通知するシグナルtextChangedと、ラベルの表示テキストを設定するスロットsetText1を接続している。それぞれQString型(Qtで利用される文字列型)の引数を持っているため、テキストエディタに入力した文字列をラベルが受け取って表示することができる。
QObject::connect(edit, &QLineEdit::textChanged, label, &QLabel::setText);
以下の行ではプッシュボタンの押下を通知するシグナルclicked2と、ウィンドウを閉じてQtの処理を停止するQApplicationクラスのスロットquit2を接続している。
このようにシグナルとスロットを利用することで、ウィンドウなどオブジェクト以外のウィジェットとも通信ができる。
QObject::connect(button, &QPushButton::clicked, &app, &QApplication::quit);
ビルドの設定
CMakeLists.txtおよびpackage.xmlにビルド設定を記述する。追加する行を以下に示す。
-
CMakeLists.txt
signal_slot.cppのQt5Core
およびQt5Widgets
への依存を解決して、実行ファイルを作成する。また、ビルド結果をqt_basic2/install/
に作成してROS 2コマンドで実行できるように設定している。find_package(ament_cmake REQUIRED) + # packages for Qt + find_package(Qt5Core REQUIRED) + find_package(Qt5Widgets REQUIRED) + add_executable(signal_slot src/signal_slot.cpp) + ament_target_dependencies(signal_slot Qt5Core Qt5Widgets) + install( + TARGETS + signal_slot + DESTINATION lib/${PROJECT_NAME} + ) if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED)
-
package.xml
qtbase5-dev
への依存を記述する。+ <build_depend>qtbase5-dev</build_depend>
GUIの起動
作成したパッケージをビルドし、ros2 run
コマンドでsignal_slot
を実行する。
cd ~/ros2_lecture_ws/
colcon build
. install/setup.bash
ros2 run qt_basic2 signal_slot
signal_slot
を起動すると以下のようなウィンドウが1つ表示される。1段目のエディタに入力した文字が2段目に表示される。また、"Quit"のボタンをクリックするとウィンドウが閉じてQtの処理を終了する。シグナルとスロットが接続され、GUIの応答処理が実装できたことが分かる。
スロットの作成
シグナルやスロットは既定のものを利用することに加えて自作も可能である。自分でスロットを作成すると、任意の応答処理が利用できるためGUIの複雑な処理が実装できる。スロットを自作する方法に関して以下で説明する。
なお、シグナルの作成については記事内で取り扱わない。スロットと似た要領で作成することが可能である。
QDialogクラス
スロットやシグナルの作成など、より詳細なGUI設定を行う場合はQDialog
クラスを利用する。
- QWidgetクラスとの関係
QWidgetクラスとQDialogクラスの間には親子関係が設定できる。
親となるQWidgetクラスへのポインタをQDialogクラスのコンストラクタに渡すと、QWidgetクラスとQDialogクラスの間の親子関係を設定できる。親子関係を設定した場合、子のQDialogクラスはウィンドウのオブジェクトとしてふるまう。一方で、親を持たないQDialogクラスはウィンドウになる。 - QDialogクラスの継承
QDialogクラスでウィンドウの設定を行うには、QDialogクラスを継承してサブクラスを作成する。サブクラスで追加する関数でGUI表示の設定やシグナルとスロットの接続などのGUI設定を行うことで、親のQWidgetクラスのウィンドウを設定する。
スロットの自作
スロットの自作はQDialog
クラスのサブクラスで行う。
スロットとして利用したい処理はクラスのメンバ関数として作成する。クラス宣言を以下のように記述すると、その関数はスロットとして利用できる。
class <クラス名> : <継承の設定>
{
Q_OBJECT
private:
...
public slots:
<スロットにしたい関数の宣言>;
...
protected slots:
<スロットにしたい関数の宣言>;
...
private slots:
<スロットにしたい関数の宣言>;
...
}
- マクロ
Q_OBJECT
はウィジェットとして動作するために必要な処理を記載したマクロである。これを記載しなければ、public slots
などのスロットの宣言が利用できない。 - スロットのアクセス権
スロットにはpublic
、protected
、private
といったアクセス権を設定する。(シグナルにはアクセス権の設定はない。)
アクセス権の意味は通常のメンバ宣言と同様である。すなわち、public slots
はクラス外から利用でき、private slots
はクラス内でのみ利用できる。基本的には同じクラス内で利用する場合がほとんどなため、private slots
で十分なことが多い。public slots
やprotected slots
を利用するパターンとしては、「ウィンドウを二つ表示したうえで、一方のウィンドウのシグナルともう一方のウィンドウのスロットを接続する」といった例があげられる。
作成したスロットを使ったGUIの作成
スロットを作成するとともに、そのスロットをシグナルに接続して利用する。
GUIには文字列を表示するラベル、1行のテキストエディタ、プッシュボタンを1つ配置する。表示されているボタンをクリックすると、テキストエディタに入力されている文字列がラベルに表示される。このとき、ボタン押下のシグナルQPushButton::clicked
に自作したスロットを接続して処理を実現する。
ソースコード
qt_basic2/src/
以下にmy_slot.cpp
およびmy_dialog.hpp
、my_dialog.cpp
を作成する。my_slot.cpp
にはメイン関数を、my_dialog.hpp
とmy_dialog.cpp
には、QDialogのサブクラスを記載する。
-
#include <QApplication> #include <QDialog> #include "my_dialog.hpp" int main(int argc, char ** argv) { QApplication app(argc, argv); QWidget * window = new QWidget; MainDialog * dialog = new MainDialog(window); dialog->show(); return app.exec(); }
-
#include <QDialog> #include <QLabel> #include <QPushButton> #include <QLineEdit> class MainDialog : public QDialog { Q_OBJECT public: MainDialog(QWidget * parent); private slots: void setLabelText(); private: QLabel * label; QLineEdit * lineEdit; QPushButton * setButton; };
-
#include <QDialog> #include <QLabel> #include <QPushButton> #include <QLineEdit> #include <QVBoxLayout> #include "my_dialog.hpp" MainDialog::MainDialog(QWidget * parent) : QDialog(parent) { label = new QLabel("empty"); setButton = new QPushButton("Set"); lineEdit = new QLineEdit; connect(setButton, &QPushButton::clicked, this, &MainDialog::setLabelText); QVBoxLayout * layout = new QVBoxLayout; layout->addWidget(label); layout->addWidget(lineEdit); layout->addWidget(setButton); setLayout(layout); } void MainDialog::setLabelText() { QString text = lineEdit->text(); label->setText(text); }
-
QDialogのサブクラスの作成
QDialog
を継承したクラスMainDialog
を作成している。
マクロQ_OBJECT
を記述するとともに、private slots
を使って引数と返り値を持たない関数setLabelText()
をスロットに設定している。また、コンストラクタはQWidget
型へのポインタを受け取る。
(my_dialog.hpp)... class MainDialog : public QDialog { Q_OBJECT public: MainDialog(QWidget * parent); private slots: void setLabelText(); ... }
-
親子関係の設定
コンストラクタが受け取ったQWidget
型のポインタは、ベースクラスQDialog
のコンストラクタへ渡される。これにより、QWidget
のインスタンスが親、MainDialogクラスのインスタンスを子とした親子関係を設定する。
(my_dialog.cpp)... MainDialog::MainDialog(QWidget * parent) : QDialog(parent) { ... }
-
ウィジェットの設定
コンストラクタではレイアウトの設定や、シグナル・スロットの接続を行っている。
文字列を表示するラベルQLabel
とプッシュボタンQPushButton
、1行のテキストエディタQLineEdit
のインスタンスを作成する。また、ボタン押下のシグナルclicked
と自作したスロットsetLabelText
を接続している。最後に、それらのオブジェクトをQVBoxLayout
およびsetLayout()
を使って、それらのオブジェクトをウィンドウに配置している。
(my_dialog.cpp)... label = new QLabel("empty"); setButton = new QPushButton("Set"); lineEdit = new QLineEdit; connect(setButton, &QPushButton::clicked, this, &MainDialog::setLabelText); QVBoxLayout * layout = new QVBoxLayout; layout->addWidget(label); layout->addWidget(lineEdit); layout->addWidget(setButton); setLayout(layout); } ...
自作したスロット
setLabelText
では、テキストエディタに入力されたテキストをラベルにセットして表示する。
(my_dialog.cpp)... void MainDialog::setLabelText() { QString text = lineEdit->text(); label->setText(text); }
-
ウィンドウの表示
最後にウィンドウを表示してQtの処理を実行する。QDialogを使うときはQDialogクラスのshow()
関数を利用する。
(my_slot.cpp)... dialog->show(); return app.exec(); }
ビルド設定
CMakeLists.txtにビルド設定を追記する。新たに作成したオブジェクトの実行ファイルを作成する設定を追加する。また、マクロQ_OBJECT
を利用するためにset(CMAKE_AUTOMOC ON)
を追記する。
+ set(CMAKE_AUTOMOC ON)
add_executable(signal_slot src/signal_slot.cpp)
+ add_executable(my_slot src/my_slot.cpp src/my_dialog.cpp)
ament_target_dependencies(signal_slot Qt5Core Qt5Widgets)
+ ament_target_dependencies(my_slot Qt5Core Qt5Widgets)
install(
TARGETS
signal_slot
+ my_slot
DESTINATION lib/${PROJECT_NAME}
)
新たに依存するパッケージは無いため、package.xmlへの追記は不要である。
GUIの起動
作成したパッケージをビルドして、ros2 run
コマンドでmy_slot
を実行する。
cd ~/ros2_lecture_ws/
colcon build
. install/setup.bash
ros2 run qt_basic2 my_slot
my_slot
を起動すると以下のウィンドウが表示される。2段目のエディタに文字を入力して3段目のボタンをクリックすると、入力した文字が1段目のラベルに表示される。作成したスロットがシグナルと接続され、意図通りに動作していることが分かる。