TurtleSim(ROSシミュレータ)のGUIをpyqt5で自作する

# 概要

「pyqt5の使い方」〜「ROS環境への実装」が主な内容になります。

ですので、本記事は以下のものを作成して実装したらゴールとします。

機能:
1. [前進]ボタンを押すと、TurtleSimの亀が前進する。
2. [左へ回転]ボタンを押すと、TurtleSimの亀が左へ回転する。
3. [右へ回転]ボタンを押すと、TurtleSimの亀が右へ回転する。

![exam_GUI.png](https://qiita-image-store.s3.amazonaws.com/0/266068/d6255867-9365-b5d2-6ce5-da42667dbc45.png)

（ROSの機能の一つである「rqt」でいいじゃんって話なんですが、
今回はデモってことで、適当に応用していただけたらって感じで...）

作業時間は「pyqt環境構築」～「実装」までで、1時間くらいです。

## 記事の対象者
- ROSを触ったことがある
- GUIを作成したことがない

## ROS環境の構築
以下を参考にさせていただきました。

ROSに触れたことがない方でも、以下の8.2節(P68)まで読んでいただけると、以下の理解が容易になると思います。

[『ROSではじめるロボットプログラミング/小倉 崇』](https://www.amazon.co.jp/ROS%E3%81%A7%E3%81%AF%E3%81%98%E3%82%81%E3%82%8B%E3%83%AD%E3%83%9C%E3%83%83%E3%83%88%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E2%80%95%E3%83%95%E3%83%AA%E3%83%BC%E3%81%AE%E3%83%AD%E3%83%9C%E3%83%83%E3%83%88%E7%94%A8%E3%80%8C%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF%E3%80%8D-I%E3%83%BB-BOOKS-%E5%B0%8F%E5%80%89-%E5%B4%87/dp/4777519015/ref=sr_1_1?s=books&ie=UTF8&qid=1547359326&sr=1-1&keywords=ROS%E3%81%A7%E3%81%AF%E3%81%98%E3%82%81%E3%82%8B)

## pyqt5環境の構築
以下を参考にさせていただきました。
[Qt DesignerによるPyQt5のGUIプログラム作成](https://tomosoft.jp/design/?p=9067)

# GUI作成の流れ
1. QtデザイナでGUIの画面を作成（「.ui」ファイルとなる）。
2. 「.ui」→「.py」へ変換。
3. GUIのメインプログラムを書く。

## Qt 5デザイナでGUIの画面を作成
![Screenshot from 2019-01-13 17-33-34.png](https://qiita-image-store.s3.amazonaws.com/0/266068/617d4eaf-2106-34fa-2837-a222c6bfd4fd.png)

Qt 5デザイナを起動し、以下の画面を開きます。
![Qt_tool.png](https://qiita-image-store.s3.amazonaws.com/0/266068/a8ae8537-fac8-8235-aa9a-30d061054386.png)

今回、用いるのは上の赤枠の4箇所です。
1. Main Window。GUIの土台です。ここに描画したものが完成するGUI画面になります。
2. 使用するウィジェット(ツール)です。ボタンとかチェックボックスとか。
3. ウィジェットのプロパティです。ウィジェットの名前や大きさ、色などを設定します。
4. シグナル/スロットのプロパティです。GUIの機能を設定します。

はじめにウィジェット(ボタンやチェックボックスなど)を配置していきます。

試しに、赤枠2のウィジェットボックスから[push botton]を選択して、赤枠1のMain Windowに貼り付けてみます。


![Screenshot from 2019-01-10 22-42-13.png](https://qiita-image-store.s3.amazonaws.com/0/266068/05e52294-be3e-c0eb-721f-64cb2d86e142.png)


次に赤枠3のウィジェットのプロパティで[push botton]の名前を変更します。

[push botton]を選択して、ObjectNameとボタンの表示名を編集しましょう。

ボタンの表示名はボタンをダブルクリックすることで編集できます。

（ここではObjectNameを「pushButton_forward」、ボタンの表示名を「前進」としました。）

![property.png](https://qiita-image-store.s3.amazonaws.com/0/266068/0b87e504-9021-4390-e8bc-ec29865c9272.png)

同様に「左へ回転」と「右へ回転」ボタンも作成します。

- 「左へ回転」ボタンのプロパティのObjectNameを「pushButton_rotate_left」に設定しました。
- 「右へ回転」ボタンのプロパティのObjectNameを「pushButton_rotate_right」に設定しました。


![Screenshot from 2019-01-10 22-47-36.png](https://qiita-image-store.s3.amazonaws.com/0/266068/fe2557d8-2bc3-f1eb-feed-3cbdc773526b.png)


次にウィジェットにシグナル/スロットを設定します。

以下の4.1をクリックして、
4.2のウィジェットをクリックアンドドラッグ、
4.3の適当な場所にドロップしてください。

![signal.png](https://qiita-image-store.s3.amazonaws.com/0/266068/a4c19942-397c-a366-1732-d43fb7b4e9b2.png)

以下の画面が開いたと思います。

![Screenshot from 2019-01-10 22-48-02.png](https://qiita-image-store.s3.amazonaws.com/0/266068/3076eb08-1ac9-c867-8306-a283ba13243d.png)

チェックボックス[QWidgetから継承したシグナルとスロットを表示する]にチェック入れてください。

デフォルトでもたくさんの機能が準備されています。
今回は<font color="Red">[前進ボタンをクリックした時に亀を前進させたい]</font>ので、
左枠のQPushButtonを<font color="Red">clicked()</font>に設定します。
このように<font color="Blue">何かが発生したことを他のオブジェクトに通知するための関数(今回は前進ボタンをクリックした時:clicked())をシグナル</font>と呼びます。

次にスロットを設定します。
<font color="Blue">スロットとはシグナル（ボタンをクリックした時）に反応して実行される関数(今回は、亀を移動・回転させたい機能)</font>のことです。
シグナルで設定した「clicked()」などの汎用的な関数はデフォルトで用意されていましたが、
今回はTurtleSimの亀を移動・回転させることがスロットに当たるので、
自分でスロットを作成する必要があります。

スロットの設定であるDialog(QDialog)の下にある[編集]ボタンを押して、
スロットを作成してください。

- clicked_forward()
- clicked_rotate_left()
- clicked_rotate_right()

スロットを作成すると、以下のように関数名が増えると思います。

![slot.png](https://qiita-image-store.s3.amazonaws.com/0/266068/1faceef9-9e6b-03f2-533f-3430ff91957c.png)

今、[前進]ボタンのシグナルとスロットを設定しているので、
スロットをclicked_forward()に設定して[OK]ボタンを押してください。
これで[前進]ボタンのシグナルとスロットの設定が終わりました。

同様に、[左へ回転]ボタンと[右へ回転]ボタンのシグナルとスロットも設定してください。
(赤枠4.1のボタンを押して、[左へ回転]ボタンでクリックアンドドラッグアンドドロップ....略)

- [左へ回転]ボタンのシグナルは「clicked()」、スロットは「clicked_rotate_left()」に設定します。
- [右へ回転]ボタンのシグナルは「clicked()」、スロットは「clicked_rotate_right()」に設定します。


完成すると以下のようになります。
![check_point.png](https://qiita-image-store.s3.amazonaws.com/0/266068/eb783406-eb8d-a964-5847-720fd55922bc.png)

上赤枠の「オブジェクトインスペクタ」と下赤枠の「シグナル/スロットエディタ」の設定が合っていればOKです。
「オブジェクトインスペクタ」の設定が違う場合は手順3に戻って、ObjectNameの再設定を行ってください。
「シグナル/スロットエディタ」の設定が違う場合は手順4に戻って、シグナル/スロットの再設定を行ってください。

画面を作成したら、「turtlesim.ui」で保存してください。

以下のようなプログラムになっていると思います。

```turtlesim.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Dialog</class>
 <widget class="QDialog" name="Dialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>362</width>
    <height>206</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Dialog</string>
  </property>
  <widget class="QPushButton" name="pushButton_forward">
   <property name="geometry">
    <rect>
     <x>140</x>
     <y>40</y>
     <width>89</width>
     <height>25</height>
    </rect>
   </property>
   <property name="text">
    <string>前進</string>
   </property>
  </widget>
  <widget class="QPushButton" name="pushButton_rotate_left">
   <property name="geometry">
    <rect>
     <x>40</x>
     <y>80</y>
     <width>89</width>
     <height>25</height>
    </rect>
   </property>
   <property name="text">
    <string>左へ回転</string>
   </property>
  </widget>
  <widget class="QPushButton" name="pushButton_rotate_right">
   <property name="geometry">
    <rect>
     <x>240</x>
     <y>80</y>
     <width>89</width>
     <height>25</height>
    </rect>
   </property>
   <property name="text">
    <string>右へ回転</string>
   </property>
  </widget>
 </widget>
 <resources/>
 <connections>
  <connection>
   <sender>pushButton_rotate_left</sender>
   <signal>clicked()</signal>
   <receiver>Dialog</receiver>
   <slot>clicked_rotate_left()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>90</x>
     <y>93</y>
    </hint>
    <hint type="destinationlabel">
     <x>91</x>
     <y>148</y>
    </hint>
   </hints>
  </connection>
  <connection>
   <sender>pushButton_forward</sender>
   <signal>clicked()</signal>
   <receiver>Dialog</receiver>
   <slot>clicked_forward()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>182</x>
     <y>59</y>
    </hint>
    <hint type="destinationlabel">
     <x>188</x>
     <y>115</y>
    </hint>
   </hints>
  </connection>
  <connection>
   <sender>pushButton_rotate_right</sender>
   <signal>clicked()</signal>
   <receiver>Dialog</receiver>
   <slot>clicked_rotate_right()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>276</x>
     <y>96</y>
    </hint>
    <hint type="destinationlabel">
     <x>273</x>
     <y>152</y>
    </hint>
   </hints>
  </connection>
 </connections>
 <slots>
  <slot>clicked_rotate_left()</slot>
  <slot>clicked_rotate_right()</slot>
  <slot>clicked_forward()</slot>
 </slots>
</ui>
```

## 「.ui」→「.py」へ変換
$ pyuic5 -o turtlesim.py turtlesim.ui

Qt 5デザイナで作成した「turtlesim.ui」から「turtlesim.py」が自動生成されます。


```turtlesim.py
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'turtlesim.ui'
#
# Created by: PyQt5 UI code generator 5.10.1
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_Dialog(object):
    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(362, 206)
        self.pushButton_forward = QtWidgets.QPushButton(Dialog)
        self.pushButton_forward.setGeometry(QtCore.QRect(140, 40, 89, 25))
        self.pushButton_forward.setObjectName("pushButton_forward")
        self.pushButton_rotate_left = QtWidgets.QPushButton(Dialog)
        self.pushButton_rotate_left.setGeometry(QtCore.QRect(40, 80, 89, 25))
        self.pushButton_rotate_left.setObjectName("pushButton_rotate_left")
        self.pushButton_rotate_right = QtWidgets.QPushButton(Dialog)
        self.pushButton_rotate_right.setGeometry(QtCore.QRect(240, 80, 89, 25))
        self.pushButton_rotate_right.setObjectName("pushButton_rotate_right")

        self.retranslateUi(Dialog)
        self.pushButton_rotate_left.clicked.connect(Dialog.clicked_rotate_left)
        self.pushButton_forward.clicked.connect(Dialog.clicked_forward)
        self.pushButton_rotate_right.clicked.connect(Dialog.clicked_rotate_right)
        QtCore.QMetaObject.connectSlotsByName(Dialog)

    def retranslateUi(self, Dialog):
        _translate = QtCore.QCoreApplication.translate
        Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
        self.pushButton_forward.setText(_translate("Dialog", "前進"))
        self.pushButton_rotate_left.setText(_translate("Dialog", "左へ回転"))
        self.pushButton_rotate_right.setText(_translate("Dialog", "右へ回転"))


```

## GUIのメインプログラムを書く。

Qtデザイナで指定したスロット名。

- clicked_rotate_left
- clicked_forward
- clicked_rotate_right

に役割を与えて終わり！！！

```main_turtlesim.py
#! /usr/bin/python3
# -*- coding: utf-8 -*-


# GUI。
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from turtlesim import Ui_Dialog　# 自動生成したファイルをインポート
# ROS。
import rospy
from geometry_msgs.msg import Twist


class Test(QDialog):
    def __init__(self,parent=None):
        # GUI。
        super(Test, self).__init__(parent)
        self.ui = Ui_Dialog()
        self.ui.setupUi(self)

        # ROS。pubの設定。
        self.cmd_vel_Twist = Twist()
        self.pub_cmd_vel = rospy.Publisher('/turtle1/cmd_vel',Twist,queue_size=10)

    def clicked_rotate_left(self):
        """
        Qtデザイナで指定したスロット名。
        「左に回転」ボタンで実行したい処理を書く。
        """
        self.cmd_vel_Twist.angular.z = 1 # 1[rad/s]で左に回転
        self.pub_cmd_vel.publish(self.cmd_vel_Twist)
        self.cmd_vel_Twist.angular.z = 0

    def clicked_forward(self):
        """
        Qtデザイナで指定したスロット名。
        「前進」ボタンで実行したい処理を書く。
        """
        self.cmd_vel_Twist.linear.x = 1 # 1[m/s]で直進
        self.pub_cmd_vel.publish(self.cmd_vel_Twist)
        self.cmd_vel_Twist.linear.x = 0

    def clicked_rotate_right(self):
        """
        Qtデザイナで指定したスロット名。
        「右に回転」ボタンで実行したい処理を書く。
        """
        self.cmd_vel_Twist.angular.z = -1 # 1[rad/s]で右に回転
        self.pub_cmd_vel.publish(self.cmd_vel_Twist)
        self.cmd_vel_Twist.angular.z = 0


if __name__ == '__main__':
    rospy.init_node('turtlesim_talker')
    app = QApplication(sys.argv)
    window = Test()
    window.show()
    sys.exit(app.exec_())
```

## 実行
1. roscoreの起動
$ roscore

2. turtlesimの実行
$ rosrun turtlesim turtlesim_node

3. GUIのメインプログラムの実行
$ python main_turtlesim.py


![TurtlrSim.gif](https://qiita-image-store.s3.amazonaws.com/0/266068/79e53aa2-07e1-fb38-17d9-7a5fbeaf028e.gif)


# 終わりに

いい感じに動きましたかね（？）

今回は僕がGUIを作成する際に躓いたポイントを重点的に書き出しました。
特にシグナルとスロットの設定と実行プログラムの関係については画像を入れて詳しく説明したつもりです。

今回はシンプルなGUIを作成しましたが、作り込めば製品化も可能らしいですね。
（僕もモチベがあればドローンのGUIを作成したいと思います）

# 参考
[ROSではじめるロボットプログラミング](https://www.amazon.co.jp/ROS%E3%81%A7%E3%81%AF%E3%81%98%E3%82%81%E3%82%8B%E3%83%AD%E3%83%9C%E3%83%83%E3%83%88%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E2%80%95%E3%83%95%E3%83%AA%E3%83%BC%E3%81%AE%E3%83%AD%E3%83%9C%E3%83%83%E3%83%88%E7%94%A8%E3%80%8C%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF%E3%80%8D-I%E3%83%BB-BOOKS-%E5%B0%8F%E5%80%89-%E5%B4%87/dp/4777519015/ref=sr_1_1?s=books&ie=UTF8&qid=1547359326&sr=1-1&keywords=ROS%E3%81%A7%E3%81%AF%E3%81%98%E3%82%81%E3%82%8B)

[Qt DesignerによるPyQt5のGUIプログラム作成](https://tomosoft.jp/design/?p=9067)

[Qt Blog](https://blog.qt.io/jp/)