環境
本記事は以下の環境を想定して記述している。
項目 | 値 |
---|---|
OS | Ubuntu 22.04 |
ROS | ROS 2 Humble |
Qt | Qt5.15.3 |
概要
このページでは、Qtを利用したGUI表示の設定に関して説明する。また、Qtのプログラムを作成し、ROSのビルドツールでQtのプログラムをビルドする。ビルドした実行ファイルを起動し、GUIが表示できることを確認する。
このページは、ROS講座69 Qtを使う1(ビルドの設定)およびROS講座70 Qtを使う2(Layout、SIGNAL・SLOT)の一部の内容をROS 2対応させたものである。
Qtとは
QtはGUI開発用のフレームワークで、LinuxやWindows等の様々なプラットフォームに対応したGUIを開発できる。
ROS上で動作するGUIの開発にもQtが利用されている。たとえば、ROSのGUIツールであるrqtは「ROS Qt」のことであり、Qtを使って作成されている。ROS上でGUIを動作させると、GUIからトピックやサービスなどのROS機能を利用できたり、ROSの情報を可視化が可能になるなど、様々な利点がある。
Qtを利用したプログラムを再配布する際は、ライセンスに注意が必要である。
Qtは、商用ライセンスとオープンソースライセンスのいずれかに準拠し利用できる。オープンソースライセンスでは、主要なモジュールはLGPLでライセンスされているが、一部モジュールはGPLでのライセンスとなっている。(Qt Licensingを参照)
前準備
前提条件
このチュートリアルは、実習ROS 2 Pub&Sub通信を実行していることを前提に、Pub&Sub通信のチュートリアルで作成したワークスペースros2_lecture_ws
を利用する。
ROS 2パッケージの作成
パッケージqt_basic1
を作成する。
cd ~/ros2_lecture_ws/src
ros2 pkg create --build-type ament_cmake qt_basic1
Qtの環境構築
Qtを利用するため、以下のコマンドでqtbase5-dev
qt5-qmake
をインストールする。
※ Qt公式のインストール手順には、qt5-default
をインストールして環境を構築するとの説明がある。しかし、qt5-default
はUbuntu16.04までのサポートのため、Ubuntu22.04では上記のパッケージを利用する。
sudo apt install qtbase5-dev qt5-qmake
QtにはQtCreatorと呼ばれる統合開発環境がある。QtCreatorは以下のような機能を持ち、効率的にGUIの開発ができる。
- ソースコードの編集
- GUIのデザイン作成
- GUIの処理をローコードで設定
- ソースコードのビルド
- プログラムの実行やデバッグ
しかし、本講座では簡単なQtの開発に焦点を当て、QtCreatorは利用しない。
QWidgetを利用したウィンドウの表示
Qtのプログラムを動作させて、ウィンドウを表示する方法を説明する。
処理は実行せず、ウィンドウのみを表示する最小のソースコードを作成する。
ウィジェット
「ウィジェット」は、QtにおけるUI要素の基本単位である。ウィンドウそのものやウィンドウ内に表示されるテキスト、ボタン、スライダー、エディタなどは全てウィジェットと呼ばれる。
ウィジェットには親子関係を設定する。親を持たない最上位のウィジェットはウィンドウになり、ウィンドウ名や最小化、最大化等のボタンが表示される。また、子ウィジェットは親ウィジェットの範囲内に配置され、文字列やボタン、スライダー、エディタ等の要素が該当する。以下の図は、この記事内で作成するUIを例に、ウィジェットの親子関係を示したものである。参考:Qt日本語ブログ-Qt をはじめよう! 第8回: QWidget の親子関係を学ぼう
ウィジェットはQWidget
クラスもしくは、QWidget
のサブクラスである。Qtを使ったソースコードではウィジェット単位でインスタンスを作成し、ウィジェットの親子関係や見た目、応答処理などの設定を記述する。
簡単のため、以下では親ウィジェットを「ウィンドウ」、子ウィジェットを「オブジェクト」と記載する。
利用するQtの要素
ウィンドウの表示には、QApplicationとQWidgetという2つのクラスを利用する。
- QApplication
QApplicationは、Qtの設定や実行処理を管理するクラスである。
ウィンドウ数に関わらず、1つのQtアプリに対して1つのQApplicationクラスのインスタンスを作成する。このクラスのexec()
関数を実行することで、ウィンドウの表示や応答処理といった各種の処理が行われる。
より詳細な説明はQt公式ドキュメント:QApplication Classに記載されている。 - QWidget
ウィンドウのGUI設定や処理の設定を行う基本になるクラスである。
ウィンドウやウィンドウ内に配置するオブジェクト等のGUI設定、ユーザーからの入力に対する応答処理の設定などを行う。
今回作成するウィンドウはオブジェクトや処理を持たないため、ウィンドウの表示のための設定のみ行う。関数show()
を実行することで、QApplicationクラスのexec()
を実行したときにウィンドウが表示される。
より詳細な説明はQt公式ドキュメント:QWidget Classに記載されている。
ソースファイルの作成
ディレクトリqt_basic1/src
内に、QApplicationとQWidgetを使ってウィンドウを表示するプログラムqwidget.cpp
を作成する。
#include <QApplication>
#include <QWidget>
int main(int argc, char ** argv)
{
QApplication app(argc, argv);
QWidget * window1 = new QWidget;
window1->show();
return app.exec();
}
作成したqwidget.cpp
の処理を説明する。
インクルード処理では、QApplicationクラスおよびQWidgetクラスを利用するために両者をインクルードする。
#include <QApplication> #include <QWidget>
QApplicationクラスおよびQWidgetクラスのインスタンスを作成する。
ここで、window1
はnew
を利用してヒープ領域に作成している。Qtでは基本的にnew
を使用してウィンドウやオブジェクトを作成し、ヒープ領域にメモリを確保する。参考:Qt日本語ブログ Qt をはじめよう! 第12回: シグナルとスロットを作成しよう
QApplication app(argc, argv); QWidget * window1 = new QWidget;
作成したQWidgetクラスのインスタンスの表示を設定し、Qtの処理を実行する。
window1->show(); return app.exec();
なお、ROS 2のノードとして動作するためには、以下のような処理を実装する必要がある。(実習ROS 2 Pub&Sub通信を参照)
-
rclcpp::Node
クラスを継承したクラスの作成 -
rclcpp::init()
によるノードの初期化 -
rclcpp::spin()
やrclcpp::spin_once()
によるノードの実行
このページで作成するプログラムでは上記の処理は実装しないため、ROS 2のビルドツールを使用してビルドするが、ROSノードとしては動作しない。
ビルドの設定
CMakeLists.txtおよびpackage.xmlにビルド設定を記述する。追加する行をdiff形式で以下に示す。
-
CMakeLists.txt
qwidget.cppのQt5Core
およびQt5Widgets
への依存を解決して、実行ファイルを作成する。また、ビルド結果をqt_basic1/install/
に作成してROS 2コマンドで実行できるように設定している。# find dependencies find_package(ament_cmake REQUIRED) + # packages for Qt + find_package(Qt5Core REQUIRED) + find_package(Qt5Widgets REQUIRED) # uncomment the following section in order to fill in # further dependencies manually. # find_package(<dependency> REQUIRED) + add_executable(qwidget src/qwidget.cpp) + ament_target_dependencies(qwidget Qt5Core Qt5Widgets) + install( + TARGETS + qwidget + DESTINATION lib/${PROJECT_NAME} + ) if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED)
-
package.xml
qtbase5-dev
への依存を記述する。<buildtool_depend>ament_cmake</buildtool_depend> + <build_depend>qtbase5-dev</build_depend> <test_depend>ament_lint_auto</test_depend> <test_depend>ament_lint_common</test_depend>
GUIの起動
作成したパッケージをビルドし、ros2 run
コマンドでqwidget
を実行する。
cd ~/ros2_lecture_ws/
colcon build
. install/setup.bash
ros2 run qt_basic1 qwidget
qwidget
を起動すると、以下のように何も表示されないウィンドウが1つ表示される。
オブジェクトの表示
qwidget
で表示したウィンドウには何も表示されていない。実際にGUIを利用するためには、ウィンドウにユーザーとの対話に必要なオブジェクトを配置する必要がある。
ウィンドウ内にオブジェクトを配置する方法について説明する。オブジェクトはQtの機能の1つであるレイアウトを利用して配置する。
オブジェクトの使い方
前述のとおり、GUIに使用するオブジェクトはQWidget
クラスのサブクラスとして用意されている。例えば以下のようなオブジェクトがよく利用される。
- プッシュボタン:QPushButtonクラス
- 指定した文字列を表示:QLabelクラス
- 1行のテキストエディタ:QLineEditクラス
オブジェクトを利用するには、オブジェクトに対応するクラスのインスタンスを作成し、レイアウトを利用してウィンドウに配置する。
レイアウトの使い方
レイアウトはオブジェクトの配置やサイズなどを管理するクラスである。一般にはオブジェクトを配置する際は、大きさや座標といった様々なパラメータをオブジェクトごとに指定しなければならない。それと比較して、レイアウトを使うことでより簡単にオブジェクトを配置できる。
レイアウトにはいくつかの種類がある。種類によってオブジェクトの配置や位置の指定方法が異なる。例えば、オブジェクトを水平方向に配置するQHBoxLayout
や、垂直方向に配置するQVBoxLayout
、グリッドに配置を指定するQGridLayout
がある。
参考:Qt Documentation - Layout Management
オブジェクトを表示するソースコード
qt_basic1/src/
以下に、レイアウトでオブジェクトを配置したウィンドウを表示するプログラムlayout.cpp
を作成する。
このプログラムを実行すると、以下の3つのウィンドウが表示される。
- プッシュボタンが水平方向に配置されたウィンドウ
- プッシュボタンがグリッド状に配置されたウィンドウ
- 文字列とエディタが垂直方向に配置されたウィンドウ
#include <QApplication>
// include layout
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QGridLayout>
// include object
#include <QPushButton>
#include <QLineEdit>
#include <QLabel>
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");
QGridLayout * layout2 = new QGridLayout;
layout2->addWidget(button2A, 0, 0);
layout2->addWidget(button2B, 0, 1);
layout2->addWidget(button2C, 1, 0, 1, 2);
window2->setLayout(layout2);
window2->show();
QWidget * window3 = new QWidget;
QLabel * label = new QLabel("###Label###");
QLineEdit * edit = new QLineEdit("###LineEdit###");
QVBoxLayout * layout3 = new QVBoxLayout;
layout3->addWidget(label);
layout3->addWidget(edit);
window3->setLayout(layout3);
window3->show();
return app.exec();
}
-
オブジェクトの作成
以下では、ウィンドウ内に配置するオブジェクトのインスタンスを作成している。qwidget.cpp
のQWidgetクラスと同様に、インスタンスはnew
を使用して作成する。QPushButton * button1A = new QPushButton("Button 1A"); QPushButton * button1B = new QPushButton("Button 1B"); QPushButton * button1C = new QPushButton("Button 1C");
QPushButton * button2A = new QPushButton("Button 2A"); QPushButton * button2B = new QPushButton("Button 2B"); QPushButton * button2C = new QPushButton("Button 2C");
QLabel * label = new QLabel("###Label###"); QLineEdit * edit = new QLineEdit("###LineEdit###");
-
オブジェクトの配置
以下ではレイアウトを作成、オブジェクトの設定を行うともに、レイアウトをウィンドウに適用している。
レイアウトのaddWidget()
関数でレイアウトにオブジェクトを設定する。レイアウトの種類によっては引数を与えてオブジェクトの配置を設定できる。また、作成したレイアウトはQWidgetのsetLayout()
関数でウィンドウに設定する。QHBoxLayout * layout1 = new QHBoxLayout; layout1->addWidget(button1A); layout1->addWidget(button1B); layout1->addWidget(button1C); window1->setLayout(layout1);
QGridLayout * layout2 = new QGridLayout; layout2->addWidget(button2A,0,0); layout2->addWidget(button2B,0,1); layout2->addWidget(button2C,1,0,1,2); window2->setLayout(layout2);
QVBoxLayout * layout3 = new QVBoxLayout; layout3->addWidget(label); layout3->addWidget(edit); window3->setLayout(layout3);
-
ウィンドウの表示
最後に、これまでに作成したウィンドウを表示するようにshow()
によって設定し、exec()
でQtを実行する。
ビルド設定
qwidget
と同様にCMakeLists.txtにビルド設定を追記する。新たに依存するパッケージは無いため、package.xmlへの追記は不要である。
add_executable(qwidget src/qwidget.cpp)
+ add_executable(layout src/layout.cpp)
ament_target_dependencies(qwidget Qt5Core Qt5Widgets)
+ ament_target_dependencies(layout Qt5Core Qt5Widgets)
install(
TARGETS
qwidget
+ layout
DESTINATION lib/${PROJECT_NAME}
)
GUIの起動
作成したパッケージをビルドして、ros2 run
コマンドでlayout
を実行する。
cd ~/ros2_lecture_ws/
colcon build
. install/setup.bash
ros2 run qt_basic1 layout
layout
を起動すると、以下のような3つのウィンドウが表示される。ウィジェットの親子関係が設定されて、レイアウトの設定どおりにオブジェクトが配置されていることが分かる。
なお、厳密にはウィンドウの子ウィジェットに各オブジェクトが設定されているわけではない。
レイアウトとウィンドウの間にも親子関係があり、setLayout()
関数ではレイアウトとウィンドウ間の親子関係を設定している(ウィンドウを親に、レイアウトを子に設定)。ウィンドウとオブジェクト間の親子関係は、レイアウトを介して間接的に設定されている。
QWidget
クラスには親子関係を直接設定する方法もある。しかし、単純なGUIではレイアウトを用いるほうが記述が容易である。また、実習ROS 2はROS 2の解説を目的としているため本講座では取り扱わない。
メモリ管理について
ウィジェットが削除されるとき、子のウィジェットのメモリを自動的に解放する機能がある。したがって、オブジェクト等の作成時に確保したメモリを解放する必要はない。(参考:Qt日本語ブログQtをはじめよう!第7回:Qtのオブジェクトモデルを理解しよう)
一方で、以下のようにメモリが解放されない場合もあるため、メモリリークの発生には注意が必要である。
- 親を持たないウィジェットを作成したとき
- 親子関係にあるウィジェットが相互に参照しあう循環参照の状態になったとき
前者は適切にdelete
を行うか適切な親子関係を設定する必要がある。また、後者は相互に参照しあうポインターの片方をQWeakPointerにすると循環参照を回避できる。