QT Designer 이용 GUI 만들기

1. 전체 UI 구조 이해

그림의 UI는 대략 이런 구조입니다.

RobotControlGUI QWidget
 ├── row1Layout
 │   ├── status_groupbox
 │   │   ├── status_label
 │   │   └── time_label
 │   └── battery_groupbox
 │       ├── battery_label
 │       └── battery_bar
 │
 ├── row2Layout
 │   ├── velocity_groupbox
 │   │   ├── linear_speed_edit
 │   │   ├── angular_speed_edit
 │   │   ├── publish_cmd_vel_button
 │   │   └── stop_button
 │   └── mode_groupbox
 │       └── mode_combo
 │
 ├── row3Layout
 │   ├── options_groupbox
 │   │   ├── obstacle_check
 │   │   ├── sensor_check
 │   │   └── camera_check
 │   └── speed_limit_groupbox
 │       ├── speed_slider
 │       └── speed_spinbox
 │
 ├── row4Layout
 │   ├── position_groupbox
 │   │   └── position_table
 │   └── long_task_groupbox
 │       ├── task_status_label
 │       ├── task_progress_bar
 │       └── system_check_button
 │
 └── log_groupbox
     └── log_text_edit

Qt Designer의 Object Inspector를 보면 실제로도 비슷하게 구성되어 있습니다.

2. 새 UI 파일 만들기

터미널에서 Qt Designer를 실행합니다.

designer

또는 ROS 2 패키지 안에서 작업한다면 예를 들어 다음 위치에 UI 파일을 만듭니다.

cd ~/ros2_ws/src/robot_control_gui/resource
designer robot_control_gui.ui

Qt Designer가 열리면 처음 창에서 다음을 선택합니다.

Widget

MainWindow가 아니라 Widget을 선택하는 것을 추천합니다.
ROS 2 GUI 노드에서는 보통 QWidget을 상속해서 쓰는 구조가 깔끔합니다.

Create 버튼을 클릭하여 새로운 위젯을 만듭니다.

3. 최상위 QWidget 설정

오른쪽 Object Inspector에서 최상위 객체를 선택합니다.

이름을 다음처럼 바꿉니다.

objectName: RobotControlGUI

창 제목도 바꿉니다.

windowTitle: ROS2 PyQt Robot Control GUI

크기는 대략 이렇게 설정합니다.

geometry: 0, 0, 900, 700

다만 이 값은 초기 크기일 뿐이고, 실제 배치는 Layout이 담당하게 해야 합니다.

먼저 파일을 my_robot_control_gui.ui로 저장합니다.

최종적으로는 다음과 같은 큰 행을 만듭니다.

1행: Robot Status + Battery
2행: Velocity Control + Mode Control
3행: Options + Speed Limit
4행: Position + Long Task
5행: Log

4. 1행: Robot Status / Battery 영역 만들기

1) Robot Status GroupBox

왼쪽에서 Group Box를 끌어다 row1Layout 안에 넣습니다.

속성:

objectName: status_groupbox
title: Robot Status

그 안에 Label 2개를 넣습니다.

첫 번째 Label:

objectName: status_label
text: Robot Status: Waiting...

두 번째 Label:

objectName: time_label
text: Time: --

GroupBox 내부를 선택한 뒤:

Form → Lay Out Vertically

을 적용합니다.

2) Battery GroupBox

두 번째 Group Box를 넣습니다.

속성:

objectName: battery_groupbox
title: Battery

내부에 Label 하나와 Progress Bar 하나를 넣습니다.

Label:

objectName: battery_label
text: Battery: 100 %

Progress Bar:

objectName: battery_bar
value: 100
minimum: 0
maximum: 100
format: %p%

Battery GroupBox 내부에도 Vertical Layout을 적용합니다.

3) Horizontal Layout 추가

Robot Status Group Box와 Battery Group Box를 Ctrl 키를 누른 상태에서 선택합니다.

그리고 단축 아이콘에서 “Lay Out Horizontally”를 선택하면 2개의 그룹박스가 레이아웃에 들어갑니다.

Object Name:

row1Layout

5. 2행: Velocity Control / Mode Control 만들기

1) Velocity Control GroupBox

GroupBox 속성:

objectName: velocity_groupbox
title: Velocity Control

내부에 다음 위젯을 순서대로 넣습니다.

2) Linear speed 입력창

Line Edit 추가:

objectName: linear_speed_edit
placeholderText: Linear speed ex) 0.3

3) Angular speed 입력창

Line Edit 추가:

objectName: angular_speed_edit
placeholderText: Angular speed ex) 0.0

4) Publish 버튼

Push Button 추가:

objectName: publish_cmd_vel_button
text: Publish /cmd_vel

5) STOP 버튼

Push Button 추가:

objectName: stop_button
text: STOP

Velocity GroupBox 내부는 Vertical Layout으로 정렬합니다.

Form → Lay Out Vertically

6) Mode Control GroupBox

GroupBox 속성:

objectName: mode_groupbox
title: Mode Control

내부에 Combo Box를 넣습니다.

objectName: mode_combo

ComboBox 항목은 다음처럼 추가합니다.

MANUAL
AUTO
FOLLOW
EMERGENCY

Qt Designer에서 ComboBox를 더블클릭하거나, 속성창의 items 항목에서 편집할 수 있습니다.

Mode GroupBox 내부도 Vertical Layout 적용합니다.

7) row2Layout 만들기

Velocity Control Group Box와 Mode Control Group Box를 Ctrl 키를 누른 상태에서 선택합니다.

그리고 단축 아이콘에서 “Lay Out Horizontally”를 선택하면 2개의 그룹박스가 레이아웃에 들어갑니다.

6. 3행: Options / Speed Limit 만들기

1) Options GroupBox

GroupBox 속성:

objectName: options_groupbox
title: Options

내부에 Check Box 3개를 넣습니다.

objectName: obstacle_check
text: Obstacle Avoidance
objectName: 

text: Sensor Logging
objectName: camera_check
text: Camera Streaming

내부에 Vertical Layout 적용합니다.

2) Speed Limit GroupBox

GroupBox 속성:

objectName: speed_limit_groupbox
title: Speed Limit

내부에 다음 위젯을 넣습니다.

Slider

Horizontal Slider 추가:

objectName: speed_slider
minimum: 0
maximum: 100
value: 0
orientation: Horizontal

SpinBox

Spin Box 추가:

objectName: speed_spinbox
minimum: 0
maximum: 100
value: 0

내부는 Vertical Layout 또는 Grid Layout을 사용하면 됩니다.

그림처럼 위에 Slider, 아래에 SpinBox가 있으면 Vertical Layout이 충분합니다.

3) row3Layout 만들기

Options Group Box와 Speed Limit Group Box를 Ctrl 키를 누른 상태에서 선택합니다.

그리고 단축 아이콘에서 “Lay Out Horizontally”를 선택하면 2개의 그룹박스가 레이아웃에 들어갑니다.

objectName: row3Layout

7. 4행: Position / Long Task 만들기

1) Position GroupBox

GroupBox 속성:

objectName: position_groupbox
title: Position

내부에 Table Widget을 넣습니다.

objectName: position_table
rowCount: 2
columnCount: 2

헤더는 예를 들어 다음처럼 설정할 수 있습니다.

Column 1: X
Column 2: Y

그림에서는 기본 숫자 헤더 1, 2가 보이므로 헤더 이름을 따로 안 바꿔도 됩니다.

Position GroupBox 내부에 Vertical Layout 적용합니다.

2) Long Task GroupBox

GroupBox 속성:

objectName: long_task_groupbox
title: Long Task

내부에 다음 위젯을 넣습니다.

Task Status Label

objectName: task_status_label
text: Task Status: Ready

Progress Bar

objectName: task_progress_bar
minimum: 0
maximum: 100
value: 0
format: %p%

Button

objectName: system_check_button
text: Start System Check

내부에 Vertical Layout 적용합니다.

3) row4Layout 만들기

Position Group Box와 Long TaskGroup Box를 Ctrl 키를 누른 상태에서 선택합니다.

그리고 단축 아이콘에서 “Lay Out Horizontally”를 선택하면 2개의 그룹박스가 레이아웃에 들어갑니다.

objectName: row4Layout

8. 5행: Log 영역 만들기

마지막에 Group Box를 하나 넣습니다.

objectName: log_groupbox
title: Log

내부에 Plain Text Edit 또는 Text Edit를 넣습니다.

추천은 Plain Text Edit입니다.
로그 출력용이면 QTextEdit보다 QPlainTextEdit가 가볍습니다.

objectName: log_text
plainText:
readOnly: true

Log GroupBox 내부에 Vertical Layout을 적용합니다.

9. Stretch 조정하기

그림처럼 왼쪽 영역이 넓고 오른쪽 Mode Control이 좁게 보이려면 Layout Stretch를 조정해야 합니다.

예를 들어 row2Layout 안에서:

velocity_groupbox : mode_groupbox = 4 : 1

정도로 설정하면 됩니다.

Qt Designer에서 설정 방법:

  1. row2Layout 선택
  2. 오른쪽 Property Editor에서 layoutStretch 찾기
  3. 값 입력
4,1

다른 행도 비슷하게 설정합니다.

row1Layout: 1,3
row2Layout: 4,1
row3Layout: 1,3
row4Layout: 1,1

10. Size Policy 설정

UI가 찌그러지지 않게 하려면 주요 위젯의 Size Policy를 조정합니다.

1) Battery ProgressBar

horizontalPolicy: Expanding
verticalPolicy: Fixed

2) Velocity Control GroupBox

horizontalPolicy: Expanding
verticalPolicy: Preferred

3) Mode Control GroupBox

horizontalPolicy: Fixed 또는 Preferred
verticalPolicy: Preferred

4) Log Text

horizontalPolicy: Expanding
verticalPolicy: Expanding

Log 영역은 아래쪽에서 가장 많이 늘어나도

괜찮으므로 Expanding이 좋습니다.

11. Tab Order 설정

키보드 Tab 이동 순서를 정리합니다.

메뉴에서:

Edit → Edit Tab Order

순서는 보통 다음이 좋습니다.

linear_speed_edit
angular_speed_edit
publish_cmd_vel_button
stop_button
mode_combo
obstacle_check
sensor_check
camera_check
speed_slider
speed_spinbox
system_check_button

설정 후 다시:

Edit → Edit Widgets

로 돌아옵니다.

12. Signal / Slot 연결

Qt Designer에서도 간단한 연결은 할 수 있습니다.

예를 들어 speed_sliderspeed_spinbox 값을 서로 연결하려면:

  1. 상단 메뉴에서 선택
Edit → Edit Signals/Slots

  1. speed_slider를 드래그해서 speed_spinbox에 연결
  2. Signal 선택
valueChanged(int)
  1. Slot 선택
setValue(int)

반대로도 연결합니다.

speed_spinbox.valueChanged(int)
→ speed_slider.setValue(int)

다만 ROS 2 퍼블리시, 버튼 동작, 로그 출력 같은 기능은 Qt Designer에서 하지 말고 Python 또는 C++ 코드에서 연결하는 게 정석입니다.

13. UI 파일 저장

파일 이름은 예를 들어 다음처럼 저장합니다.

my_robot_control_gui.ui

ROS 2 패키지에서는 보통 다음 위치를 추천합니다.

pyqt_component_gui/
├── package.xml
├── setup.py
├── resource/
│   └── my_robot_control_gui.ui
└── pyqt_component_gui/
    ├── __init__.py
    └── main_gui.py

14. PyQt5에서 UI 불러오기 예시

ui 파일을 pyqt_component_gui 패키지로 복사합니다.

cd pyqt_study/
cp my_robot_control_gui.ui ~/pyqt_ws/src/pyqt_component_gui/resource/

cd ~/pyqt_ws/src/pyqt_component_gui/pyqt_component_gui/
touch main_gui.py

ROS 2 Python 노드에서 .ui 파일을 직접 로드할 수 있습니다.

import sys
import rclpy
import os
from ament_index_python.packages import get_package_share_directory

from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import QTimer

from rclpy.node import Node
from geometry_msgs.msg import Twist


class RobotControlNode(Node):
    def __init__(self):
        super().__init__('main_gui')
        self.cmd_vel_pub = self.create_publisher(Twist, '/cmd_vel', 10)


class RobotControlGUI(QWidget):
    def __init__(self, ros_node):
        super().__init__()

        self.ros_node = ros_node

        package_share_dir = get_package_share_directory('pyqt_component_gui')

        ui_path = os.path.join(
            package_share_dir,
            'resource',
            'my_robot_control_gui.ui'
        )

        uic.loadUi(ui_path, self)

        self.publish_cmd_vel_button.clicked.connect(self.publish_cmd_vel)
        self.stop_button.clicked.connect(self.stop_robot)
        self.system_check_button.clicked.connect(self.start_system_check)

        self.speed_slider.valueChanged.connect(self.speed_spinbox.setValue)
        self.speed_spinbox.valueChanged.connect(self.speed_slider.setValue)

        self.timer = QTimer()
        self.timer.timeout.connect(self.spin_ros_once)
        self.timer.start(10)

    def spin_ros_once(self):
        rclpy.spin_once(self.ros_node, timeout_sec=0.001)

    def publish_cmd_vel(self):
        try:
            linear = float(self.linear_speed_edit.text())
            angular = float(self.angular_speed_edit.text())
        except ValueError:
            self.log_text.appendPlainText('[ERROR] Invalid speed input')
            return

        msg = Twist()
        msg.linear.x = linear
        msg.angular.z = angular

        self.ros_node.cmd_vel_pub.publish(msg)

        self.log_text.appendPlainText(
            f'[CMD] /cmd_vel linear={linear}, angular={angular}'
        )

    def stop_robot(self):
        msg = Twist()
        msg.linear.x = 0.0
        msg.angular.z = 0.0

        self.ros_node.cmd_vel_pub.publish(msg)

        self.log_text.appendPlainText('[CMD] STOP')

    def start_system_check(self):
        self.task_status_label.setText('Task Status: Checking...')
        self.task_progress_bar.setValue(0)
        self.log_text.appendPlainText('[TASK] System check started')


def main():
    rclpy.init()

    app = QApplication(sys.argv)

    ros_node = RobotControlNode()
    gui = RobotControlGUI(ros_node)
    gui.show()

    exit_code = app.exec_()

    ros_node.destroy_node()
    rclpy.shutdown()

    sys.exit(exit_code)


if __name__ == '__main__':
    main()

주의할 점은 .ui 파일 경로입니다. 실행 위치에 따라 상대 경로가 안 맞을 수 있습니다. 실제 ROS 2 패키지에서는 ament_index_python으로 패키지 경로를 가져오는 방식이 더 안정적입니다.

1) import

from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import QTimer

PyQt5 관련 import입니다.

모듈역할
uicQt Designer에서 만든 .ui 파일을 Python에서 불러옴
QApplicationPyQt 프로그램 전체를 관리하는 객체
QWidgetGUI 창의 기본 클래스
QTimer일정 시간마다 함수를 실행하는 타이머

여기서 중요한 것은 uic입니다.

uic.loadUi('resource/robot_control_gui.ui', self)

이 코드가 Qt Designer로 만든 UI 파일을 실제 화면으로 불러옵니다.

2) RobotControlNode 클래스 설명

class RobotControlNode(Node):
def __init__(self):
super().__init__('robot_control_gui_node')
self.cmd_vel_pub = self.create_publisher(Twist, '/cmd_vel', 10)

이 클래스는 ROS 2 노드입니다.

즉, GUI 프로그램 안에서 ROS 2 통신을 담당하는 부분입니다.

3) /cmd_vel Publisher 생성

self.cmd_vel_pub = self.create_publisher(Twist, '/cmd_vel', 10)

이 코드는 /cmd_vel 토픽으로 Twist 메시지를 발행하는 Publisher를 만듭니다.

4) RobotControlGUI 클래스 설명

class RobotControlGUI(QWidget):

이 클래스는 GUI 창입니다.

QWidget을 상속했기 때문에 PyQt5 화면으로 표시될 수 있습니다.

5) 생성자

def __init__(self, ros_node):
super().__init__()

self.ros_node = ros_node

ros_node를 외부에서 받아서 GUI 안에 저장합니다.

즉, GUI 클래스 안에서 ROS 2 Publisher를 사용할 수 있게 만드는 코드입니다.

self.ros_node.cmd_vel_pub.publish(msg)

이런 식으로 GUI 버튼 함수에서 ROS 2 메시지를 발행할 수 있습니다.

6) Qt Designer UI 파일 로드

uic.loadUi('resource/robot_control_gui.ui', self)

이 코드는 Qt Designer에서 만든 UI 파일을 불러옵니다.

즉, robot_control_gui.ui 안에 있는 버튼, 라벨, 프로그레스바, 입력창 등이 현재 클래스 안에 연결됩니다.

예를 들어 Qt Designer에서 버튼 이름을 이렇게 만들었다면:

objectName: stop_button

Python 코드에서는 바로 이렇게 접근할 수 있습니다.

self.stop_button

입력창 이름이 이렇게 되어 있다면:

objectName: linear_speed_edit

Python 코드에서는 이렇게 접근합니다.

self.linear_speed_edit.text()

중요합니다.
Qt Designer에서 objectName이 코드와 다르면 실행 시 오류가 납니다.

예를 들어 UI 파일에 stop_button이 없으면 이런 오류가 납니다.

AttributeError: 'RobotControlGUI' object has no attribute 'stop_button'

7) 버튼과 함수 연결

self.publish_cmd_vel_button.clicked.connect(self.publish_cmd_vel)
self.stop_button.clicked.connect(self.stop_robot)
self.system_check_button.clicked.connect(self.start_system_check)

이 부분은 버튼 클릭 이벤트를 함수와 연결합니다.

버튼 objectName클릭 시 실행되는 함수
publish_cmd_vel_buttonpublish_cmd_vel()
stop_buttonstop_robot()
system_check_buttonstart_system_check()

즉, 사용자가 Publish /cmd_vel 버튼을 누르면 이 함수가 실행됩니다.

def publish_cmd_vel(self):

사용자가 STOP 버튼을 누르면 이 함수가 실행됩니다.

def stop_robot(self):

8) Slider와 SpinBox 연결

self.speed_slider.valueChanged.connect(self.speed_spinbox.setValue)
self.speed_spinbox.valueChanged.connect(self.speed_slider.setValue)

이 코드는 속도 제한 UI를 서로 연결합니다.

예를 들어 Slider를 움직이면 SpinBox 숫자가 같이 바뀝니다.

Slider 변경 → SpinBox 값 변경

반대로 SpinBox 값을 바꾸면 Slider 위치도 같이 바뀝니다.

SpinBox 변경 → Slider 위치 변경

즉, 두 위젯이 같은 값을 공유하게 됩니다.

단, 현재 코드에서는 이 speed_slider, speed_spinbox 값이 실제 /cmd_vel 계산에 사용되지는 않습니다.
지금은 UI 동작 확인용입니다.

실제 속도 제한까지 적용하려면 publish_cmd_vel()에서 다음처럼 반영해야 합니다.

speed_limit = self.speed_spinbox.value() / 100.0
msg.linear.x = linear * speed_limit
msg.angular.z = angular * speed_limit

9) QTimer 설명

self.timer = QTimer()
self.timer.timeout.connect(self.spin_ros_once)
self.timer.start(10)

이 부분은 매우 중요합니다.

PyQt5와 ROS 2는 각각 자기만의 반복 실행 구조가 있습니다.

PyQt5는 이것을 사용합니다.

app.exec_()

ROS 2는 보통 이것을 사용합니다.

rclpy.spin(node)

그런데 둘을 동시에 그냥 실행하면 문제가 생깁니다.
app.exec_()도 계속 실행되고, rclpy.spin()도 계속 실행되기 때문입니다.

그래서 이 코드에서는 rclpy.spin()을 직접 쓰지 않고, QTimer로 아주 짧은 주기마다 rclpy.spin_once()를 호출합니다.

def spin_ros_once(self):
rclpy.spin_once(self.ros_node, timeout_sec=0.001)

즉, 구조는 이렇습니다.

PyQt5 메인 루프 실행 중

10ms마다 spin_ros_once() 호출

ROS 2 이벤트를 조금씩 처리

self.timer.start(10)의 의미는 다음과 같습니다.

10ms마다 timeout 발생

따라서 대략 1초에 100번 정도 spin_ros_once()가 호출됩니다.

10) spin_ros_once 함수 설명

def spin_ros_once(self):
rclpy.spin_once(self.ros_node, timeout_sec=0.001)

ROS 2 이벤트를 한 번만 처리하는 함수입니다.

현재 코드에서는 Subscriber가 없기 때문에 큰 역할은 없어 보일 수 있습니다.

하지만 나중에 다음 기능을 추가하면 꼭 필요합니다.

/odom 구독
/battery_state 구독
/robot_status 구독
센서 데이터 구독
서비스 응답 처리
액션 상태 처리

즉, 지금은 확장성을 위해 들어가 있는 코드입니다.

11) publish_cmd_vel 함수 설명

def publish_cmd_vel(self):
try:
linear = float(self.linear_speed_edit.text())
angular = float(self.angular_speed_edit.text())
except ValueError:
self.log_text.appendPlainText('[ERROR] Invalid speed input')
return

이 함수는 Publish /cmd_vel 버튼을 눌렀을 때 실행됩니다.

먼저 GUI 입력창에서 값을 읽습니다.

self.linear_speed_edit.text()
self.angular_speed_edit.text()

예를 들어 사용자가 입력창에 다음처럼 입력했다고 가정합니다.

linear_speed_edit: 0.3
angular_speed_edit: 0.0

그러면 코드에서는 문자열로 읽힙니다.

"0.3"
"0.0"

그래서 float()로 숫자로 변환합니다.

linear = float("0.3")
angular = float("0.0")

결과:

linear = 0.3
angular = 0.0

12) 잘못된 입력 처리

사용자가 숫자가 아니라 문자를 입력하면 오류가 납니다.

예:

abc

이 경우 float("abc")는 실패합니다.

그래서 try-except로 처리합니다.

except ValueError:
self.log_text.appendPlainText('[ERROR] Invalid speed input')
return

오류가 발생하면 로그창에 다음 메시지를 출력하고 함수 실행을 중단합니다.

[ERROR] Invalid speed input

13) Twist 메시지 생성

msg = Twist()
msg.linear.x = linear
msg.angular.z = angular

Twist 메시지를 하나 만듭니다.

그리고 입력받은 값을 넣습니다.

의미
msg.linear.x전진/후진 속도
msg.angular.z좌회전/우회전 회전 속도

14) /cmd_vel 발행

self.ros_node.cmd_vel_pub.publish(msg)

이 코드가 실제로 ROS 2 토픽을 발행하는 부분입니다.

즉, /cmd_velTwist 메시지가 나갑니다.

확인하려면 다른 터미널에서 다음 명령을 실행하면 됩니다.

ros2 topic echo /cmd_vel

GUI에서 버튼을 누르면 다음과 비슷하게 출력됩니다.

linear:
x: 0.3
y: 0.0
z: 0.0
angular:
x: 0.0
y: 0.0
z: 0.0

15) 로그 출력

self.log_text.appendPlainText(
f'[CMD] /cmd_vel linear={linear}, angular={angular}'
)

GUI 아래쪽 로그창에 명령 내역을 출력합니다.

예:

[CMD] /cmd_vel linear=0.3, angular=0.0

16) stop_robot 함수 설명

def stop_robot(self):
msg = Twist()
msg.linear.x = 0.0
msg.angular.z = 0.0

self.ros_node.cmd_vel_pub.publish(msg)

self.log_text.appendPlainText('[CMD] STOP')

이 함수는 STOP 버튼을 눌렀을 때 실행됩니다.

역할은 단순합니다.

linear.x = 0.0
angular.z = 0.0

즉, 로봇에게 정지 명령을 보냅니다.

발행되는 메시지는 다음과 같습니다.

linear:
x: 0.0
angular:
z: 0.0

주의할 점은, 실제 로봇에서 /cmd_vel을 한 번만 0으로 보냈다고 완전히 안전하다고 보면 안 됩니다.
실제 주행 로봇에서는 정지 명령을 일정 시간 반복 발행하거나, 하드웨어 비상 정지 로직을 따로 두는 것이 안전합니다.

17) start_system_check 함수 설명

def start_system_check(self):
self.task_status_label.setText('Task Status: Checking...')
self.task_progress_bar.setValue(0)
self.log_text.appendPlainText('[TASK] System check started')

이 함수는 Start System Check 버튼을 눌렀을 때 실행됩니다.

현재는 실제 시스템 점검을 하는 코드는 아닙니다.

하는 일은 3개뿐입니다.

1. 상태 라벨 문구 변경
2. 진행률 바를 0으로 초기화
3. 로그창에 메시지 출력

즉, 아직은 “버튼 눌렀을 때 UI가 반응하는지 확인하는 테스트용 함수”에 가깝습니다.

실제로 점검 기능을 넣으려면 예를 들어 다음을 추가할 수 있습니다.

- /battery_state 구독 상태 확인
- /odom 수신 여부 확인
- /cmd_vel publisher 연결 확인
- 센서 토픽 수신 여부 확인
- 모터 드라이버 상태 확인

18) main 함수 설명

def main():
rclpy.init()

ROS 2를 초기화합니다.

ROS 2 노드를 만들기 전에 반드시 호출해야 합니다.

app = QApplication(sys.argv)

PyQt5 애플리케이션 객체를 만듭니다. PyQt GUI 프로그램에서는 반드시 필요합니다.

ros_node = RobotControlNode()

ROS 2 노드를 생성합니다.

이 시점에서 /cmd_vel Publisher가 만들어집니다.

gui = RobotControlGUI(ros_node)
gui.show()

GUI 창을 생성하고 화면에 표시합니다. 여기서 ros_node를 GUI에 넘깁니다.

그래서 GUI 클래스 안에서 ROS 2 Publisher를 사용할 수 있습니다.

exit_code = app.exec_()

PyQt5 이벤트 루프를 시작합니다.

이 코드가 실행되면 GUI 창이 계속 떠 있게 됩니다.

버튼 클릭, 입력창 수정, 타이머 동작 등이 이 이벤트 루프 안에서 처리됩니다.

ros_node.destroy_node()
rclpy.shutdown()

GUI 창이 닫힌 뒤 ROS 2 노드를 정리하고 종료합니다.

sys.exit(exit_code)

프로그램을 정상 종료합니다.

15. ROS 2 패키지에서 UI 파일 경로 안정적으로 잡기

setup.py에 UI 파일을 설치하도록 추가합니다.

from setuptools import setup
import os
from glob import glob

package_name = pyqt_component_gui'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        (os.path.join('share', package_name, 'resource'),
            glob('resource/*.ui')),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='user',
    maintainer_email='user@example.com',
    description='ROS2 PyQt Robot Control GUI',
    license='MIT',
    entry_points={
        'console_scripts': [
          'main_gui = pyqt_component_gui.main_gui:main',
        ],
    },
)

Python 코드에서는 이렇게 불러옵니다.

import os
from ament_index_python.packages import get_package_share_directory

ui_path = os.path.join(
get_package_share_directory('robot_control_gui'),
'resource',
'robot_control_gui.ui'
)

uic.loadUi(ui_path, self)

16. 빌드 및 실행

패키지 루트 기준으로 빌드합니다.

cd ~/pyqt_ws
colcon build --packages-select robot_control_gui_node
source install/setup.bash
ros2 run robot_control_gui robot_control_gui_node

Leave a Comment